mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
Repo cleanup: remove ~3.3GB of cruft, dead code, and stale docs
Disk/data: - Delete scripts/ entirely: 3.2GB of gitignored Jan-Feb run telemetry plus the legacy per-skill script/lab_log layout superseded by bots/ (tracked files remain recoverable from git history) - Prune bots/ to the 10 most-used bots (claude46, Codex7, max, codexpl8, runepl8, maxbittker, codex53, aaravA, codex_53, multibot) + _template; all removed dirs archived to ~/rs-sdk-cleanup-archive-2026-07-13.tgz along with stream/ (held live Twitch/bot/Anthropic credentials) - Remove orphaned sdk/node_modules + sdk/bun.lock (pathfinder is vendored in server/vendor; sdk has no deps of its own) Git hygiene: - Untrack sdk/collision-data.json (22MB blob per regen; kept on disk, regen curl documented in DEVELOPERS.md and .gitignore) - gitignore backups/, .obsidian/, and engine public/client/*.js (the gap that let stale client.js shadow fresh webclient builds return) - Delete the untracked stale public/client/client.js shadow copy Dead code/config: - Drop unused deps: tmi.js, @types/tmi.js, ts-morph - Drop dead tsconfig globs (stream/, bot_arcs/) - Remove unused nix toolchain (flake.nix, flake.lock, .envrc) Docs: - GEMINI.md/agents.md were byte-identical 4-month-stale copies of claude.md; replace with symlinks so all tools read one file - DEVELOPERS.md: remove abolished copy-client.js-to-engine step and nonexistent agent//javaclient sections; document collision regen - sdk/README.md: fix reference to nonexistent scripts/example-remote.ts - Remove stale sdk/SDK_DESIGN.md and misleading sdk/test/test_status.md Also commit the forgotten sdk/test/click-dialog-by-text.test.ts (tests a shipped API, was never git-added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b30931f040
commit
716a9375f9
56 files changed
+199
-13854
No files matched your search
+5
-3
@@ -10,9 +10,11 @@ public-client/
|
||||
server.json
|
||||
.env
|
||||
|
||||
# Nix
|
||||
result
|
||||
.direnv/
|
||||
backups/
|
||||
.obsidian/
|
||||
|
||||
# Regenerable: curl https://rs-sdk-demo.fly.dev/api/exportCollision > sdk/collision-data.json
|
||||
sdk/collision-data.json
|
||||
|
||||
# Bot folders (except template)
|
||||
bots/*/
|
||||
|
||||
+10
-26
@@ -30,9 +30,9 @@ The game server handling world simulation, player logic, and network protocol.
|
||||
```
|
||||
engine/
|
||||
├── src/ # Server source code
|
||||
├── public/ # Static files served to clients
|
||||
│ ├── client/ # Standard web client build
|
||||
│ └── bot/ # Bot client build (with BotSDK)
|
||||
├── public/ # Static assets (soundfont, wasm). Built clients are served
|
||||
│ # directly from webclient/out — do NOT copy client.js here,
|
||||
│ # it would shadow fresh builds (see commit 6569364d2)
|
||||
├── view/ # EJS templates (bot.ejs for bot interface)
|
||||
├── data/ # Runtime game data
|
||||
├── prisma/ # Database schema and migrations
|
||||
@@ -111,32 +111,16 @@ The interactive menu provides options to:
|
||||
| `bun run build` | Build client bundles (standard + bot) |
|
||||
| `bun run build:dev` | Build in development mode |
|
||||
|
||||
After building, copy to engine:
|
||||
No copy step is needed after building — the engine serves the built bundles
|
||||
directly from `webclient/out`.
|
||||
|
||||
### Collision data (`sdk/collision-data.json`)
|
||||
|
||||
Used by `sdk/pathfinding.ts` and not tracked in git. Regenerate it from a running server:
|
||||
```sh
|
||||
cp out/standard/client.js ../engine/public/client/client.js
|
||||
cp out/bot/client.js ../engine/public/bot/client.js
|
||||
curl https://rs-sdk-demo.fly.dev/api/exportCollision > sdk/collision-data.json
|
||||
```
|
||||
|
||||
### Agent (`cd agent`)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bun run gateway` | Start gateway (unified sync + controller) |
|
||||
| `bun run gateway:dev` | Gateway with hot-reload |
|
||||
| `bun run agent` | Start Claude Agent service |
|
||||
| `bun run agent:dev` | Agent service with hot-reload |
|
||||
| `bun run cli` | Run agent CLI |
|
||||
| `bun cli.ts launch <bot> "goal"` | Launch browser + start agent |
|
||||
| `bun cli.ts status` | View connected bots |
|
||||
| `bun run login` | Automated login helper |
|
||||
|
||||
### Java Client (`cd javaclient`)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `./gradlew build` | Build the Java client |
|
||||
| `./gradlew run --args="10 0 highmem members 32"` | Run the Java client |
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -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!
|
||||
@@ -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!
|
||||
@@ -1,17 +1,15 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^7.5.3",
|
||||
"open": "^11.0.0",
|
||||
"tmi.js": "^1.8.5",
|
||||
"ts-morph": "^27.0.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/tmi.js": "^1.8.6",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
@@ -47,24 +45,16 @@
|
||||
|
||||
"@inquirer/type": ["@inquirer/type@3.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA=="],
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
|
||||
|
||||
"@types/tmi.js": ["@types/tmi.js@1.8.6", "", {}, "sha512-LVzNK7AxTMyh9qHLanAQR1o0I9XzfbIcXk85cx85igmCJHHO1Sm71sdhQ8Mj1WmRGzynPIoCXx6mVaFynWbsQw=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
@@ -73,8 +63,6 @@
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
@@ -89,8 +77,6 @@
|
||||
|
||||
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
@@ -103,20 +89,12 @@
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
@@ -129,30 +107,16 @@
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tmi.js": ["tmi.js@1.8.5", "", { "dependencies": { "node-fetch": "^2.6.1", "ws": "^8.2.0" } }, "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew=="],
|
||||
|
||||
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="],
|
||||
|
||||
"type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
|
||||
|
||||
Generated
-27
@@ -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
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
description = "rs-sdk dev shell";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, ... }:
|
||||
let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
+2
-5
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
# Lab Log: Agility Training
|
||||
|
||||
## Overview
|
||||
|
||||
**Goal**: Train Agility from level 1 to 10+ using the Gnome Stronghold Agility Course
|
||||
|
||||
**Starting Preset**: `TestPresets.LUMBRIDGE_SPAWN` (fresh account at Lumbridge)
|
||||
|
||||
**Challenge**: The Gnome Stronghold is located very far northwest (~400+ tiles from Lumbridge). This requires a long travel phase before training can begin.
|
||||
|
||||
## Course Information
|
||||
|
||||
### Gnome Stronghold Agility Course
|
||||
- **Location**: (2474, 3436) - Northwest of Ardougne
|
||||
- **Requirements**: Level 1 Agility (starter course)
|
||||
- **Members-only**: Yes (may be blocked on some servers)
|
||||
|
||||
### Course Layout (7 obstacles in order)
|
||||
| # | Obstacle | Action | XP |
|
||||
|---|----------|--------|-----|
|
||||
| 1 | Log balance | Walk-across | 7.5 |
|
||||
| 2 | Obstacle net | Climb-over | 7.5 |
|
||||
| 3 | Tree branch | Climb | 5 |
|
||||
| 4 | Balancing rope | Walk-on | 7.5 |
|
||||
| 5 | Tree branch | Climb-down | 5 |
|
||||
| 6 | Obstacle net | Climb-over | 7.5 |
|
||||
| 7 | Obstacle pipe | Squeeze-through | 7.5 |
|
||||
| - | Lap bonus | - | 39 |
|
||||
| **Total** | | | **~86.5** |
|
||||
|
||||
### XP Requirements
|
||||
- Level 10 = 1,154 XP
|
||||
- Laps needed = ~14
|
||||
|
||||
---
|
||||
|
||||
## Run 001 - Initial Test
|
||||
|
||||
**Date**: 2026-01-27
|
||||
**Timeout**: 10 min
|
||||
**Stall timeout**: 90s
|
||||
|
||||
**Hypothesis**: The script should travel from Lumbridge to Gnome Stronghold and begin training.
|
||||
|
||||
### What Happened
|
||||
|
||||
Travel progressed well through the southern route:
|
||||
1. Lumbridge (3222, 3218) -> West of Lumbridge ✓
|
||||
2. Near Draynor Manor road ✓
|
||||
3. Draynor area ✓ (took some HP damage from wandering monsters)
|
||||
4. West of Draynor ✓
|
||||
5. Port Sarim area ✓
|
||||
6. Rimmington area ✓
|
||||
7. **BLOCKED at (2908, 3319)** - South of Falador
|
||||
|
||||
### Outcome
|
||||
|
||||
**FAILED** - Path blocked south of Falador
|
||||
|
||||
The pathfinder returned "No path found" for all waypoints north of position (2908, 3319). This suggests:
|
||||
- The Falador walls block direct entry
|
||||
- The gate/entrance may not be pathable
|
||||
- Or the area north is members-only content
|
||||
|
||||
### Analysis
|
||||
|
||||
Position (2908, 3319) is approximately:
|
||||
- South of Falador's western walls
|
||||
- Near the crafting guild area
|
||||
- The city of Falador has walls that require using gates
|
||||
|
||||
The Gnome Stronghold is in a members-only area. The path north from Rimmington goes through:
|
||||
- Falador (need to enter through a gate)
|
||||
- Taverley (members-only)
|
||||
- Catherby area (members-only)
|
||||
- Gnome Stronghold (members-only)
|
||||
|
||||
### Root Cause
|
||||
|
||||
**Members-only content is not accessible from a F2P spawn point.**
|
||||
|
||||
The Gnome Stronghold Agility Course is the only level 1 agility course, but it's in a members-only area. Without members access, there is no way to train Agility from level 1 using LUMBRIDGE_SPAWN preset.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. Members-Only Area
|
||||
The Gnome Stronghold is in a members-only region. If the server doesn't support members content, travel will fail.
|
||||
|
||||
**Detection**: Script will log position when it gets stuck and throw an error.
|
||||
|
||||
### 2. Long Travel Time
|
||||
The journey from Lumbridge to Gnome Stronghold is ~400+ tiles:
|
||||
- Lumbridge (3222, 3218)
|
||||
- Gnome Stronghold (2474, 3436)
|
||||
- Requires passing through Draynor, Falador, Taverley
|
||||
|
||||
**Mitigation**: 90s stall timeout, 10 min total time limit.
|
||||
|
||||
### 3. Obstacles Have Directional Requirements
|
||||
Some obstacles can only be approached from certain directions:
|
||||
- `gnome_obstacle_net_1`: Must approach from south (z > loc z)
|
||||
- `gnome_obstacle_net_2`: Must approach from north (z < loc z)
|
||||
- `gnome_obstacle_pipe`: Must enter from south
|
||||
|
||||
**Mitigation**: Course is designed to flow in one direction. Script follows natural order.
|
||||
|
||||
---
|
||||
|
||||
## Obstacle Interaction Patterns
|
||||
|
||||
From `content/scripts/skill_agility/scripts/gnome_course.rs2`:
|
||||
|
||||
```rs2
|
||||
[oploc1,gnome_log_balance]
|
||||
~agility_force_move(75, human_walk_logbalance, movecoord(coord, 0, 0, -7));
|
||||
|
||||
[oploc1,gnome_obstacle_net_1]
|
||||
if(coordz(coord) <= coordz(loc_coord)) {
|
||||
mes("You can not do that from here.");
|
||||
return;
|
||||
}
|
||||
~agility_climb_up(75, movecoord(coord, 0, 1, -2));
|
||||
|
||||
[oploc1,gnome_obstacle_pipe]
|
||||
if(coordz(coord) > coordz(loc_coord)) {
|
||||
mes("You can't enter the pipe from this side.");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Key observations:
|
||||
- Each obstacle awards 5-7.5 XP individually
|
||||
- Completing all 7 obstacles in sequence awards 39 XP bonus
|
||||
- Course progress tracked via `%gnome_course_progress` varp (1-7, resets to 0)
|
||||
|
||||
---
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
### 1. Spawn at Gnome Stronghold
|
||||
Could modify preset to spawn at the course, but this violates the methodology:
|
||||
> "Preset is a constraint, not a variable"
|
||||
|
||||
### 2. Barbarian Outpost Course
|
||||
Another option at level 35, but requires higher level and is also far from Lumbridge.
|
||||
|
||||
### 3. Wilderness Agility Course
|
||||
Level 52 requirement, dangerous area. Not suitable for level 1.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
### STATUS: BLOCKED - MEMBERS-ONLY CONTENT
|
||||
|
||||
**Agility cannot be trained from level 1 using `TestPresets.LUMBRIDGE_SPAWN`.**
|
||||
|
||||
The only level 1 Agility course (Gnome Stronghold) is in a members-only area that is inaccessible from Lumbridge.
|
||||
|
||||
### Travel Test Results
|
||||
|
||||
| Waypoint | Coordinates | Status |
|
||||
|----------|-------------|--------|
|
||||
| Lumbridge | (3222, 3218) | Start |
|
||||
| West of Lumbridge | (3200, 3210) | ✓ |
|
||||
| Near Draynor Manor | (3170, 3220) | ✓ |
|
||||
| Draynor area | (3100, 3250) | ✓ |
|
||||
| West of Draynor | (3050, 3260) | ✓ |
|
||||
| Port Sarim | (2980, 3270) | ✓ |
|
||||
| Rimmington | (2920, 3280) | ✓ |
|
||||
| South of Falador | (2920, 3350) | **BLOCKED** |
|
||||
| Gnome Stronghold | (2474, 3436) | Unreachable |
|
||||
|
||||
**Maximum distance traveled**: ~315 tiles (Lumbridge to Rimmington)
|
||||
**Remaining distance**: ~450 tiles to Gnome Stronghold
|
||||
|
||||
### What Would Be Needed
|
||||
|
||||
To train Agility from level 1, one of these would be required:
|
||||
1. **Members-only preset** that spawns at or near Gnome Stronghold
|
||||
2. **Server-side change** to make Gnome area accessible
|
||||
3. **Alternative course** that doesn't exist in classic RS (e.g., a F2P agility course)
|
||||
|
||||
### Learnings
|
||||
|
||||
1. **Members-only content** - Agility is fundamentally a members-only skill in classic RS
|
||||
2. **Pathfinding works** - The `bot.walkTo()` successfully navigated ~315 tiles
|
||||
3. **Long-distance travel** - The waypoint system worked well for multi-leg journeys
|
||||
4. **HP regeneration** - Bot took some combat damage but HP naturally regenerated during travel
|
||||
|
||||
---
|
||||
|
||||
## Run 002 - Fixed Waypoints (Falador Gate Route)
|
||||
|
||||
**Date**: 2026-01-27
|
||||
**Change**: Fixed waypoints to route through Falador's south gate instead of walking straight north into walls
|
||||
|
||||
### Problem
|
||||
Previous waypoints tried to go straight north from Rimmington (2920, 3280) to "South of Falador" (2920, 3350), which walked directly into Falador's walls and got stuck at (2908, 3319).
|
||||
|
||||
### Fix Applied
|
||||
Changed waypoints to:
|
||||
1. From Rimmington, go **EAST** first toward Falador south gate (~2965, 3305)
|
||||
2. Enter through south gate entrance (~2965, 3340)
|
||||
3. Navigate through Falador city center
|
||||
4. Exit through west/north side toward Taverley
|
||||
|
||||
### What Happened
|
||||
|
||||
Travel progression:
|
||||
1. Lumbridge (3222, 3218) -> West of Lumbridge ✓
|
||||
2. Near Draynor Manor road ✓
|
||||
3. Draynor area ✓
|
||||
4. West of Draynor ✓
|
||||
5. Port Sarim ✓
|
||||
6. Rimmington (2921, 3271) ✓
|
||||
7. **Approach Falador south gate (2961, 3302) ✓** ← NEW WAYPOINT
|
||||
8. South gate entrance - struggled but got in via east side (3001, 3356) ✓
|
||||
9. **Inside Falador south (2948, 3376) ✓** ← MADE IT INSIDE!
|
||||
10. **Falador center (2951, 3395) ✓** ← NEW PROGRESS!
|
||||
11. **BLOCKED at (2946, 3407)** - Cannot exit north to Taverley
|
||||
|
||||
### Outcome
|
||||
|
||||
**PARTIAL SUCCESS** - Waypoint fix worked, now blocked at members-only boundary
|
||||
|
||||
| Metric | Run 001 | Run 002 | Change |
|
||||
|--------|---------|---------|--------|
|
||||
| Distance traveled | ~315 tiles | ~550 tiles | +235 tiles |
|
||||
| Stuck position | (2908, 3319) | (2946, 3407) | Inside Falador now |
|
||||
| Progress | South of Falador walls | Inside Falador center | Much better |
|
||||
| Remaining distance | ~450 tiles | ~473 tiles | Similar |
|
||||
|
||||
### Analysis
|
||||
|
||||
1. **Waypoint fix successful** - Bot no longer gets stuck at Falador walls
|
||||
2. **Got inside Falador** - Bot reached (2946, 3407) which is inside the city
|
||||
3. **Members-only boundary confirmed** - No path found to any waypoint north/west of Falador
|
||||
4. **Taverley is blocked** - All paths to Taverley (2880, 3440) return "No path found"
|
||||
|
||||
### Root Cause Confirmed
|
||||
|
||||
**The Gnome Stronghold is in members-only territory.** The boundary is at the north edge of Falador. Everything beyond (Taverley, Catherby, Gnome Stronghold) is inaccessible without members access.
|
||||
|
||||
### Updated Travel Map
|
||||
|
||||
```
|
||||
Lumbridge (3222, 3218)
|
||||
↓ ~22 tiles
|
||||
West of Lumbridge (3200, 3210) ✓
|
||||
↓ ~30 tiles
|
||||
Draynor area (3100, 3250) ✓
|
||||
↓ ~50 tiles
|
||||
Port Sarim (2980, 3270) ✓
|
||||
↓ ~60 tiles
|
||||
Rimmington (2920, 3280) ✓
|
||||
↓ ~45 tiles (go EAST first!)
|
||||
Falador South Gate (2965, 3305) ✓
|
||||
↓ ~35 tiles
|
||||
Inside Falador (2948, 3376) ✓
|
||||
↓ ~25 tiles
|
||||
Falador Center (2946, 3407) ✓
|
||||
↓ BLOCKED - Members-only
|
||||
Taverley (2880, 3440) ✗
|
||||
↓
|
||||
Gnome Stronghold (2474, 3436) ✗
|
||||
```
|
||||
|
||||
**Total F2P traversable distance**: ~550 tiles (Lumbridge to Falador center)
|
||||
|
||||
---
|
||||
|
||||
## Run 003 - Manual Gate Navigation for Falador-Taverley
|
||||
|
||||
**Date**: 2026-01-27
|
||||
**Change**: Implemented manual gate handling since pathfinder can't traverse the Falador-Taverley passage
|
||||
|
||||
### Problem from Run 002
|
||||
Bot got stuck at Falador center (2946, 3407) because the pathfinder couldn't handle the gate/passage to Taverley. However, the gates ARE clickable and Taverley IS accessible on this server.
|
||||
|
||||
### Fix Applied
|
||||
Added manual gate navigation:
|
||||
1. Detect when bot is in "Falador gate zone" (x: 2920-2960, z: 3400-3450)
|
||||
2. Walk northwest toward gate area with smaller waypoints
|
||||
3. Use `getNearbyLocs()` to find gates/doors/passages
|
||||
4. Use `sendInteractLoc()` to click/open gates
|
||||
5. Handle any dialogs that appear
|
||||
6. Walk through after clicking
|
||||
|
||||
Key new functions:
|
||||
- `isPastTaverleyGate()` - Check if x < 2900 (past gate area)
|
||||
- `isInFaladorGateZone()` - Detect when manual navigation needed
|
||||
- `findAndUseGate()` - Search for and interact with gates
|
||||
- `navigateFaladorToTaverley()` - Orchestrate gate passage
|
||||
|
||||
### What Happened
|
||||
|
||||
**MAJOR SUCCESS** - Bot passed through Falador-Taverley gate!
|
||||
|
||||
Travel progression:
|
||||
1. Lumbridge -> Falador center ✓ (same as Run 002)
|
||||
2. Detected "gate zone" at (2952, 3395) ✓
|
||||
3. **Switched to manual gate navigation** ✓
|
||||
4. Found doors at (2949, 3450) and gates at (2935, 3450) ✓
|
||||
5. **Clicked Door, walked through** ✓ - Position changed (2949, 3450) -> (2944, 3455)
|
||||
6. Found more gates at (2935, 3450) ✓
|
||||
7. **Clicked Gate, walked through** ✓ - Position changed (2936, 3451) -> (2932, 3453)
|
||||
8. **Exited to Taverley area** - reached (2880, 3434) ✓
|
||||
9. Continued west toward Catherby - reached (2704, 3440) ✓
|
||||
10. **NEW BLOCKER at (2641, 3448)** - Near White Wolf Mountain
|
||||
|
||||
### Gate Detection Output
|
||||
|
||||
```
|
||||
Found 4 potential gates/doors:
|
||||
- Door at (2949, 3450) dist=5.0 opts=[Open]
|
||||
- Door at (2952, 3453) dist=8.0 opts=[Open]
|
||||
- Gate at (2935, 3450) dist=10.0 opts=[Open]
|
||||
- Gate at (2935, 3451) dist=10.0 opts=[Open]
|
||||
```
|
||||
|
||||
The `findAndUseGate()` function successfully:
|
||||
1. Found gates using `/gate|door|passage|entrance|exit|barrier/i` regex
|
||||
2. Identified "Open" option on the gates
|
||||
3. Walked closer when needed
|
||||
4. Clicked the gate with `sendInteractLoc()`
|
||||
5. Walked through after clicking
|
||||
|
||||
### Outcome
|
||||
|
||||
| Metric | Run 002 | Run 003 | Change |
|
||||
|--------|---------|---------|--------|
|
||||
| Distance traveled | ~550 tiles | ~600+ tiles | **+50+ tiles** |
|
||||
| Stuck position | (2946, 3407) | (2625, 3398) | **Past Taverley!** |
|
||||
| Progress | Falador center | West of Catherby | **Major progress** |
|
||||
| Remaining distance | ~473 tiles | ~156 tiles | **-317 tiles!** |
|
||||
|
||||
### Analysis
|
||||
|
||||
1. **Gate handling worked!** - `findAndUseGate()` successfully found and clicked through gates
|
||||
2. **Multiple gates needed** - Had to pass through at least 2 doors/gates in the area
|
||||
3. **Taverley accessible** - This server DOES have Taverley accessible
|
||||
4. **New blocker: White Wolf Mountain** - Around (2641, 3448) there's another obstacle
|
||||
|
||||
### New Blocker: White Wolf Mountain Area
|
||||
|
||||
After passing Taverley, the bot got stuck around (2641, 3448) / (2625, 3398). This area is:
|
||||
- Between Taverley and Catherby
|
||||
- Near White Wolf Mountain
|
||||
- May have wolves that attack or terrain obstacles
|
||||
|
||||
The pathfinder returned "No path found" suggesting:
|
||||
- Mountain terrain blocks direct path
|
||||
- May need to go around (north or south)
|
||||
- Or there's another gate/passage needed
|
||||
|
||||
### Updated Travel Map
|
||||
|
||||
```
|
||||
Lumbridge (3222, 3218)
|
||||
↓ ~22 tiles
|
||||
West of Lumbridge (3200, 3210) ✓
|
||||
↓ ~30 tiles
|
||||
Draynor area (3100, 3250) ✓
|
||||
↓ ~50 tiles
|
||||
Port Sarim (2980, 3270) ✓
|
||||
↓ ~60 tiles
|
||||
Rimmington (2920, 3280) ✓
|
||||
↓ ~45 tiles
|
||||
Falador South Gate (2965, 3305) ✓
|
||||
↓ ~35 tiles
|
||||
Inside Falador (2948, 3376) ✓
|
||||
↓ ~25 tiles
|
||||
Falador Center (2952, 3395) ✓
|
||||
↓ MANUAL GATE NAVIGATION
|
||||
North Falador (2947, 3419) ✓
|
||||
↓ Gate/Door at (2949, 3450) - CLICKED THROUGH
|
||||
Near Taverley gate (2945, 3445) ✓
|
||||
↓ Gate at (2935, 3450) - CLICKED THROUGH
|
||||
Taverley area (2880, 3434) ✓ ← NEW PROGRESS!
|
||||
↓ ~75 tiles
|
||||
West of Taverley (2779, 3442) ✓ ← NEW PROGRESS!
|
||||
↓ ~75 tiles
|
||||
Toward Catherby (2704, 3440) ✓ ← NEW PROGRESS!
|
||||
↓ BLOCKED
|
||||
White Wolf Mountain area (2641, 3448) ✗
|
||||
↓
|
||||
Gnome Stronghold (2474, 3436) ✗
|
||||
```
|
||||
|
||||
### Key Learnings
|
||||
|
||||
1. **Manual gate handling works** - When pathfinder fails, use `getNearbyLocs()` + `sendInteractLoc()`
|
||||
2. **Position change detection** - Check if position changed significantly after clicking gate
|
||||
3. **Walk after click** - Need to explicitly walk through after clicking gate open
|
||||
4. **Multiple gates** - Some passages have multiple gates/doors in sequence
|
||||
5. **Progressive discovery** - Each run reveals new blockers further along the path
|
||||
|
||||
### Next Steps
|
||||
|
||||
Need to handle White Wolf Mountain area:
|
||||
1. Investigate what's at (2641, 3448) - wolves? terrain? gate?
|
||||
2. May need to go around the mountain (north via Burthorpe or south)
|
||||
3. Or find another gate/passage to click through
|
||||
|
||||
### Code Changes Summary
|
||||
|
||||
```typescript
|
||||
// New helper to detect gate zone
|
||||
function isInFaladorGateZone(ctx): boolean {
|
||||
// x: 2920-2960, z: 3400-3450
|
||||
}
|
||||
|
||||
// New helper to check if past gate
|
||||
function isPastTaverleyGate(ctx): boolean {
|
||||
return state.player.worldX < 2900;
|
||||
}
|
||||
|
||||
// Search and use gates
|
||||
async function findAndUseGate(ctx): Promise<boolean> {
|
||||
const locs = ctx.sdk.getNearbyLocs();
|
||||
const gateKeywords = /gate|door|passage|entrance|exit|barrier/i;
|
||||
// Find gates, click Open option, walk through
|
||||
}
|
||||
|
||||
// Orchestrate gate passage
|
||||
async function navigateFaladorToTaverley(ctx): Promise<boolean> {
|
||||
// Walk to gate area waypoints
|
||||
// Use findAndUseGate() when blocked
|
||||
}
|
||||
```
|
||||
@@ -1,291 +0,0 @@
|
||||
/**
|
||||
* Agility Training Script
|
||||
*
|
||||
* Goal: Train Agility from level 1 to 10+ using the Gnome Stronghold Agility Course
|
||||
*
|
||||
* Uses GNOME_AGILITY preset to start directly at the course.
|
||||
*
|
||||
* Course obstacles (in order):
|
||||
* 1. Log balance (Walk-across) - 7.5 XP
|
||||
* 2. Obstacle net (Climb-over) - 7.5 XP
|
||||
* 3. Tree branch (Climb) - 5 XP
|
||||
* 4. Balancing rope (Walk-on) - 7.5 XP
|
||||
* 5. Tree branch (Climb-down) - 5 XP
|
||||
* 6. Obstacle net (Climb-over) - 7.5 XP
|
||||
* 7. Obstacle pipe (Squeeze-through) - 7.5 XP + 39 XP bonus for completing course
|
||||
*
|
||||
* Total per lap: ~86.5 XP
|
||||
* XP needed for level 10: 1,154 XP = ~14 laps
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Gnome Stronghold Agility Course start location
|
||||
const GNOME_AGILITY_START = { x: 2474, z: 3436 };
|
||||
|
||||
// XP requirements
|
||||
const TARGET_LEVEL = 10;
|
||||
const XP_FOR_LEVEL_10 = 1154;
|
||||
|
||||
// Course obstacle definitions (in order around the course)
|
||||
const COURSE_OBSTACLES = [
|
||||
{ name: /log balance/i, option: /walk/i, description: 'Log balance' },
|
||||
{ name: /obstacle net/i, option: /climb/i, description: 'First net' },
|
||||
{ name: /tree branch/i, option: /^climb$/i, description: 'Tree branch up' },
|
||||
{ name: /balancing rope/i, option: /walk/i, description: 'Balancing rope' },
|
||||
{ name: /tree branch/i, option: /climb-down/i, description: 'Tree branch down' },
|
||||
{ name: /obstacle net/i, option: /climb/i, description: 'Second net' },
|
||||
{ name: /obstacle pipe/i, option: /squeeze/i, description: 'Obstacle pipe' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate distance to a point
|
||||
*/
|
||||
function distanceTo(ctx: ScriptContext, x: number, z: number): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return Infinity;
|
||||
const dx = x - state.player.worldX;
|
||||
const dz = z - state.player.worldZ;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next agility obstacle to interact with
|
||||
*/
|
||||
function findNextObstacle(ctx: ScriptContext, courseIndex: number) {
|
||||
const locs = ctx.sdk.getNearbyLocs();
|
||||
const target = COURSE_OBSTACLES[courseIndex % COURSE_OBSTACLES.length];
|
||||
|
||||
if (!target) return null;
|
||||
|
||||
// Find obstacle matching this course position
|
||||
const obstacle = locs.find(loc =>
|
||||
target.name.test(loc.name) &&
|
||||
loc.optionsWithIndex.some(o => target.option.test(o.text))
|
||||
);
|
||||
|
||||
// Debug: if not found, log nearby agility-like obstacles
|
||||
if (!obstacle && courseIndex === 0) {
|
||||
const agilityLocs = locs.filter(loc =>
|
||||
loc.optionsWithIndex.some(o =>
|
||||
/walk|climb|squeeze|balance|cross/i.test(o.text)
|
||||
)
|
||||
);
|
||||
if (agilityLocs.length > 0) {
|
||||
console.log(`Nearby agility objects: ${agilityLocs.map(l => `${l.name}@(${l.x},${l.z})`).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return obstacle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete one obstacle
|
||||
*/
|
||||
async function completeObstacle(ctx: ScriptContext, courseIndex: number): Promise<boolean> {
|
||||
const xpBefore = ctx.sdk.getSkill('Agility')?.experience ?? 0;
|
||||
const target = COURSE_OBSTACLES[courseIndex % COURSE_OBSTACLES.length];
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
|
||||
const obstacle = findNextObstacle(ctx, courseIndex);
|
||||
if (!obstacle) {
|
||||
console.warn(`Could not find obstacle: ${target?.description}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const opt = obstacle.optionsWithIndex.find(o => target!.option.test(o.text));
|
||||
if (!opt) {
|
||||
console.warn(`No matching option on ${obstacle.name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Attempting: ${obstacle.name} (${opt.text})`);
|
||||
|
||||
// Walk closer if needed
|
||||
if (obstacle.distance > 3) {
|
||||
await ctx.bot.walkTo(obstacle.x, obstacle.z, 2);
|
||||
}
|
||||
|
||||
// Interact with obstacle
|
||||
await ctx.sdk.sendInteractLoc(obstacle.x, obstacle.z, obstacle.id, opt.opIndex);
|
||||
|
||||
// Wait for XP gain or significant position change
|
||||
const startX = ctx.sdk.getState()?.player?.worldX ?? 0;
|
||||
const startZ = ctx.sdk.getState()?.player?.worldZ ?? 0;
|
||||
|
||||
try {
|
||||
await ctx.sdk.waitForCondition(state => {
|
||||
// Dismiss dialogs (level-up messages)
|
||||
if (state.dialog.isOpen) {
|
||||
ctx.sdk.sendClickDialog(0).catch(() => {});
|
||||
return false;
|
||||
}
|
||||
|
||||
// XP gain
|
||||
const xpNow = state.skills.find(s => s.name === 'Agility')?.experience ?? 0;
|
||||
if (xpNow > xpBefore) return true;
|
||||
|
||||
// Significant position change (obstacle completed)
|
||||
const dx = Math.abs((state.player?.worldX ?? 0) - startX);
|
||||
const dz = Math.abs((state.player?.worldZ ?? 0) - startZ);
|
||||
if (dx > 4 || dz > 4) return true;
|
||||
|
||||
// Check for "can not do that from here" message
|
||||
for (const msg of state.gameMessages) {
|
||||
if (msg.tick > startTick && msg.text.toLowerCase().includes('can not do that')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 20000);
|
||||
|
||||
const xpAfter = ctx.sdk.getSkill('Agility')?.experience ?? 0;
|
||||
if (xpAfter > xpBefore) {
|
||||
console.log(` XP gained: +${xpAfter - xpBefore}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Position changed but no XP - might need to re-attempt
|
||||
const endX = ctx.sdk.getState()?.player?.worldX ?? 0;
|
||||
const endZ = ctx.sdk.getState()?.player?.worldZ ?? 0;
|
||||
if (Math.abs(endX - startX) > 4 || Math.abs(endZ - startZ) > 4) {
|
||||
console.log(` Position changed - obstacle may be complete`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(` Obstacle did not complete`);
|
||||
return false;
|
||||
|
||||
} catch {
|
||||
console.warn(` Timeout waiting for obstacle completion`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete one full lap of the course
|
||||
*/
|
||||
async function completeLap(ctx: ScriptContext): Promise<boolean> {
|
||||
console.log('Starting new lap...');
|
||||
|
||||
for (let i = 0; i < COURSE_OBSTACLES.length; i++) {
|
||||
const success = await completeObstacle(ctx, i);
|
||||
if (!success) {
|
||||
// Try to find any nearby obstacle and continue
|
||||
const locs = ctx.sdk.getNearbyLocs();
|
||||
const anyObstacle = locs.find(loc =>
|
||||
loc.optionsWithIndex.some(o =>
|
||||
/walk|climb|squeeze|balance/i.test(o.text)
|
||||
)
|
||||
);
|
||||
|
||||
if (anyObstacle) {
|
||||
console.log(`Found alternate obstacle: ${anyObstacle.name}`);
|
||||
const opt = anyObstacle.optionsWithIndex.find(o =>
|
||||
/walk|climb|squeeze|balance/i.test(o.text)
|
||||
);
|
||||
if (opt) {
|
||||
await ctx.sdk.sendInteractLoc(anyObstacle.x, anyObstacle.z, anyObstacle.id, opt.opIndex);
|
||||
try {
|
||||
await ctx.sdk.waitForStateChange(5000);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main training function
|
||||
*/
|
||||
async function trainAgility(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) throw new Error('No initial state');
|
||||
|
||||
const startXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
|
||||
const startLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
|
||||
|
||||
console.log(`=== Agility Training ===`);
|
||||
console.log(`Starting Level: ${startLevel}`);
|
||||
console.log(`Starting XP: ${startXp}`);
|
||||
console.log(`Target: Level ${TARGET_LEVEL} (${XP_FOR_LEVEL_10} XP)`);
|
||||
console.log(`Position: (${state.player.worldX}, ${state.player.worldZ})`);
|
||||
|
||||
// Train at the agility course
|
||||
let lapsCompleted = 0;
|
||||
const MAX_LAPS = 20; // Safety limit
|
||||
|
||||
while (lapsCompleted < MAX_LAPS) {
|
||||
const currentXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
|
||||
const currentLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
|
||||
|
||||
// Check if we've reached our goal
|
||||
if (currentLevel >= TARGET_LEVEL) {
|
||||
console.log(`\nGoal reached! Level ${currentLevel}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`\n--- Lap ${lapsCompleted + 1} ---`);
|
||||
console.log(`Level: ${currentLevel}, XP: ${currentXp}/${XP_FOR_LEVEL_10}`);
|
||||
|
||||
// Walk to the course START (Log balance location) before each lap
|
||||
const LOG_BALANCE_LOCATION = { x: 2474, z: 3438 };
|
||||
let distToStart = distanceTo(ctx, LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z);
|
||||
|
||||
if (distToStart > 10) {
|
||||
console.log(`Walking to course start (Log balance)...`);
|
||||
await ctx.bot.walkTo(LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z, 5);
|
||||
distToStart = distanceTo(ctx, LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z);
|
||||
|
||||
// If still far, use raw walk
|
||||
if (distToStart > 10) {
|
||||
console.log(` Using raw walk to course start...`);
|
||||
await ctx.sdk.sendWalk(LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z, true);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
await completeLap(ctx);
|
||||
lapsCompleted++;
|
||||
|
||||
// Dismiss any level-up dialogs
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
}
|
||||
|
||||
// Final summary
|
||||
const finalXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
|
||||
const finalLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
|
||||
|
||||
console.log('\n=== Training Complete ===');
|
||||
console.log(`Level: ${startLevel} -> ${finalLevel}`);
|
||||
console.log(`XP: ${startXp} -> ${finalXp} (+${finalXp - startXp})`);
|
||||
console.log(`Laps completed: ${lapsCompleted}`);
|
||||
|
||||
if (finalLevel < TARGET_LEVEL) {
|
||||
console.warn(`Did not reach target level ${TARGET_LEVEL}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const username = `ag${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.GNOME_AGILITY);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
await trainAgility(ctx);
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
---
|
||||
@@ -1,795 +0,0 @@
|
||||
/**
|
||||
* Combat Trainer Script
|
||||
*
|
||||
* Goal: Maximize Attack + Strength + Defence + Hitpoints levels over 10 minutes.
|
||||
*
|
||||
* Strategy:
|
||||
* - Phase 1: Kill cows at Lumbridge cow field, collect hides (~50 needed)
|
||||
* - Phase 2: Sell hides at Lumbridge general store for ~130gp
|
||||
* - Phase 3: Pass through Al Kharid gate (10gp toll), buy iron scimitar (112gp)
|
||||
* - Phase 4: Return to cow field and train with iron scimitar
|
||||
* - Cycle combat styles for balanced Attack/Strength/Defence training
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Combat style indices for swords (4 styles: Stab, Lunge, Slash, Block)
|
||||
// See: https://oldschool.runescape.wiki/w/Combat_Options
|
||||
const COMBAT_STYLES = {
|
||||
ACCURATE: 0, // Stab - Trains Attack
|
||||
AGGRESSIVE: 1, // Lunge - Trains Strength
|
||||
CONTROLLED: 2, // Slash - Trains Attack+Strength+Defence evenly
|
||||
DEFENSIVE: 3, // Block - Trains Defence only
|
||||
};
|
||||
|
||||
// Training locations
|
||||
const LOCATIONS = {
|
||||
COW_FIELD: { x: 3253, z: 3269 }, // Lumbridge cow field (east of castle)
|
||||
LUMBRIDGE_GENERAL_STORE: { x: 3211, z: 3247 }, // To sell hides
|
||||
ALKHARID_GATE: { x: 3268, z: 3228 }, // Gate to Al Kharid
|
||||
ALKHARID_SCIMITAR_SHOP: { x: 3287, z: 3186 }, // Zeke's shop
|
||||
};
|
||||
|
||||
// Upgrade config
|
||||
const IRON_SCIMITAR_PRICE = 112;
|
||||
const GATE_TOLL = 10;
|
||||
const COINS_NEEDED = IRON_SCIMITAR_PRICE + GATE_TOLL + 10; // 132gp buffer
|
||||
const HIDES_NEEDED = 50; // ~2-3gp each = 100-150gp
|
||||
|
||||
/**
|
||||
* Count total cowhides in inventory (they don't stack, so count items)
|
||||
*/
|
||||
function countCowhides(ctx: ScriptContext): number {
|
||||
const inventory = ctx.sdk.getInventory();
|
||||
return inventory.filter(i => /^cow\s?hide$/i.test(i.name)).reduce((sum, i) => sum + (i.count ?? 1), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total coins in inventory
|
||||
*/
|
||||
function countCoins(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get free inventory slots
|
||||
*/
|
||||
function getFreeSlots(ctx: ScriptContext): number {
|
||||
const inventory = ctx.sdk.getInventory();
|
||||
return 28 - inventory.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop bones to make room for hides (bones are worth less than hides)
|
||||
*/
|
||||
async function dropBonesIfNeeded(ctx: ScriptContext): Promise<void> {
|
||||
const freeSlots = getFreeSlots(ctx);
|
||||
|
||||
// If inventory has 2 or fewer free slots, drop bones to make room
|
||||
if (freeSlots <= 2) {
|
||||
const inventory = ctx.sdk.getInventory();
|
||||
const bones = inventory.filter(i => /^bones$/i.test(i.name));
|
||||
|
||||
if (bones.length > 0) {
|
||||
console.log(`Dropping ${bones.length} bones to make room for hides...`);
|
||||
for (const bone of bones.slice(0, 5)) { // Drop up to 5 bones at a time
|
||||
await ctx.sdk.sendDropItem(bone.slot);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track combat statistics
|
||||
interface CombatStats {
|
||||
kills: number;
|
||||
damageDealt: number;
|
||||
damageTaken: number;
|
||||
startXp: { atk: number; str: number; def: number; hp: number };
|
||||
foodEaten: number;
|
||||
looted: number;
|
||||
hidesCollected: number;
|
||||
coinsCollected: number;
|
||||
weaponUpgraded: boolean;
|
||||
phase: 'farming' | 'selling' | 'buying' | 'training'; // Cow-based phases
|
||||
lastStatsLog: number;
|
||||
startTime: number; // For timing
|
||||
lastTimeBasedLog: number; // Last time-based log timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best cow to attack.
|
||||
* Cows are level 2, easy targets that drop valuable hides.
|
||||
*/
|
||||
function findBestTarget(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const targets = state.nearbyNpcs
|
||||
.filter(npc => /^cow$/i.test(npc.name)) // Only cows (not "cow calf")
|
||||
.filter(npc => npc.options.some(o => /attack/i.test(o)))
|
||||
// Filter out NPCs we know are busy (recently returned "already in combat")
|
||||
.filter(npc => canAttackNpc(npc.index))
|
||||
// Filter out NPCs already fighting someone else
|
||||
.filter(npc => {
|
||||
if (npc.targetIndex === -1) return true;
|
||||
return !npc.inCombat;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Prefer NPCs not in combat
|
||||
if (a.inCombat !== b.inCombat) {
|
||||
return a.inCombat ? 1 : -1;
|
||||
}
|
||||
// Then by distance
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return targets[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should eat food based on HP
|
||||
*/
|
||||
function shouldEat(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
|
||||
const hp = state.skills.find(s => s.name === 'Hitpoints');
|
||||
if (!hp) return false;
|
||||
|
||||
// Eat if below 50% HP
|
||||
return hp.level < hp.baseLevel * 0.5;
|
||||
}
|
||||
|
||||
// Style rotation for 2:3:2 ratio (Attack:Strength:Defence)
|
||||
// Strength-focused training for faster kills
|
||||
const STYLE_ROTATION = [
|
||||
{ style: COMBAT_STYLES.ACCURATE, name: 'Stab (Attack)' },
|
||||
{ style: COMBAT_STYLES.ACCURATE, name: 'Stab (Attack)' },
|
||||
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
|
||||
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
|
||||
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
|
||||
{ style: COMBAT_STYLES.DEFENSIVE, name: 'Block (Defence)' },
|
||||
{ style: COMBAT_STYLES.DEFENSIVE, name: 'Block (Defence)' },
|
||||
];
|
||||
|
||||
// Track style cycling - initialized in resetStyleCycling()
|
||||
let lastStyleChange = 0;
|
||||
let currentStyleIndex = 0;
|
||||
const STYLE_CYCLE_MS = 20_000; // Change every 20 seconds (full rotation = 140s)
|
||||
|
||||
// Track which style we last SET (not what the game reports)
|
||||
let lastSetStyle = -1;
|
||||
|
||||
// Track failed pickups to avoid retrying (key: "x,z,id", value: timestamp)
|
||||
const failedPickups = new Map<string, number>();
|
||||
const FAILED_PICKUP_COOLDOWN = 30_000; // Don't retry failed pickups for 30 seconds
|
||||
|
||||
function canAttemptPickup(item: { x: number; z: number; id: number }): boolean {
|
||||
const key = `${item.x},${item.z},${item.id}`;
|
||||
const lastFailed = failedPickups.get(key);
|
||||
if (lastFailed && Date.now() - lastFailed < FAILED_PICKUP_COOLDOWN) {
|
||||
return false; // Still on cooldown
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function markPickupFailed(item: { x: number; z: number; id: number }): void {
|
||||
const key = `${item.x},${item.z},${item.id}`;
|
||||
failedPickups.set(key, Date.now());
|
||||
// Clean up old entries
|
||||
const now = Date.now();
|
||||
for (const [k, v] of failedPickups.entries()) {
|
||||
if (now - v > FAILED_PICKUP_COOLDOWN * 2) {
|
||||
failedPickups.delete(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track NPCs that are "already in combat" to avoid attacking them repeatedly
|
||||
const busyNpcs = new Map<number, number>(); // NPC index -> timestamp
|
||||
const BUSY_NPC_COOLDOWN = 15_000; // Don't retry NPCs that were busy for 15 seconds
|
||||
|
||||
function canAttackNpc(npcIndex: number): boolean {
|
||||
const lastBusy = busyNpcs.get(npcIndex);
|
||||
if (lastBusy && Date.now() - lastBusy < BUSY_NPC_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function markNpcBusy(npcIndex: number): void {
|
||||
busyNpcs.set(npcIndex, Date.now());
|
||||
// Clean up old entries
|
||||
const now = Date.now();
|
||||
for (const [k, v] of busyNpcs.entries()) {
|
||||
if (now - v > BUSY_NPC_COOLDOWN * 2) {
|
||||
busyNpcs.delete(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset style cycling state - call at start of training
|
||||
*/
|
||||
function resetStyleCycling(): void {
|
||||
lastStyleChange = Date.now();
|
||||
currentStyleIndex = 0;
|
||||
lastSetStyle = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle combat style for 2:3:2 Attack:Strength:Defence ratio.
|
||||
* Time-based cycling since kill counting is unreliable.
|
||||
* IMPORTANT: Always set the style, don't rely on game state reporting correctly.
|
||||
*/
|
||||
async function cycleCombatStyle(ctx: ScriptContext): Promise<void> {
|
||||
// Check if it's time to switch styles
|
||||
const now = Date.now();
|
||||
if (now - lastStyleChange >= STYLE_CYCLE_MS) {
|
||||
currentStyleIndex = (currentStyleIndex + 1) % STYLE_ROTATION.length;
|
||||
lastStyleChange = now;
|
||||
}
|
||||
|
||||
const target = STYLE_ROTATION[currentStyleIndex]!;
|
||||
|
||||
// Always set style if it differs from what we last set (don't trust game state)
|
||||
if (lastSetStyle !== target.style) {
|
||||
console.log(`Setting ${target.name} style (slot ${currentStyleIndex}/7)`);
|
||||
await ctx.sdk.sendSetCombatStyle(target.style);
|
||||
lastSetStyle = target.style;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for current combat to complete (NPC dies or we need to heal)
|
||||
* Uses multiple detection methods: XP gains, combatCycle, and NPC disappearance.
|
||||
*/
|
||||
async function waitForCombatEnd(
|
||||
ctx: ScriptContext,
|
||||
targetNpc: NearbyNpc,
|
||||
stats: CombatStats
|
||||
): Promise<'kill' | 'fled' | 'lost_target' | 'need_heal'> {
|
||||
let lastSeenTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
let combatStarted = false;
|
||||
let ticksSinceCombatEnded = 0;
|
||||
let loopCount = 0;
|
||||
|
||||
// Track starting XP to detect combat via XP gains
|
||||
const startState = ctx.sdk.getState();
|
||||
const startXp = {
|
||||
def: startState?.skills.find(s => s.name === 'Defence')?.experience ?? 0,
|
||||
hp: startState?.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
|
||||
};
|
||||
|
||||
// Wait up to 30 seconds for combat to resolve
|
||||
const maxWaitMs = 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Initial delay to let combat actually start (attack animation takes time)
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
loopCount++;
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 'lost_target';
|
||||
|
||||
const currentTick = state.tick;
|
||||
|
||||
// Check if we need to heal
|
||||
if (shouldEat(ctx)) {
|
||||
return 'need_heal';
|
||||
}
|
||||
|
||||
// Check XP gains as combat indicator (most reliable!)
|
||||
const currentXp = {
|
||||
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
|
||||
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
|
||||
};
|
||||
const xpGained = (currentXp.def - startXp.def) + (currentXp.hp - startXp.hp);
|
||||
if (xpGained > 0) {
|
||||
combatStarted = true; // XP gain = definitely in combat
|
||||
}
|
||||
|
||||
// Find our target NPC
|
||||
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
|
||||
|
||||
if (!target) {
|
||||
// NPC disappeared - count as kill if we gained XP or waited a bit
|
||||
if (combatStarted || xpGained > 0 || loopCount >= 2) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
// Check NPC health - if 0, it died (only valid once maxHp > 0)
|
||||
if (target.maxHp > 0 && target.hp === 0) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
// Track combat events
|
||||
for (const event of state.combatEvents) {
|
||||
if (event.tick > lastSeenTick) {
|
||||
if (event.type === 'damage_dealt' && event.targetIndex === targetNpc.index) {
|
||||
stats.damageDealt += event.damage;
|
||||
combatStarted = true;
|
||||
}
|
||||
if (event.type === 'damage_taken') {
|
||||
stats.damageTaken += event.damage;
|
||||
combatStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastSeenTick = currentTick;
|
||||
|
||||
// Check combat status via combatCycle
|
||||
const npcInCombat = target.combatCycle > currentTick;
|
||||
const playerInCombat = state.player?.combat?.inCombat ?? false;
|
||||
const inActiveCombat = playerInCombat || npcInCombat || xpGained > 0;
|
||||
|
||||
if (inActiveCombat) {
|
||||
combatStarted = true;
|
||||
ticksSinceCombatEnded = 0;
|
||||
} else if (combatStarted) {
|
||||
ticksSinceCombatEnded++;
|
||||
if (ticksSinceCombatEnded >= 4) {
|
||||
return 'fled';
|
||||
}
|
||||
} else if (loopCount >= 8) {
|
||||
// Combat never started after ~4 seconds
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sell cow hides at Lumbridge general store.
|
||||
*/
|
||||
async function sellHides(ctx: ScriptContext, stats: CombatStats): Promise<boolean> {
|
||||
console.log('=== Selling Cow Hides ===');
|
||||
stats.phase = 'selling';
|
||||
|
||||
const hideCount = countCowhides(ctx);
|
||||
if (hideCount === 0) {
|
||||
console.warn('No cow hides to sell!');
|
||||
stats.phase = 'farming';
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Walking to Lumbridge general store with ${hideCount} hides...`);
|
||||
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_GENERAL_STORE.x, LOCATIONS.LUMBRIDGE_GENERAL_STORE.z);
|
||||
|
||||
const shopResult = await ctx.bot.openShop(/shop.?keeper/i);
|
||||
if (!shopResult.success) {
|
||||
console.warn('Failed to open general store');
|
||||
stats.phase = 'farming';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sell all hides (one at a time since they don't stack)
|
||||
const inventory = ctx.sdk.getInventory();
|
||||
const allHides = inventory.filter(i => /^cow\s?hide$/i.test(i.name));
|
||||
let soldCount = 0;
|
||||
|
||||
for (const hide of allHides) {
|
||||
const sellResult = await ctx.bot.sellToShop(/^cow\s?hide$/i, 1);
|
||||
if (sellResult.success) {
|
||||
soldCount++;
|
||||
} else {
|
||||
console.warn(`Failed to sell hide: ${sellResult.message}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Sold ${soldCount} cow hides!`);
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
// Check coins
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
const coinCount = coins?.count ?? 0;
|
||||
console.log(`Total coins: ${coinCount}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy iron scimitar from Al Kharid.
|
||||
* 1. Pass through toll gate (10gp)
|
||||
* 2. Buy iron scimitar from Zeke (112gp)
|
||||
* 3. Return to cow field
|
||||
*/
|
||||
async function buyIronScimitar(ctx: ScriptContext, stats: CombatStats): Promise<boolean> {
|
||||
console.log('=== Buying Iron Scimitar ===');
|
||||
stats.phase = 'buying';
|
||||
|
||||
// Check we have enough coins
|
||||
let coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
let coinCount = coins?.count ?? 0;
|
||||
|
||||
if (coinCount < COINS_NEEDED) {
|
||||
console.warn(`Not enough coins (${coinCount}/${COINS_NEEDED})`);
|
||||
stats.phase = 'farming';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk to Al Kharid gate
|
||||
console.log('Walking to Al Kharid gate...');
|
||||
await ctx.bot.walkTo(LOCATIONS.ALKHARID_GATE.x, LOCATIONS.ALKHARID_GATE.z);
|
||||
|
||||
// Handle toll gate
|
||||
console.log('Opening toll gate...');
|
||||
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name));
|
||||
if (gate) {
|
||||
const openOpt = gate.optionsWithIndex.find(o => /open/i.test(o.text));
|
||||
if (openOpt) {
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle gate dialog
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
continue;
|
||||
}
|
||||
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Paying 10gp toll...');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
break;
|
||||
}
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Walk through gate
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if ((state?.player?.worldX ?? 0) >= 3270) {
|
||||
console.log('Entered Al Kharid!');
|
||||
break;
|
||||
}
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
continue;
|
||||
}
|
||||
await ctx.bot.walkTo(3277, 3227);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
}
|
||||
|
||||
// Verify in Al Kharid
|
||||
if ((ctx.sdk.getState()?.player?.worldX ?? 0) < 3270) {
|
||||
console.warn('Failed to enter Al Kharid');
|
||||
stats.phase = 'farming';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk to Zeke's shop
|
||||
console.log('Walking to Zeke\'s Scimitar Shop...');
|
||||
await ctx.bot.walkTo(LOCATIONS.ALKHARID_SCIMITAR_SHOP.x, LOCATIONS.ALKHARID_SCIMITAR_SHOP.z);
|
||||
|
||||
// Buy iron scimitar
|
||||
const scimitarShop = await ctx.bot.openShop(/zeke/i);
|
||||
if (!scimitarShop.success) {
|
||||
console.warn('Failed to open scimitar shop');
|
||||
stats.phase = 'training';
|
||||
return false;
|
||||
}
|
||||
|
||||
const buyScim = await ctx.bot.buyFromShop(/iron scimitar/i, 1);
|
||||
if (buyScim.success) {
|
||||
console.log('*** IRON SCIMITAR PURCHASED! ***');
|
||||
stats.weaponUpgraded = true;
|
||||
} else {
|
||||
console.warn(`Failed to buy scimitar: ${buyScim.message}`);
|
||||
}
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
// Equip the scimitar
|
||||
const scimitar = ctx.sdk.findInventoryItem(/iron scimitar/i);
|
||||
if (scimitar) {
|
||||
console.log('Equipping iron scimitar...');
|
||||
await ctx.bot.equipItem(scimitar);
|
||||
}
|
||||
|
||||
// Return to cow field
|
||||
console.log('Returning to cow field...');
|
||||
// Walk back through gate (no toll to exit)
|
||||
await ctx.bot.walkTo(3267, 3227); // Outside gate
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
|
||||
stats.phase = 'training';
|
||||
console.log('=== Ready to train with Iron Scimitar! ===');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main combat training loop - Cow Hide Strategy
|
||||
*/
|
||||
async function combatTrainingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
// Initialize stats tracking
|
||||
const now = Date.now();
|
||||
const stats: CombatStats = {
|
||||
kills: 0,
|
||||
damageDealt: 0,
|
||||
damageTaken: 0,
|
||||
startXp: {
|
||||
atk: state.skills.find(s => s.name === 'Attack')?.experience ?? 0,
|
||||
str: state.skills.find(s => s.name === 'Strength')?.experience ?? 0,
|
||||
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
|
||||
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
|
||||
},
|
||||
foodEaten: 0,
|
||||
looted: 0,
|
||||
hidesCollected: 0,
|
||||
coinsCollected: 0,
|
||||
weaponUpgraded: false,
|
||||
phase: 'farming', // Start farming cow hides
|
||||
lastStatsLog: 0,
|
||||
startTime: now,
|
||||
lastTimeBasedLog: now,
|
||||
};
|
||||
|
||||
console.log('=== Combat Trainer - Cow Hide Strategy ===');
|
||||
console.log(`Goal: Collect ${HIDES_NEEDED} cow hides → Sell → Buy Iron Scimitar → Train`);
|
||||
console.log(`Starting XP - Atk: ${stats.startXp.atk}, Str: ${stats.startXp.str}, Def: ${stats.startXp.def}, HP: ${stats.startXp.hp}`);
|
||||
|
||||
// Initialize style cycling timer now (not at module load)
|
||||
resetStyleCycling();
|
||||
|
||||
// Equip starting gear
|
||||
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
|
||||
if (sword) {
|
||||
console.log(`Equipping ${sword.name}...`);
|
||||
await ctx.bot.equipItem(sword);
|
||||
}
|
||||
|
||||
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
|
||||
if (shield) {
|
||||
console.log(`Equipping ${shield.name}...`);
|
||||
await ctx.bot.equipItem(shield);
|
||||
}
|
||||
|
||||
// Walk to cow field
|
||||
console.log('Walking to cow field...');
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
|
||||
// Main training loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any blocking dialogs
|
||||
if (currentState.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Log periodic stats (every 10 kills or every 5 minutes)
|
||||
const timeSinceLastLog = Date.now() - stats.lastTimeBasedLog;
|
||||
const shouldLogByKills = stats.kills > 0 && stats.kills % 10 === 0 && stats.kills !== stats.lastStatsLog;
|
||||
const shouldLogByTime = timeSinceLastLog >= 5 * 60_000; // 5 minutes
|
||||
|
||||
if (shouldLogByKills || shouldLogByTime) {
|
||||
stats.lastStatsLog = stats.kills;
|
||||
stats.lastTimeBasedLog = Date.now();
|
||||
logStats(ctx, stats);
|
||||
}
|
||||
|
||||
// Cycle combat style for balanced training (time-based)
|
||||
await cycleCombatStyle(ctx);
|
||||
|
||||
// Check current resources using proper counting
|
||||
const hideCount = countCowhides(ctx);
|
||||
const coinCount = countCoins(ctx);
|
||||
|
||||
// Phase logic: farming → selling → buying → training
|
||||
if (stats.phase === 'farming' && !stats.weaponUpgraded) {
|
||||
// Check if we have enough hides to sell
|
||||
if (hideCount >= HIDES_NEEDED) {
|
||||
console.log(`Collected ${hideCount} hides! Time to sell and buy scimitar!`);
|
||||
await sellHides(ctx, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also check if we already have enough coins (from previous attempts or loot)
|
||||
if (coinCount >= COINS_NEEDED) {
|
||||
console.log(`Already have ${coinCount} coins! Skipping to buy scimitar!`);
|
||||
await buyIronScimitar(ctx, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drop bones if inventory is getting full (prioritize hides over bones)
|
||||
await dropBonesIfNeeded(ctx);
|
||||
}
|
||||
|
||||
if (stats.phase === 'selling') {
|
||||
// After selling, try to buy
|
||||
const newCoinCount = countCoins(ctx);
|
||||
if (newCoinCount >= COINS_NEEDED) {
|
||||
await buyIronScimitar(ctx, stats);
|
||||
} else {
|
||||
console.log(`Only ${newCoinCount} coins, need ${COINS_NEEDED}. Collecting more hides...`);
|
||||
stats.phase = 'farming';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pick up loot - prioritize cowhides!
|
||||
// Skip bones if: inventory tight, or in training phase (don't need money anymore)
|
||||
const skipBones = getFreeSlots(ctx) <= 4 || stats.phase === 'training';
|
||||
// In training phase, only pick up hides if we're still working toward iron scimitar
|
||||
const shouldLoot = stats.phase === 'farming' || !stats.weaponUpgraded;
|
||||
|
||||
const loot = shouldLoot ? ctx.sdk.getGroundItems()
|
||||
.filter(i => /cow\s?hide|cowhide|bones|coins/i.test(i.name))
|
||||
.filter(i => !skipBones || !/^bones$/i.test(i.name)) // Skip bones if inventory tight
|
||||
.filter(i => i.distance <= 3) // Reduced from 5 to avoid timeouts
|
||||
.filter(i => canAttemptPickup(i)) // Skip items we recently failed to pick up
|
||||
.sort((a, b) => {
|
||||
// Priority: hides > coins > bones
|
||||
const priority = (name: string) => {
|
||||
if (/cow\s?hide|cowhide/i.test(name)) return 0;
|
||||
if (/coins/i.test(name)) return 1;
|
||||
return 2;
|
||||
};
|
||||
return priority(a.name) - priority(b.name) || a.distance - b.distance;
|
||||
}) : [];
|
||||
|
||||
if (loot.length > 0) {
|
||||
const item = loot[0]!;
|
||||
const result = await ctx.bot.pickupItem(item);
|
||||
if (result.success) {
|
||||
stats.looted++;
|
||||
if (/cow\s?hide|cowhide/i.test(item.name)) {
|
||||
stats.hidesCollected += item.count ?? 1;
|
||||
const totalHides = countCowhides(ctx);
|
||||
console.log(`Picked up cowhide (${totalHides}/${HIDES_NEEDED})`);
|
||||
} else if (/coins/i.test(item.name)) {
|
||||
stats.coinsCollected += item.count ?? 1;
|
||||
}
|
||||
} else {
|
||||
// Mark this item as failed so we don't retry immediately
|
||||
markPickupFailed(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Find a cow to attack
|
||||
const target = findBestTarget(ctx);
|
||||
if (!target) {
|
||||
console.log('No cows nearby - walking to cow field...');
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already fighting
|
||||
const playerCombat = currentState.player?.combat;
|
||||
if (playerCombat?.inCombat && playerCombat.targetIndex === target.index) {
|
||||
const result = await waitForCombatEnd(ctx, target, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack the cow
|
||||
const attackResult = await ctx.bot.attackNpc(target);
|
||||
if (!attackResult.success) {
|
||||
console.warn(`Attack failed: ${attackResult.message}`);
|
||||
// Mark NPC as busy if already in combat (won't retry for 15s)
|
||||
if (attackResult.reason === 'already_in_combat') {
|
||||
markNpcBusy(target.index);
|
||||
}
|
||||
// Try opening gate if blocked
|
||||
if (attackResult.reason === 'out_of_reach') {
|
||||
await ctx.bot.openDoor(/gate/i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for combat to complete
|
||||
const combatResult = await waitForCombatEnd(ctx, target, stats);
|
||||
if (combatResult === 'kill') {
|
||||
console.log(`Kill #${stats.kills}! (Hides: ${hideCount}/${HIDES_NEEDED})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log current training statistics
|
||||
*/
|
||||
function logStats(ctx: ScriptContext, stats: CombatStats): void {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
const currentXp = {
|
||||
atk: state.skills.find(s => s.name === 'Attack')?.experience ?? 0,
|
||||
str: state.skills.find(s => s.name === 'Strength')?.experience ?? 0,
|
||||
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
|
||||
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
|
||||
};
|
||||
|
||||
const xpGained = {
|
||||
atk: currentXp.atk - stats.startXp.atk,
|
||||
str: currentXp.str - stats.startXp.str,
|
||||
def: currentXp.def - stats.startXp.def,
|
||||
hp: currentXp.hp - stats.startXp.hp,
|
||||
};
|
||||
|
||||
const totalXp = xpGained.atk + xpGained.str + xpGained.def + xpGained.hp;
|
||||
const hides = countCowhides(ctx);
|
||||
|
||||
// Calculate XP/hour
|
||||
const elapsedMs = Date.now() - stats.startTime;
|
||||
const elapsedMinutes = Math.round(elapsedMs / 60_000);
|
||||
const xpPerHour = elapsedMs > 60_000 ? Math.round(totalXp / (elapsedMs / 3_600_000)) : 0;
|
||||
|
||||
console.log(`--- Stats after ${elapsedMinutes}m / ${stats.kills} kills (Phase: ${stats.phase}) ---`);
|
||||
console.log(`XP: Atk +${xpGained.atk}, Str +${xpGained.str}, Def +${xpGained.def}, HP +${xpGained.hp} (Total: +${totalXp})`);
|
||||
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
|
||||
console.log(`Hides: ${hides}/${HIDES_NEEDED}, Coins: ${stats.coinsCollected}`);
|
||||
console.log(`Weapon: ${stats.weaponUpgraded ? 'IRON SCIMITAR!' : 'Bronze Sword'}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `CT${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await combatTrainingLoop(ctx);
|
||||
} finally {
|
||||
// Log final stats
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
const skills = state.skills;
|
||||
const atk = skills.find(s => s.name === 'Attack');
|
||||
const str = skills.find(s => s.name === 'Strength');
|
||||
const def = skills.find(s => s.name === 'Defence');
|
||||
const hp = skills.find(s => s.name === 'Hitpoints');
|
||||
const scimitar = ctx.sdk.findInventoryItem(/iron scimitar/i) ||
|
||||
state.equipment?.find(e => /iron scimitar/i.test(e.name));
|
||||
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Combat Level: ${state.player?.combatLevel ?? '?'}`);
|
||||
console.log(`Attack: Level ${atk?.baseLevel} (${atk?.experience} XP)`);
|
||||
console.log(`Strength: Level ${str?.baseLevel} (${str?.experience} XP)`);
|
||||
console.log(`Defence: Level ${def?.baseLevel} (${def?.experience} XP)`);
|
||||
console.log(`Hitpoints: Level ${hp?.baseLevel} (${hp?.experience} XP)`);
|
||||
console.log(`Iron Scimitar: ${scimitar ? 'YES!' : 'No'}`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 30 * 60 * 1000,
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
|
||||
@@ -1,707 +0,0 @@
|
||||
/**
|
||||
* Cooking Trainer Script
|
||||
*
|
||||
* Goal: Train Cooking from level 1 to 10+
|
||||
*
|
||||
* Strategy (v3 - Al-Kharid with toll gate):
|
||||
* - Start at Lumbridge Castle with fishing net, tinderbox, axe, shortbow
|
||||
* - Sell shortbow at Lumbridge general store for 20gp
|
||||
* - Walk to Al-Kharid toll gate and pay 10gp
|
||||
* - Fish at Al-Kharid fishing spot (safe area)
|
||||
* - Cook at Al-Kharid range
|
||||
* - Repeat until level 10
|
||||
*
|
||||
* This avoids the dangerous Dark Wizard area near Draynor.
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc, NearbyLoc, InventoryItem } from '../../sdk/types';
|
||||
|
||||
// Key locations
|
||||
const LOCATIONS = {
|
||||
LUMBRIDGE_SHOP: { x: 3212, z: 3247 }, // General store
|
||||
TOLL_GATE: { x: 3268, z: 3228 }, // Al-Kharid toll gate
|
||||
ALKHARID_FISHING: { x: 3265, z: 3153 }, // Shrimp fishing - moved north away from scorpions
|
||||
ALKHARID_RANGE: { x: 3273, z: 3180 }, // Range for cooking
|
||||
};
|
||||
|
||||
interface Stats {
|
||||
fishCaught: number;
|
||||
fishCooked: number;
|
||||
fishBurnt: number;
|
||||
cycles: number;
|
||||
startCookingXp: number;
|
||||
startTime: number;
|
||||
lastProgressTime: number;
|
||||
}
|
||||
|
||||
// ============ Helper Functions ============
|
||||
|
||||
function getCookingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function getCookingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return null;
|
||||
return { x: state.player.worldX, z: state.player.worldZ };
|
||||
}
|
||||
|
||||
function distanceTo(ctx: ScriptContext, target: { x: number; z: number }): number {
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (!pos) return 999;
|
||||
return Math.abs(pos.x - target.x) + Math.abs(pos.z - target.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest fishing spot with Net option
|
||||
*/
|
||||
function findFishingSpot(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
// Find any fishing spot with "Net" option
|
||||
const spots = state.nearbyNpcs
|
||||
.filter(npc => /fishing\s*spot/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(opt => /^net$/i.test(opt)))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return spots[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count raw fish in inventory
|
||||
*/
|
||||
function countRawFish(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
return state.inventory
|
||||
.filter(item => /^raw\s/i.test(item.name))
|
||||
.reduce((sum, item) => sum + item.count, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all raw fish items in inventory
|
||||
*/
|
||||
function getRawFishItems(ctx: ScriptContext): InventoryItem[] {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return [];
|
||||
|
||||
return state.inventory.filter(item => /^raw\s/i.test(item.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available slots (keeping essential items: net, tinderbox, axe)
|
||||
*/
|
||||
function getAvailableSlots(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
return 28 - state.inventory.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current HP
|
||||
*/
|
||||
function getCurrentHp(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 10;
|
||||
return state.skills.find(s => s.name === 'Hitpoints')?.level ?? 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eat food if HP is low, or run away if no food
|
||||
*/
|
||||
async function eatIfLowHp(ctx: ScriptContext, stats: Stats): Promise<boolean> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
|
||||
// Get current HP from Hitpoints skill level (it reflects current HP, not just max)
|
||||
const hitpointsSkill = state.skills.find(s => s.name === 'Hitpoints');
|
||||
const hp = hitpointsSkill?.level ?? 10;
|
||||
if (hp > 5) return false; // HP is fine
|
||||
|
||||
// Find food to eat (cooked shrimps, bread, or cooked anchovies)
|
||||
const food = state.inventory.find(i =>
|
||||
(/shrimps/i.test(i.name) && !/^raw\s/i.test(i.name)) ||
|
||||
/bread/i.test(i.name) ||
|
||||
(/anchov/i.test(i.name) && !/^raw\s/i.test(i.name))
|
||||
);
|
||||
|
||||
if (!food) {
|
||||
// No food - run away from combat!
|
||||
if (hp <= 3) {
|
||||
console.log(`HP critical (${hp}), running away!`);
|
||||
// Run north towards bank (safer area)
|
||||
await ctx.sdk.sendWalk(LOCATIONS.ALKHARID_RANGE.x, LOCATIONS.ALKHARID_RANGE.z, true);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`HP low (${hp}), eating ${food.name}`);
|
||||
await ctx.sdk.sendUseItem(food.slot, 1); // Option 1 = "Eat"
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss dialogs (level-up notifications, etc.)
|
||||
*/
|
||||
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
|
||||
let dismissed = 0;
|
||||
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
dismissed++;
|
||||
}
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a position with pathfinding (with retries)
|
||||
*/
|
||||
async function walkToPosition(ctx: ScriptContext, stats: Stats, target: { x: number; z: number }, name: string): Promise<boolean> {
|
||||
const startDist = distanceTo(ctx, target);
|
||||
console.log(`Walking to ${name} (${target.x}, ${target.z}), dist: ${startDist}...`);
|
||||
|
||||
// Use pathfinding for long distances - retry up to 3 times
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const currentDist = distanceTo(ctx, target);
|
||||
if (currentDist <= 10) {
|
||||
console.log(`Arrived at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentDist > 20) {
|
||||
console.log(`Pathfinding attempt ${attempt + 1}, dist: ${currentDist}`);
|
||||
try {
|
||||
await ctx.bot.walkTo(target.x, target.z);
|
||||
} catch (e) {
|
||||
console.warn(`Pathfinding error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
let finalDist = distanceTo(ctx, target);
|
||||
if (finalDist <= 10) {
|
||||
console.log(`Arrived at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Direct walking for remaining distance
|
||||
console.log(`Direct walking to finish, dist: ${finalDist}`);
|
||||
await ctx.sdk.sendWalk(target.x, target.z, true);
|
||||
|
||||
// Wait to arrive
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
|
||||
await dismissDialogs(ctx, stats, 1);
|
||||
|
||||
const dist = distanceTo(ctx, target);
|
||||
if (dist <= 5) {
|
||||
console.log(`Arrived at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Re-send walk periodically
|
||||
if (i > 0 && i % 10 === 0) {
|
||||
ctx.sdk.sendWalk(target.x, target.z, true);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`Failed to reach ${name} (dist: ${distanceTo(ctx, target)})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============ Main Phases ============
|
||||
|
||||
/**
|
||||
* Phase 0a: Sell shortbow at Lumbridge general store for coins
|
||||
*/
|
||||
async function sellShortbowForCoins(ctx: ScriptContext, stats: Stats): Promise<boolean> {
|
||||
// Check if we already have coins
|
||||
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
if (coins && coins.count >= 10) {
|
||||
console.log(`Already have ${coins.count} coins`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we have shortbow to sell
|
||||
const shortbow = ctx.sdk.getState()?.inventory.find(i => /shortbow/i.test(i.name));
|
||||
if (!shortbow) {
|
||||
console.warn('No shortbow to sell');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Walking to Lumbridge general store...');
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.LUMBRIDGE_SHOP, 'Lumbridge shop')) {
|
||||
console.warn('Could not reach shop');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open shop
|
||||
console.log('Opening shop...');
|
||||
const result = await ctx.bot.openShop(/shop\s*keeper/i);
|
||||
if (!result.success) {
|
||||
console.warn(`Failed to open shop: ${result.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sell shortbow
|
||||
console.log('Selling shortbow...');
|
||||
const sellResult = await ctx.bot.sellToShop(/shortbow/i);
|
||||
if (!sellResult.success) {
|
||||
console.warn(`Failed to sell shortbow: ${sellResult.message}`);
|
||||
}
|
||||
|
||||
// Close shop
|
||||
await ctx.bot.closeShop();
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
const newCoins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
console.log(`Now have ${newCoins?.count ?? 0} coins`);
|
||||
return (newCoins?.count ?? 0) >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0b: Pass through Al-Kharid toll gate
|
||||
*/
|
||||
async function passThruTollGate(ctx: ScriptContext, stats: Stats): Promise<boolean> {
|
||||
// Check if already in Al-Kharid
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (pos && pos.x >= 3270) {
|
||||
console.log('Already in Al-Kharid');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Walking to toll gate...');
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.TOLL_GATE, 'toll gate')) {
|
||||
console.warn('Could not reach toll gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find and interact with gate
|
||||
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name));
|
||||
if (!gate) {
|
||||
console.warn('Gate not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const openOpt = gate.optionsWithIndex.find(o => /open|pay/i.test(o.text));
|
||||
if (!openOpt) {
|
||||
console.warn('No open option on gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Opening toll gate...');
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Handle dialog - click through until "Yes" option appears
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.dialog.isOpen) {
|
||||
// Check if we're already through
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (pos && pos.x >= 3270) {
|
||||
console.log('Passed through toll gate!');
|
||||
return true;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
const yesOpt = state.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Paying toll...');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue; // Keep checking dialog/position
|
||||
}
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Wait and walk through repeatedly
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// Check if already through
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (pos && pos.x >= 3270) {
|
||||
console.log('Passed through toll gate!');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dismiss any remaining dialogs
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// Try to walk through
|
||||
await ctx.sdk.sendWalk(3277, 3227, true); // Inside Al-Kharid
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
|
||||
console.warn('Failed to pass through toll gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0c: Drop non-essential items to make room for fish
|
||||
*/
|
||||
async function dropNonEssentials(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
// Keep: fishing net, coins (for kebabs if needed)
|
||||
// Keep: net, coins, food items (for emergency healing)
|
||||
// Note: only keep NON-raw shrimps (the cooked one in starting inventory)
|
||||
const essentialPatterns = [/fishing\s*net/i, /coins/i, /bread/i, /^shrimps$/i];
|
||||
|
||||
const itemsToDrop = state.inventory.filter(item =>
|
||||
!essentialPatterns.some(p => p.test(item.name))
|
||||
);
|
||||
|
||||
if (itemsToDrop.length === 0) {
|
||||
console.log('No non-essential items to drop');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Dropping ${itemsToDrop.length} non-essential items...`);
|
||||
for (const item of itemsToDrop) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
console.log(`Inventory now has ${ctx.sdk.getState()?.inventory.length ?? 0} items, ${getAvailableSlots(ctx)} slots free`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1: Fish until inventory full at Al-Kharid
|
||||
*/
|
||||
async function fishUntilFull(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log(`Phase 1: Fishing at Al-Kharid until inventory full...`);
|
||||
let lastFishCount = countRawFish(ctx);
|
||||
let noSpotCount = 0;
|
||||
|
||||
// Fish until inventory full
|
||||
while (getAvailableSlots(ctx) > 0) {
|
||||
// Check HP and eat if needed
|
||||
await eatIfLowHp(ctx, stats);
|
||||
|
||||
// Dismiss dialogs first
|
||||
if (await dismissDialogs(ctx, stats) > 0) continue;
|
||||
|
||||
// Check for new fish
|
||||
const currentFish = countRawFish(ctx);
|
||||
if (currentFish > lastFishCount) {
|
||||
stats.fishCaught += currentFish - lastFishCount;
|
||||
if (currentFish % 5 === 0) {
|
||||
console.log(`Fish caught: ${stats.fishCaught} (${getAvailableSlots(ctx)} slots free)`);
|
||||
}
|
||||
noSpotCount = 0;
|
||||
}
|
||||
lastFishCount = currentFish;
|
||||
|
||||
// Find and use fishing spot
|
||||
const spot = findFishingSpot(ctx);
|
||||
if (!spot) {
|
||||
noSpotCount++;
|
||||
if (noSpotCount % 50 === 0) console.log('Waiting for fishing spot...');
|
||||
|
||||
// Check if we drifted too far (e.g., respawned in Lumbridge)
|
||||
if (noSpotCount > 50) {
|
||||
const dist = distanceTo(ctx, LOCATIONS.ALKHARID_FISHING);
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (dist > 30) {
|
||||
console.log(`Drifted too far (${dist} tiles at ${pos?.x},${pos?.z}), walking back...`);
|
||||
|
||||
// Check if we're in Lumbridge (x < 3260) and need to pass toll gate
|
||||
if (pos && pos.x < 3260) {
|
||||
console.log('Respawned in Lumbridge, need to handle re-entry');
|
||||
// If we still have fish, we already made progress - just go to cooking
|
||||
if (countRawFish(ctx) > 5) {
|
||||
console.log('Have fish to cook, skipping re-entry to Al-Kharid');
|
||||
return; // Exit fishing phase, will proceed to cooking
|
||||
}
|
||||
// Check if we have coins for toll
|
||||
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
if (!coins || coins.count < 10) {
|
||||
console.log('No coins for toll, cannot re-enter Al-Kharid');
|
||||
throw new Error('Respawned without coins, cannot continue');
|
||||
}
|
||||
// Pass through toll gate
|
||||
const passedToll = await passThruTollGate(ctx, stats);
|
||||
if (!passedToll) {
|
||||
throw new Error('Could not re-enter Al-Kharid after respawn');
|
||||
}
|
||||
}
|
||||
|
||||
await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing');
|
||||
noSpotCount = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk around if no spot for too long
|
||||
if (noSpotCount > 100) {
|
||||
console.log('No spot visible, walking around...');
|
||||
await ctx.sdk.sendWalk(
|
||||
LOCATIONS.ALKHARID_FISHING.x + (Math.random() > 0.5 ? 3 : -3),
|
||||
LOCATIONS.ALKHARID_FISHING.z,
|
||||
true
|
||||
);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
noSpotCount = 0;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
continue;
|
||||
}
|
||||
|
||||
noSpotCount = 0;
|
||||
const netOpt = spot.optionsWithIndex.find(o => /^net$/i.test(o.text));
|
||||
if (!netOpt) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
console.log(`Inventory full: ${countRawFish(ctx)} raw fish`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Cook all fish at Al-Kharid range
|
||||
*/
|
||||
async function cookAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log('Phase 2: Walking to Al-Kharid range...');
|
||||
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_RANGE, 'Al-Kharid range')) {
|
||||
console.warn('Could not reach range');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Cooking fish at range...');
|
||||
|
||||
// Find the range
|
||||
const range = ctx.sdk.getState()?.nearbyLocs.find(loc => /range|stove/i.test(loc.name));
|
||||
if (!range) {
|
||||
console.warn('No range found nearby');
|
||||
const locs = ctx.sdk.getState()?.nearbyLocs.slice(0, 10) ?? [];
|
||||
console.log(`Nearby locs: ${locs.map(l => l.name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found: ${range.name} at (${range.x}, ${range.z})`);
|
||||
|
||||
const cookingXpBefore = getCookingXp(ctx);
|
||||
const rawFishBefore = countRawFish(ctx);
|
||||
|
||||
// Cook each raw fish
|
||||
while (countRawFish(ctx) > 0) {
|
||||
await dismissDialogs(ctx, stats, 1);
|
||||
|
||||
const rawFish = getRawFishItems(ctx)[0];
|
||||
if (!rawFish) break;
|
||||
|
||||
// Use fish on range
|
||||
await ctx.sdk.sendUseItemOnLoc(rawFish.slot, range.x, range.z, range.id);
|
||||
|
||||
// Wait for cooking interface or direct cooking
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
const state = ctx.sdk.getState();
|
||||
|
||||
// Handle cooking interface if it appears
|
||||
const firstInterfaceOpt = state?.interface?.options[0];
|
||||
if (state?.interface?.isOpen && firstInterfaceOpt) {
|
||||
console.log('Clicking cook option...');
|
||||
await ctx.sdk.sendClickInterfaceOption(0);
|
||||
// Wait for batch cooking to complete
|
||||
let noChange = 0;
|
||||
while (noChange < 15 && countRawFish(ctx) > 0) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
await dismissDialogs(ctx, stats, 1);
|
||||
const prev = countRawFish(ctx);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
if (countRawFish(ctx) === prev) noChange++;
|
||||
else noChange = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle dialog
|
||||
if (state?.dialog?.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
|
||||
// Check if this fish was cooked
|
||||
const currentRaw = countRawFish(ctx);
|
||||
if (currentRaw < rawFishBefore) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate results
|
||||
const xpGained = getCookingXp(ctx) - cookingXpBefore;
|
||||
const fishCooked = rawFishBefore - countRawFish(ctx);
|
||||
stats.fishCooked += fishCooked;
|
||||
|
||||
console.log(`Cooked ${fishCooked} fish (XP: +${xpGained})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3: Drop all cooked and burnt fish
|
||||
*/
|
||||
async function dropAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
// Drop everything except fishing net
|
||||
const itemsToDrop = state.inventory.filter(item =>
|
||||
/shrimp|anchov/i.test(item.name) ||
|
||||
/^burnt\s/i.test(item.name)
|
||||
);
|
||||
|
||||
if (itemsToDrop.length === 0) {
|
||||
console.log('Nothing to drop');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Dropping ${itemsToDrop.length} items...`);
|
||||
for (const item of itemsToDrop) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log final statistics
|
||||
*/
|
||||
function logFinalStats(ctx: ScriptContext, stats: Stats) {
|
||||
const state = ctx.sdk.getState();
|
||||
const cooking = state?.skills.find(s => s.name === 'Cooking');
|
||||
|
||||
const cookingXpGained = (cooking?.experience ?? 0) - stats.startCookingXp;
|
||||
const duration = (Date.now() - stats.startTime) / 1000;
|
||||
|
||||
console.log('');
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Duration: ${Math.round(duration)}s`);
|
||||
console.log(`Cycles: ${stats.cycles}`);
|
||||
console.log(`--- Cooking ---`);
|
||||
console.log(` Level: ${cooking?.baseLevel ?? '?'}`);
|
||||
console.log(` XP Gained: ${cookingXpGained}`);
|
||||
console.log(` Fish Caught: ${stats.fishCaught}`);
|
||||
console.log(` Fish Cooked: ${stats.fishCooked}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop
|
||||
*/
|
||||
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const GOAL_LEVEL = 10;
|
||||
|
||||
console.log('=== Cooking Trainer (v3 - Al-Kharid) ===');
|
||||
console.log(`Goal: Reach Cooking level ${GOAL_LEVEL}`);
|
||||
console.log(`Starting level: ${getCookingLevel(ctx)}`);
|
||||
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
|
||||
console.log(`Inventory: ${ctx.sdk.getState()?.inventory.length ?? 0} items`);
|
||||
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Phase 0a: Sell shortbow for coins
|
||||
const gotCoins = await sellShortbowForCoins(ctx, stats);
|
||||
if (!gotCoins) {
|
||||
throw new Error('Could not get coins for toll');
|
||||
}
|
||||
|
||||
// Phase 0b: Pass through toll gate to Al-Kharid
|
||||
const enteredAlKharid = await passThruTollGate(ctx, stats);
|
||||
if (!enteredAlKharid) {
|
||||
throw new Error('Could not enter Al-Kharid');
|
||||
}
|
||||
|
||||
// Phase 0c: Drop non-essential items
|
||||
await dropNonEssentials(ctx, stats);
|
||||
|
||||
// Walk to Al-Kharid fishing spot
|
||||
console.log('Walking to Al-Kharid fishing spot...');
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing')) {
|
||||
throw new Error('Could not reach Al-Kharid fishing spot');
|
||||
}
|
||||
|
||||
while (getCookingLevel(ctx) < GOAL_LEVEL) {
|
||||
stats.cycles++;
|
||||
console.log(`\n--- Cycle ${stats.cycles} (Cooking level ${getCookingLevel(ctx)}) ---`);
|
||||
|
||||
// Phase 1: Fish until inventory full
|
||||
await fishUntilFull(ctx, stats);
|
||||
|
||||
// Phase 2: Cook at range
|
||||
await cookAllFish(ctx, stats);
|
||||
|
||||
// Phase 3: Drop cooked fish
|
||||
await dropAllFish(ctx, stats);
|
||||
|
||||
// Walk back to fishing spot
|
||||
console.log('Returning to fishing spot...');
|
||||
await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing');
|
||||
}
|
||||
|
||||
console.log(`\nGoal achieved! Cooking level ${getCookingLevel(ctx)}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `CK${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
const stats: Stats = {
|
||||
fishCaught: 0,
|
||||
fishCooked: 0,
|
||||
fishBurnt: 0,
|
||||
cycles: 0,
|
||||
startCookingXp: getCookingXp(ctx),
|
||||
startTime: Date.now(),
|
||||
lastProgressTime: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
await mainLoop(ctx, stats);
|
||||
} catch (e) {
|
||||
console.error(`Script aborted: ${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
logFinalStats(ctx, stats);
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 15 * 60 * 1000,
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,578 +0,0 @@
|
||||
/**
|
||||
* Cow Hide Banking Script
|
||||
*
|
||||
* Goal: Collect and bank as many cow hides as possible in 15 minutes.
|
||||
*
|
||||
* Strategy:
|
||||
* - Start at Lumbridge (standard post-tutorial spawn)
|
||||
* - Equip combat gear (bronze sword + wooden shield)
|
||||
* - Walk to Lumbridge cow field
|
||||
* - Kill cows, pick up cow hides
|
||||
* - When inventory is full, walk to Lumbridge Castle bank and deposit
|
||||
* - Track: kills, hides collected, hides banked, bank trips
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Locations
|
||||
const LOCATIONS = {
|
||||
COW_FIELD: { x: 3253, z: 3270 }, // Lumbridge cow field (east of castle)
|
||||
LUMBRIDGE_BANK: { x: 3208, z: 3220 }, // Lumbridge Castle bank (top floor, level 2)
|
||||
LUMBRIDGE_CASTLE_ENTRANCE: { x: 3210, z: 3217 }, // Castle main entrance
|
||||
LUMBRIDGE_STAIRS_GROUND: { x: 3206, z: 3208 }, // Stairs inside castle ground floor
|
||||
};
|
||||
|
||||
// Configuration
|
||||
const INVENTORY_THRESHOLD = 24; // Bank when inventory has this many items
|
||||
const MAX_HIDES_BEFORE_BANK = 8; // Force bank trip after collecting this many hides (lowered for faster iteration)
|
||||
|
||||
// Statistics tracking
|
||||
interface HideStats {
|
||||
kills: number;
|
||||
hidesCollected: number;
|
||||
hidesBanked: number;
|
||||
bankTrips: number;
|
||||
startTime: number;
|
||||
lastProgressTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best cow to attack
|
||||
* - Filters out cows already in combat with someone else
|
||||
* - Prefers idle cows over cows in combat
|
||||
* - Sorts by distance
|
||||
*/
|
||||
function findBestCow(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const cows = state.nearbyNpcs
|
||||
.filter(npc => /^cow$/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(o => /attack/i.test(o)))
|
||||
// Filter out cows already fighting someone else
|
||||
.filter(npc => {
|
||||
if (npc.targetIndex === -1) return true;
|
||||
return !npc.inCombat;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Prefer cows not in combat
|
||||
if (a.inCombat !== b.inCombat) {
|
||||
return a.inCombat ? 1 : -1;
|
||||
}
|
||||
// Then by distance
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return cows[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for combat to complete
|
||||
*/
|
||||
async function waitForCombatEnd(
|
||||
ctx: ScriptContext,
|
||||
targetNpc: NearbyNpc,
|
||||
stats: HideStats
|
||||
): Promise<'kill' | 'fled' | 'lost_target'> {
|
||||
let combatStarted = false;
|
||||
let ticksSinceCombatEnded = 0;
|
||||
let loopCount = 0;
|
||||
|
||||
const maxWaitMs = 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Initial delay for attack animation
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
loopCount++;
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 'lost_target';
|
||||
|
||||
// CRITICAL: Dismiss any dialogs during combat (level-up messages)
|
||||
// Without this, dialogs block game responses and cause server timeouts
|
||||
if (state.dialog.isOpen) {
|
||||
console.log('Dismissing dialog during combat...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentTick = state.tick;
|
||||
|
||||
// Find our target cow
|
||||
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
|
||||
|
||||
if (!target) {
|
||||
// Cow disappeared - likely died
|
||||
if (combatStarted || loopCount >= 2) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
// Check cow health
|
||||
if (target.maxHp > 0 && target.hp === 0) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
// Check combat status
|
||||
const npcInCombat = target.combatCycle > currentTick;
|
||||
const playerInCombat = state.player?.combat?.inCombat ?? false;
|
||||
const inActiveCombat = playerInCombat || npcInCombat;
|
||||
|
||||
if (inActiveCombat) {
|
||||
combatStarted = true;
|
||||
ticksSinceCombatEnded = 0;
|
||||
} else if (combatStarted) {
|
||||
ticksSinceCombatEnded++;
|
||||
if (ticksSinceCombatEnded >= 4) {
|
||||
return 'fled';
|
||||
}
|
||||
} else if (loopCount >= 8) {
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick up cow hides from the ground
|
||||
*/
|
||||
async function pickupHides(ctx: ScriptContext, stats: HideStats): Promise<number> {
|
||||
let pickedUp = 0;
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
// Check inventory space
|
||||
if (state.inventory.length >= 28) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const groundItems = ctx.sdk.getGroundItems()
|
||||
.filter(i => /cow\s*hide/i.test(i.name))
|
||||
.filter(i => i.distance <= 8)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
for (const item of groundItems.slice(0, 3)) {
|
||||
if (ctx.sdk.getState()!.inventory.length >= 28) break;
|
||||
|
||||
console.log(`Picking up ${item.name}...`);
|
||||
const result = await ctx.bot.pickupItem(item);
|
||||
if (result.success) {
|
||||
pickedUp++;
|
||||
stats.hidesCollected++;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
return pickedUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bank the collected hides at Lumbridge Castle bank
|
||||
*/
|
||||
async function bankHides(ctx: ScriptContext, stats: HideStats): Promise<boolean> {
|
||||
console.log('=== Banking Trip ===');
|
||||
stats.bankTrips++;
|
||||
|
||||
const currentState = ctx.sdk.getState();
|
||||
const currentLevel = currentState?.player?.level ?? 0;
|
||||
const hidesBeforeBank = currentState?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
|
||||
|
||||
console.log(`Current floor level: ${currentLevel}, hides in inventory: ${hidesBeforeBank}`);
|
||||
|
||||
// If we're at ground level (0), we need to go up stairs
|
||||
if (currentLevel === 0) {
|
||||
console.log('Walking to castle entrance...');
|
||||
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_CASTLE_ENTRANCE.x, LOCATIONS.LUMBRIDGE_CASTLE_ENTRANCE.z);
|
||||
|
||||
console.log('Walking to castle stairs...');
|
||||
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_STAIRS_GROUND.x, LOCATIONS.LUMBRIDGE_STAIRS_GROUND.z);
|
||||
|
||||
// Climb up stairs to level 1
|
||||
console.log('Climbing to first floor...');
|
||||
const nearbyLocs = ctx.sdk.getState()?.nearbyLocs ?? [];
|
||||
console.log(`Nearby locs: ${nearbyLocs.slice(0, 5).map(l => l.name).join(', ')}`);
|
||||
|
||||
const stairs1 = nearbyLocs.find(l => /staircase/i.test(l.name));
|
||||
if (stairs1) {
|
||||
console.log(`Found stairs: ${stairs1.name} at (${stairs1.x}, ${stairs1.z})`);
|
||||
console.log(`Options: ${stairs1.optionsWithIndex.map(o => o.text).join(', ')}`);
|
||||
const climbOpt = stairs1.optionsWithIndex.find(o => /climb.?up/i.test(o.text));
|
||||
if (climbOpt) {
|
||||
console.log(`Using climb option: ${climbOpt.text}`);
|
||||
await ctx.sdk.sendInteractLoc(stairs1.x, stairs1.z, stairs1.id, climbOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
} else {
|
||||
console.warn('No climb-up option found on stairs');
|
||||
}
|
||||
} else {
|
||||
console.warn('No staircase found nearby!');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're on first floor (level 1), need to go to level 2
|
||||
const midState = ctx.sdk.getState();
|
||||
const midLevel = midState?.player?.level ?? 0;
|
||||
console.log(`After first climb, floor level: ${midLevel}`);
|
||||
|
||||
if (midLevel === 1) {
|
||||
// Find stairs to climb to level 2
|
||||
console.log('Climbing to second floor (bank floor)...');
|
||||
const stairs2 = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
|
||||
if (stairs2) {
|
||||
const climbOpt = stairs2.optionsWithIndex.find(o => /climb.?up/i.test(o.text));
|
||||
if (climbOpt) {
|
||||
await ctx.sdk.sendInteractLoc(stairs2.x, stairs2.z, stairs2.id, climbOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we should be at level 2, walk to bank area
|
||||
console.log('Walking to bank...');
|
||||
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_BANK.x, LOCATIONS.LUMBRIDGE_BANK.z);
|
||||
|
||||
// Open bank - find bank booth or banker
|
||||
console.log('Opening bank...');
|
||||
let bankOpened = false;
|
||||
|
||||
// Try bank booth first
|
||||
const bankBooth = ctx.sdk.getState()?.nearbyLocs.find(l => /bank booth|bank chest/i.test(l.name));
|
||||
if (bankBooth) {
|
||||
const bankOpt = bankBooth.optionsWithIndex.find(o => /^bank$/i.test(o.text)) ||
|
||||
bankBooth.optionsWithIndex.find(o => /use/i.test(o.text)) ||
|
||||
bankBooth.optionsWithIndex[0];
|
||||
if (bankOpt) {
|
||||
console.log(`Using bank booth option: ${bankOpt.text}`);
|
||||
await ctx.sdk.sendInteractLoc(bankBooth.x, bankBooth.z, bankBooth.id, bankOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Wait for interface to open
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.interface?.isOpen) {
|
||||
bankOpened = true;
|
||||
console.log('Bank interface opened!');
|
||||
break;
|
||||
}
|
||||
if (state?.dialog?.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} else {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no bank booth, try banker NPC
|
||||
if (!bankOpened) {
|
||||
const banker = ctx.sdk.findNearbyNpc(/banker/i);
|
||||
if (banker) {
|
||||
const bankOpt = banker.optionsWithIndex.find(o => /bank/i.test(o.text));
|
||||
if (bankOpt) {
|
||||
console.log(`Using banker: ${banker.name}`);
|
||||
await ctx.sdk.sendInteractNpc(banker.index, bankOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Wait for interface to open
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.interface?.isOpen) {
|
||||
bankOpened = true;
|
||||
console.log('Bank interface opened!');
|
||||
break;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bankOpened) {
|
||||
console.warn('Failed to open bank interface - aborting banking trip');
|
||||
// Return to cow field without depositing
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
console.log('=== Banking failed, back at cow field ===');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Count hides before depositing
|
||||
const hidesBefore = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
|
||||
|
||||
// Deposit all cow hides
|
||||
const hides = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)) ?? [];
|
||||
|
||||
for (const hide of hides) {
|
||||
console.log(`Depositing ${hide.name} from slot ${hide.slot}...`);
|
||||
await ctx.sdk.sendBankDeposit(hide.slot, hide.count);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Wait for items to leave inventory
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Verify deposits worked by checking inventory
|
||||
const hidesAfter = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
|
||||
const actualDeposited = hidesBefore - hidesAfter;
|
||||
|
||||
if (actualDeposited > 0) {
|
||||
stats.hidesBanked += actualDeposited;
|
||||
console.log(`Deposited ${actualDeposited} hides. Total banked: ${stats.hidesBanked}`);
|
||||
} else {
|
||||
console.warn(`Deposit failed - hides still in inventory (${hidesAfter})`);
|
||||
}
|
||||
|
||||
// Close bank interface by pressing escape or clicking close
|
||||
// The interface will close when we walk away anyway
|
||||
console.log('Returning to cow field...');
|
||||
|
||||
// Climb down to level 1
|
||||
const stairs = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
|
||||
if (stairs) {
|
||||
const downOpt = stairs.optionsWithIndex.find(o => /climb.?down/i.test(o.text));
|
||||
if (downOpt) {
|
||||
await ctx.sdk.sendInteractLoc(stairs.x, stairs.z, stairs.id, downOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
// Climb down to level 0
|
||||
const stairs2 = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
|
||||
if (stairs2) {
|
||||
const downOpt = stairs2.optionsWithIndex.find(o => /climb.?down/i.test(o.text));
|
||||
if (downOpt) {
|
||||
await ctx.sdk.sendInteractLoc(stairs2.x, stairs2.z, stairs2.id, downOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
// Walk back to cow field
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
|
||||
console.log('=== Back at cow field ===');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log current statistics
|
||||
*/
|
||||
function logStats(ctx: ScriptContext, stats: HideStats): void {
|
||||
const elapsed = Math.floor((Date.now() - stats.startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
|
||||
const hidesPerMinute = elapsed > 0 ? (stats.hidesBanked / (elapsed / 60)).toFixed(1) : '0';
|
||||
|
||||
console.log(`--- Stats (${minutes}m ${seconds}s) ---`);
|
||||
console.log(`Kills: ${stats.kills}, Hides collected: ${stats.hidesCollected}`);
|
||||
console.log(`Hides banked: ${stats.hidesBanked}, Bank trips: ${stats.bankTrips}`);
|
||||
console.log(`Rate: ${hidesPerMinute} hides/min`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should clear inventory (drop hides to make space)
|
||||
*/
|
||||
function shouldDropHides(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
|
||||
// Drop hides if inventory is getting full
|
||||
return state.inventory.length >= INVENTORY_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop cow hides to make inventory space
|
||||
*/
|
||||
async function dropHides(ctx: ScriptContext, stats: HideStats): Promise<void> {
|
||||
console.log('=== Dropping hides to make space ===');
|
||||
|
||||
const hides = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)) ?? [];
|
||||
|
||||
for (const hide of hides) {
|
||||
console.log(`Dropping ${hide.name}...`);
|
||||
await ctx.sdk.sendDropItem(hide.slot);
|
||||
stats.hidesCollected++; // Count as collected even though dropped
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
console.log(`Dropped ${hides.length} hides. Continuing training...`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main cow hide collecting loop
|
||||
*/
|
||||
async function cowhideLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
const stats: HideStats = {
|
||||
kills: 0,
|
||||
hidesCollected: 0,
|
||||
hidesBanked: 0,
|
||||
bankTrips: 0,
|
||||
startTime: Date.now(),
|
||||
lastProgressTime: Date.now(),
|
||||
};
|
||||
|
||||
console.log('=== Cow Hide Banking Script Started ===');
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
|
||||
// Equip combat gear
|
||||
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
|
||||
if (sword) {
|
||||
console.log('Equipping bronze sword...');
|
||||
await ctx.bot.equipItem(sword);
|
||||
}
|
||||
|
||||
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
|
||||
if (shield) {
|
||||
console.log('Equipping wooden shield...');
|
||||
await ctx.bot.equipItem(shield);
|
||||
}
|
||||
|
||||
// Set combat style to Aggressive for Strength XP
|
||||
console.log('Setting combat style to Aggressive (Strength training)...');
|
||||
await ctx.sdk.sendSetCombatStyle(1); // 0=accurate, 1=aggressive, 2=defensive, 3=controlled
|
||||
|
||||
// Walk to cow field
|
||||
console.log('Walking to cow field...');
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
|
||||
let lastStatsLog = 0;
|
||||
|
||||
// Main loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any blocking dialogs (level-up messages)
|
||||
if (currentState.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check HP and eat food if needed
|
||||
const hp = currentState.skills.find(s => s.name === 'Hitpoints');
|
||||
if (hp && hp.level < hp.baseLevel * 0.5) {
|
||||
// Try to eat available food
|
||||
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|kebab)$/i);
|
||||
if (food) {
|
||||
console.log(`HP low (${hp.level}/${hp.baseLevel}) - eating ${food.name}`);
|
||||
await ctx.bot.eatFood(food);
|
||||
continue;
|
||||
} else {
|
||||
console.warn(`HP low (${hp.level}/${hp.baseLevel}) but no food!`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log stats periodically (every 10 kills)
|
||||
if (stats.kills > 0 && stats.kills % 10 === 0 && stats.kills !== lastStatsLog) {
|
||||
lastStatsLog = stats.kills;
|
||||
logStats(ctx, stats);
|
||||
}
|
||||
|
||||
// Check if we need to drop hides to make space (simplified - no banking for now)
|
||||
if (shouldDropHides(ctx)) {
|
||||
await dropHides(ctx, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to pick up any nearby hides first
|
||||
await pickupHides(ctx, stats);
|
||||
|
||||
// Find a cow to attack
|
||||
const cow = findBestCow(ctx);
|
||||
if (!cow) {
|
||||
// No cows nearby, walk to cow field center
|
||||
console.log('No cows nearby, walking to cow field...');
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already in combat with this cow
|
||||
const playerCombat = currentState.player?.combat;
|
||||
if (playerCombat?.inCombat && playerCombat.targetIndex === cow.index) {
|
||||
const result = await waitForCombatEnd(ctx, cow, stats);
|
||||
console.log(`Combat ended: ${result}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack the cow
|
||||
console.log(`Attacking ${cow.name} (dist: ${cow.distance})`);
|
||||
const attackResult = await ctx.bot.attackNpc(cow);
|
||||
|
||||
if (!attackResult.success) {
|
||||
console.warn(`Attack failed: ${attackResult.message}`);
|
||||
|
||||
// Try to open gate if blocked
|
||||
if (attackResult.reason === 'out_of_reach') {
|
||||
console.log('Trying to open gate...');
|
||||
const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
if (gateResult.success) {
|
||||
console.log('Gate opened!');
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for combat to complete
|
||||
const combatResult = await waitForCombatEnd(ctx, cow, stats);
|
||||
|
||||
if (combatResult === 'kill') {
|
||||
console.log(`Kill #${stats.kills}!`);
|
||||
// Immediately try to pick up the hide
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
await pickupHides(ctx, stats);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `ch${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await cowhideLoop(ctx);
|
||||
} finally {
|
||||
// Log final stats
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
console.log('=== Final Results ===');
|
||||
const hides = state.inventory.filter(i => /cow\s*hide/i.test(i.name));
|
||||
console.log(`Hides in inventory: ${hides.length}`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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()`
|
||||
@@ -1,869 +0,0 @@
|
||||
/**
|
||||
* Crafting Training Script
|
||||
*
|
||||
* Goal: Train Crafting from level 1 to level 10+ starting from Lumbridge spawn.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Sell shortbow at general store to get coins
|
||||
* 2. Collect cowhides from cow field
|
||||
* 3. Travel to Al Kharid (pay toll)
|
||||
* 4. Buy needle + thread from craft shop
|
||||
* 5. Tan hides at Ellis
|
||||
* 6. Craft leather items
|
||||
* 7. Repeat as needed
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Locations
|
||||
const LOCATIONS = {
|
||||
LUMBRIDGE_GENERAL_STORE: { x: 3211, z: 3247 },
|
||||
COW_FIELD: { x: 3253, z: 3270 },
|
||||
TOLL_GATE: { x: 3268, z: 3228 },
|
||||
INSIDE_AL_KHARID: { x: 3277, z: 3227 },
|
||||
// Dommik's Crafting Store - trying multiple locations
|
||||
// Standard OSRS location should be around (3321, 3179) - east of general store
|
||||
AL_KHARID_CRAFT_SHOP: { x: 3321, z: 3179 },
|
||||
AL_KHARID_TANNER: { x: 3274, z: 3192 },
|
||||
AL_KHARID_BANK: { x: 3269, z: 3167 },
|
||||
};
|
||||
|
||||
// Configuration
|
||||
// Note: Server has tick freeze issues during extended combat (known bug)
|
||||
// Using small batches (2 hides) for speed
|
||||
const HIDES_PER_TRIP = 2; // Collect this many hides before crafting
|
||||
const TARGET_LEVEL = 10;
|
||||
|
||||
// Stats tracking
|
||||
interface CraftingStats {
|
||||
hidesCollected: number;
|
||||
hidesTanned: number;
|
||||
itemsCrafted: number;
|
||||
xpGained: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
// ============ Helper Functions ============
|
||||
|
||||
function getCoins(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
function getHideCount(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name))
|
||||
.reduce((sum, i) => sum + i.count, 0) ?? 0;
|
||||
}
|
||||
|
||||
function getLeatherCount(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.inventory.filter(i => /^leather$/i.test(i.name))
|
||||
.reduce((sum, i) => sum + i.count, 0) ?? 0;
|
||||
}
|
||||
|
||||
function getCraftingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Crafting')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getCraftingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Crafting')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function isInsideAlKharid(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return false;
|
||||
return state.player.worldX >= 3270;
|
||||
}
|
||||
|
||||
function hasNeedle(ctx: ScriptContext): boolean {
|
||||
return ctx.sdk.findInventoryItem(/needle/i) !== null;
|
||||
}
|
||||
|
||||
function hasThread(ctx: ScriptContext): boolean {
|
||||
return ctx.sdk.findInventoryItem(/thread/i) !== null;
|
||||
}
|
||||
|
||||
async function dismissDialogs(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
|
||||
while (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Phase 1: Get Initial Coins ============
|
||||
|
||||
async function getInitialCoins(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
|
||||
if (getCoins(ctx) >= 30) {
|
||||
console.log(`Already have ${getCoins(ctx)}gp`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Phase 1: Getting coins from general store...');
|
||||
|
||||
// Walk to general store
|
||||
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_GENERAL_STORE.x, LOCATIONS.LUMBRIDGE_GENERAL_STORE.z);
|
||||
|
||||
// Open shop
|
||||
const openResult = await ctx.bot.openShop(/shop keeper/i);
|
||||
if (!openResult.success) {
|
||||
console.warn(`Failed to open shop: ${openResult.message}`);
|
||||
return;
|
||||
}
|
||||
console.log('Shop opened');
|
||||
|
||||
// Sell shortbow (worth ~20gp)
|
||||
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 'all');
|
||||
if (sellResult.success) {
|
||||
console.log(sellResult.message);
|
||||
}
|
||||
|
||||
// Close shop
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
console.log(`Have ${getCoins(ctx)}gp after selling`);
|
||||
}
|
||||
|
||||
// ============ Phase 2: Collect Cowhides ============
|
||||
|
||||
function findBestCow(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const cows = state.nearbyNpcs
|
||||
.filter(npc => /^cow$/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(o => /attack/i.test(o)))
|
||||
.filter(npc => !npc.inCombat || npc.targetIndex === -1)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return cows[0] ?? null;
|
||||
}
|
||||
|
||||
async function waitForCombatEnd(
|
||||
ctx: ScriptContext,
|
||||
targetNpc: NearbyNpc,
|
||||
stats: CraftingStats
|
||||
): Promise<'kill' | 'fled' | 'lost_target'> {
|
||||
let combatStarted = false;
|
||||
let ticksSinceCombatEnded = 0;
|
||||
let loopCount = 0;
|
||||
|
||||
const maxWaitMs = 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
loopCount++;
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 'lost_target';
|
||||
|
||||
// Dismiss level-up dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
console.log('Dismissing dialog during combat...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentTick = state.tick;
|
||||
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
|
||||
|
||||
if (!target) {
|
||||
if (combatStarted || loopCount >= 2) {
|
||||
return 'kill';
|
||||
}
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
if (target.maxHp > 0 && target.hp === 0) {
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
const npcInCombat = target.combatCycle > currentTick;
|
||||
const playerInCombat = state.player?.combat?.inCombat ?? false;
|
||||
const inActiveCombat = playerInCombat || npcInCombat;
|
||||
|
||||
if (inActiveCombat) {
|
||||
combatStarted = true;
|
||||
ticksSinceCombatEnded = 0;
|
||||
} else if (combatStarted) {
|
||||
ticksSinceCombatEnded++;
|
||||
if (ticksSinceCombatEnded >= 4) {
|
||||
return 'fled';
|
||||
}
|
||||
} else if (loopCount >= 8) {
|
||||
return 'lost_target';
|
||||
}
|
||||
}
|
||||
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
async function collectCowhides(ctx: ScriptContext, stats: CraftingStats, targetHides: number): Promise<void> {
|
||||
console.log(`Phase 2: Collecting ${targetHides} cowhides...`);
|
||||
|
||||
// Equip combat gear
|
||||
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
|
||||
if (sword) {
|
||||
await ctx.bot.equipItem(sword);
|
||||
}
|
||||
|
||||
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
|
||||
if (shield) {
|
||||
await ctx.bot.equipItem(shield);
|
||||
}
|
||||
|
||||
// Walk to cow field
|
||||
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
|
||||
|
||||
while (getHideCount(ctx) < targetHides) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) break;
|
||||
|
||||
// Check inventory space
|
||||
if (state.inventory.length >= 28) {
|
||||
console.log('Inventory full, stopping collection');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss dialogs
|
||||
await dismissDialogs(ctx, stats);
|
||||
|
||||
// Pick up nearby hides
|
||||
const groundHides = ctx.sdk.getGroundItems()
|
||||
.filter(i => /cow\s*hide/i.test(i.name))
|
||||
.filter(i => i.distance <= 8)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
for (const hide of groundHides.slice(0, 2)) {
|
||||
if (ctx.sdk.getState()!.inventory.length >= 28) break;
|
||||
const result = await ctx.bot.pickupItem(hide);
|
||||
if (result.success) {
|
||||
stats.hidesCollected++;
|
||||
console.log(`Picked up hide (total: ${getHideCount(ctx)})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Find cow to kill
|
||||
const cow = findBestCow(ctx);
|
||||
if (!cow) {
|
||||
console.log('No cows nearby, walking around...');
|
||||
await ctx.bot.walkTo(
|
||||
LOCATIONS.COW_FIELD.x + (Math.random() - 0.5) * 20,
|
||||
LOCATIONS.COW_FIELD.z + (Math.random() - 0.5) * 20
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already in combat
|
||||
const playerCombat = state.player?.combat;
|
||||
if (playerCombat?.inCombat) {
|
||||
await waitForCombatEnd(ctx, cow, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack cow
|
||||
const attackResult = await ctx.bot.attackNpc(cow);
|
||||
if (!attackResult.success) {
|
||||
if (attackResult.reason === 'out_of_reach') {
|
||||
const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
if (gateResult.success) {
|
||||
console.log('Opened gate');
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for kill
|
||||
const combatResult = await waitForCombatEnd(ctx, cow, stats);
|
||||
if (combatResult === 'kill') {
|
||||
console.log(`Kill! Hides: ${getHideCount(ctx)}`);
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Collected ${getHideCount(ctx)} hides`);
|
||||
}
|
||||
|
||||
// ============ Phase 3: Travel to Al Kharid ============
|
||||
|
||||
// Check if player is in the cow field area (fenced area)
|
||||
function isInCowField(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return false;
|
||||
const x = state.player.worldX;
|
||||
const z = state.player.worldZ;
|
||||
// Cow field is roughly (3242-3265, 3255-3298) - inside the fenced area
|
||||
return x >= 3242 && x <= 3265 && z >= 3255 && z <= 3298;
|
||||
}
|
||||
|
||||
async function exitCowField(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
|
||||
console.log('Exiting cow field...');
|
||||
|
||||
// The cow field gate is on the south side around (3253, 3255)
|
||||
// Try to find and open any nearby gate
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
// Look for a gate
|
||||
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 10);
|
||||
|
||||
if (gate) {
|
||||
const openOpt = gate.optionsWithIndex.find(o => /open/i.test(o.text));
|
||||
if (openOpt) {
|
||||
console.log(`Opening cow field gate at (${gate.x}, ${gate.z})`);
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
}
|
||||
}
|
||||
|
||||
// Walk towards the exit point just south of the cow field
|
||||
const result = await ctx.bot.walkTo(3253, 3250);
|
||||
if (result.success && !isInCowField(ctx)) {
|
||||
console.log('Exited cow field');
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we're not making progress, try opening door
|
||||
if (!result.success) {
|
||||
const doorResult = await ctx.bot.openDoor(/gate/i);
|
||||
if (doorResult.success) {
|
||||
console.log('Opened gate via openDoor');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !isInCowField(ctx);
|
||||
}
|
||||
|
||||
async function travelToAlKharid(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
|
||||
if (isInsideAlKharid(ctx)) {
|
||||
console.log('Already in Al Kharid');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Phase 3: Traveling to Al Kharid...');
|
||||
|
||||
if (getCoins(ctx) < 10) {
|
||||
console.warn(`Not enough coins for toll (have ${getCoins(ctx)})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// First, exit the cow field if we're in it
|
||||
if (isInCowField(ctx)) {
|
||||
const exited = await exitCowField(ctx, stats);
|
||||
if (!exited) {
|
||||
console.warn('Could not exit cow field');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk to gate - make sure we're at the correct position
|
||||
console.log(`Walking to toll gate from (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
|
||||
await ctx.bot.walkTo(LOCATIONS.TOLL_GATE.x, LOCATIONS.TOLL_GATE.z);
|
||||
await new Promise(r => setTimeout(r, 1000)); // Wait for pathfinding
|
||||
|
||||
// Find and interact with gate
|
||||
let gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 15);
|
||||
|
||||
// If gate not found, try walking closer to known toll gate position
|
||||
if (!gate) {
|
||||
console.log('Gate not found, walking to exact toll gate position...');
|
||||
await ctx.bot.walkTo(3267, 3228); // Exact toll gate position
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 15);
|
||||
}
|
||||
if (!gate) {
|
||||
console.warn('No gate found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const openOpt = gate.optionsWithIndex.find(o => /pay|open/i.test(o.text));
|
||||
if (!openOpt) {
|
||||
console.warn('No open/pay option on gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Click gate to trigger dialog
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Handle toll dialog
|
||||
let paidToll = false;
|
||||
for (let i = 0; i < 20 && !paidToll; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
continue;
|
||||
}
|
||||
|
||||
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Paying toll...');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
paidToll = true;
|
||||
} else {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Wait and dismiss remaining dialogs
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
await dismissDialogs(ctx, stats);
|
||||
|
||||
// Walk through
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await ctx.bot.walkTo(LOCATIONS.INSIDE_AL_KHARID.x, LOCATIONS.INSIDE_AL_KHARID.z);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
if (isInsideAlKharid(ctx)) break;
|
||||
}
|
||||
|
||||
const success = isInsideAlKharid(ctx);
|
||||
console.log(success ? 'Arrived in Al Kharid!' : 'Failed to enter Al Kharid');
|
||||
return success;
|
||||
}
|
||||
|
||||
// ============ Phase 4: Buy Crafting Supplies ============
|
||||
|
||||
async function buyCraftingSupplies(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
|
||||
if (hasNeedle(ctx) && hasThread(ctx)) {
|
||||
console.log('Already have needle and thread');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Phase 4: Buying crafting supplies...');
|
||||
|
||||
// Walk to craft shop
|
||||
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_CRAFT_SHOP.x, LOCATIONS.AL_KHARID_CRAFT_SHOP.z);
|
||||
|
||||
// Log player position for debugging
|
||||
const pos = ctx.sdk.getState()?.player;
|
||||
console.log(`Player at: (${pos?.worldX}, ${pos?.worldZ})`);
|
||||
|
||||
// Log all nearby NPCs to find Dommik
|
||||
const allNpcs = ctx.sdk.getState()?.nearbyNpcs || [];
|
||||
console.log(`All NPCs nearby (${allNpcs.length}): ${allNpcs.slice(0, 15).map(n => `${n.name}(${n.distance})`).join(', ')}`);
|
||||
|
||||
// Find Dommik the crafting shop owner
|
||||
let shopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
|
||||
if (!shopkeeper) {
|
||||
// Try broader search
|
||||
shopkeeper = ctx.sdk.findNearbyNpc(/craft/i);
|
||||
}
|
||||
if (!shopkeeper) {
|
||||
// Dommik might not be loaded yet - try walking around to find more NPCs
|
||||
console.log('Dommik not found, searching nearby...');
|
||||
|
||||
// Try walking to different spots in the area
|
||||
const searchSpots = [
|
||||
{ x: 3321, z: 3179 },
|
||||
{ x: 3316, z: 3175 },
|
||||
{ x: 3325, z: 3180 },
|
||||
{ x: 3318, z: 3183 },
|
||||
];
|
||||
|
||||
for (const spot of searchSpots) {
|
||||
await ctx.bot.walkTo(spot.x, spot.z);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
shopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
|
||||
if (shopkeeper) {
|
||||
console.log(`Found Dommik at spot (${spot.x}, ${spot.z})`);
|
||||
break;
|
||||
}
|
||||
|
||||
const npcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 10).map(n => `${n.name}(${n.distance})`).join(', ') ?? 'none';
|
||||
console.log(`At (${spot.x}, ${spot.z}): NPCs = ${npcs}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shopkeeper) {
|
||||
console.warn(`Could not find Dommik after searching`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Found shopkeeper: ${shopkeeper.name} at distance ${shopkeeper.distance}`);
|
||||
|
||||
// Open shop using Trade option
|
||||
const tradeOpt = shopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
|
||||
if (!tradeOpt) {
|
||||
console.warn(`No trade option on Dommik. Options: ${shopkeeper.options.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Trading with Dommik (distance: ${shopkeeper.distance})...`);
|
||||
await ctx.sdk.sendInteractNpc(shopkeeper.index, tradeOpt.opIndex);
|
||||
|
||||
// Wait for shop to open (longer wait - interaction will walk us closer)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
// Check for shop opening
|
||||
if (ctx.sdk.getState()?.shop.isOpen) break;
|
||||
|
||||
// If dialog opened instead (Talk-to triggered), dismiss it
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
console.log('Dialog opened, clicking through...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.sdk.getState()?.shop.isOpen) {
|
||||
// Maybe we need to open a door first?
|
||||
const door = ctx.sdk.getState()?.nearbyLocs.find(l => /door/i.test(l.name) && l.distance < 5);
|
||||
if (door) {
|
||||
console.log('Trying to open door...');
|
||||
const openOpt = door.optionsWithIndex.find(o => /open/i.test(o.text));
|
||||
if (openOpt) {
|
||||
await ctx.sdk.sendInteractLoc(door.x, door.z, door.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Try trading again
|
||||
const newShopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
|
||||
if (newShopkeeper) {
|
||||
const newTradeOpt = newShopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
|
||||
if (newTradeOpt) {
|
||||
await ctx.sdk.sendInteractNpc(newShopkeeper.index, newTradeOpt.opIndex);
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
if (ctx.sdk.getState()?.shop.isOpen) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.sdk.getState()?.shop.isOpen) {
|
||||
console.warn('Shop still did not open');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Shop opened: ${ctx.sdk.getState()?.shop.title}`);
|
||||
|
||||
// Buy needle if needed
|
||||
if (!hasNeedle(ctx)) {
|
||||
const buyResult = await ctx.bot.buyFromShop(/needle/i, 1);
|
||||
console.log(buyResult.success ? 'Bought needle' : `Failed to buy needle: ${buyResult.message}`);
|
||||
}
|
||||
|
||||
// Buy thread if needed
|
||||
if (!hasThread(ctx)) {
|
||||
const buyResult = await ctx.bot.buyFromShop(/thread/i, 5);
|
||||
console.log(buyResult.success ? 'Bought thread' : `Failed to buy thread: ${buyResult.message}`);
|
||||
}
|
||||
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
return hasNeedle(ctx) && hasThread(ctx);
|
||||
}
|
||||
|
||||
// ============ Phase 5: Tan Hides ============
|
||||
|
||||
async function tanHides(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
|
||||
const hideCount = getHideCount(ctx);
|
||||
if (hideCount === 0) {
|
||||
console.log('No hides to tan');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Phase 5: Tanning ${hideCount} hides...`);
|
||||
|
||||
// Walk to tanner
|
||||
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_TANNER.x, LOCATIONS.AL_KHARID_TANNER.z);
|
||||
|
||||
// Find the tanner
|
||||
const tanner = ctx.sdk.findNearbyNpc(/^tanner$/i);
|
||||
if (!tanner) {
|
||||
const npcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 5).map(n => `${n.name}(${n.options.join('/')})`).join(', ') ?? 'none';
|
||||
console.warn(`No tanner found. Nearby NPCs: ${npcs}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found tanner: ${tanner.name} at distance ${tanner.distance}, options: ${tanner.options.join(', ')}`);
|
||||
|
||||
// Talk to the tanner (uses Talk-to, not Trade)
|
||||
const talkOpt = tanner.optionsWithIndex.find(o => /talk/i.test(o.text));
|
||||
if (!talkOpt) {
|
||||
console.warn('No talk option on tanner');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Talking to tanner...');
|
||||
await ctx.sdk.sendInteractNpc(tanner.index, talkOpt.opIndex);
|
||||
|
||||
// Wait for dialog - game will auto-walk if needed
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
if (ctx.sdk.getState()?.dialog?.isOpen) break;
|
||||
}
|
||||
|
||||
if (!ctx.sdk.getState()?.dialog?.isOpen) {
|
||||
console.warn('Dialog did not open with tanner');
|
||||
// Log current state
|
||||
const player = ctx.sdk.getState()?.player;
|
||||
console.log(`Player at (${player?.worldX}, ${player?.worldZ})`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Tanner dialog opened!');
|
||||
|
||||
// Handle dialog - need to:
|
||||
// 1. Click through initial greeting
|
||||
// 2. Say "Yes please" when asked about tanning hides
|
||||
// 3. Select "Soft leather" option
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog?.isOpen) {
|
||||
// Dialog closed - tanning should be complete
|
||||
console.log('Dialog closed');
|
||||
break;
|
||||
}
|
||||
|
||||
const options = s.dialog.options;
|
||||
const optText = options.map(o => o.text).join(' | ');
|
||||
console.log(`Dialog options: ${optText}`);
|
||||
|
||||
// Look for "Soft leather" option first (this is what we want)
|
||||
const softLeatherOpt = options.find(o => /soft leather/i.test(o.text));
|
||||
if (softLeatherOpt) {
|
||||
console.log('Selecting "Soft leather"');
|
||||
await ctx.sdk.sendClickDialog(softLeatherOpt.index);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for "Yes please" to confirm tanning
|
||||
const yesOpt = options.find(o => /yes.*please/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Clicking "Yes please"');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If just "Click here to continue", click it
|
||||
if (options.length === 1 && /continue/i.test(options[0]!.text)) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default - click option 0
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
|
||||
// Wait for tanning to complete
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
const leather = getLeatherCount(ctx);
|
||||
const hidesLeft = getHideCount(ctx);
|
||||
stats.hidesTanned += leather;
|
||||
console.log(`Tanning complete. Leather: ${leather}, Hides remaining: ${hidesLeft}`);
|
||||
}
|
||||
|
||||
// ============ Phase 6: Craft Leather Items ============
|
||||
|
||||
async function craftLeatherItems(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
|
||||
const leatherCount = getLeatherCount(ctx);
|
||||
if (leatherCount === 0) {
|
||||
console.log('No leather to craft');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Phase 6: Crafting with ${leatherCount} leather...`);
|
||||
|
||||
const xpBefore = getCraftingXp(ctx);
|
||||
|
||||
// Craft all leather using the porcelain function
|
||||
while (getLeatherCount(ctx) > 0) {
|
||||
const result = await ctx.bot.craftLeather('gloves');
|
||||
|
||||
if (result.success) {
|
||||
console.log(`${result.message}`);
|
||||
stats.itemsCrafted += result.itemsCrafted ?? 1;
|
||||
stats.xpGained += result.xpGained ?? 0;
|
||||
} else {
|
||||
console.warn(`Craft failed: ${result.message} (reason: ${result.reason})`);
|
||||
|
||||
// If level too low, we can't craft anything
|
||||
if (result.reason === 'level_too_low') {
|
||||
break;
|
||||
}
|
||||
|
||||
// If interface didn't open, the game might be stuck - try again
|
||||
if (result.reason === 'interface_not_opened') {
|
||||
await dismissDialogs(ctx, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no materials, stop
|
||||
if (result.reason === 'no_leather' || result.reason === 'no_needle' || result.reason === 'no_thread') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Timeout or no XP gained - try again once
|
||||
if (result.reason === 'timeout' || result.reason === 'no_xp_gained') {
|
||||
await dismissDialogs(ctx, stats);
|
||||
// Try one more time then give up
|
||||
const retry = await ctx.bot.craftLeather('gloves');
|
||||
if (!retry.success) {
|
||||
console.warn(`Retry also failed: ${retry.message}`);
|
||||
break;
|
||||
}
|
||||
console.log(`Retry succeeded: ${retry.message}`);
|
||||
stats.itemsCrafted += retry.itemsCrafted ?? 1;
|
||||
stats.xpGained += retry.xpGained ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss any level-up dialogs between crafts
|
||||
await dismissDialogs(ctx, stats);
|
||||
}
|
||||
|
||||
const totalXpGained = getCraftingXp(ctx) - xpBefore;
|
||||
console.log(`Crafting complete. Total: +${totalXpGained} XP. Level: ${getCraftingLevel(ctx)}`);
|
||||
}
|
||||
|
||||
// ============ Phase 7: Bank and Repeat ============
|
||||
|
||||
async function bankItems(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
|
||||
console.log('Banking items...');
|
||||
|
||||
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_BANK.x, LOCATIONS.AL_KHARID_BANK.z);
|
||||
|
||||
const openResult = await ctx.bot.openBank();
|
||||
if (!openResult.success) {
|
||||
console.warn(`Failed to open bank: ${openResult.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deposit crafted items (leather gloves, etc.)
|
||||
const craftedItems = ctx.sdk.getState()?.inventory.filter(i =>
|
||||
/gloves|boots|vamb|chaps|body|cowl/i.test(i.name) && /leather/i.test(i.name)
|
||||
) ?? [];
|
||||
|
||||
for (const item of craftedItems) {
|
||||
await ctx.sdk.sendBankDeposit(item.slot, item.count);
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
await ctx.bot.closeBank();
|
||||
}
|
||||
|
||||
// ============ Main Loop ============
|
||||
|
||||
async function craftingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
const stats: CraftingStats = {
|
||||
hidesCollected: 0,
|
||||
hidesTanned: 0,
|
||||
itemsCrafted: 0,
|
||||
xpGained: 0,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
console.log('=== Crafting Training Script ===');
|
||||
console.log(`Starting level: ${getCraftingLevel(ctx)}`);
|
||||
console.log(`Target level: ${TARGET_LEVEL}`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
|
||||
// Get initial coins
|
||||
await getInitialCoins(ctx, stats);
|
||||
|
||||
let cycle = 0;
|
||||
while (getCraftingLevel(ctx) < TARGET_LEVEL) {
|
||||
cycle++;
|
||||
console.log(`\n=== Cycle ${cycle} ===`);
|
||||
console.log(`Level: ${getCraftingLevel(ctx)}, XP: ${getCraftingXp(ctx)}`);
|
||||
|
||||
// Collect hides if we don't have enough
|
||||
if (getHideCount(ctx) < HIDES_PER_TRIP && !isInsideAlKharid(ctx)) {
|
||||
await collectCowhides(ctx, stats, HIDES_PER_TRIP);
|
||||
}
|
||||
|
||||
// Travel to Al Kharid if not there
|
||||
if (!isInsideAlKharid(ctx)) {
|
||||
const success = await travelToAlKharid(ctx, stats);
|
||||
if (!success) {
|
||||
throw new Error('Could not travel to Al Kharid');
|
||||
}
|
||||
}
|
||||
|
||||
// Buy supplies if needed
|
||||
if (!hasNeedle(ctx) || !hasThread(ctx)) {
|
||||
const success = await buyCraftingSupplies(ctx, stats);
|
||||
if (!success) {
|
||||
throw new Error('Could not buy crafting supplies');
|
||||
}
|
||||
}
|
||||
|
||||
// Tan hides
|
||||
if (getHideCount(ctx) > 0) {
|
||||
await tanHides(ctx, stats);
|
||||
}
|
||||
|
||||
// Craft items
|
||||
if (getLeatherCount(ctx) > 0) {
|
||||
await craftLeatherItems(ctx, stats);
|
||||
}
|
||||
|
||||
// Bank crafted items if inventory is getting full
|
||||
if (ctx.sdk.getState()!.inventory.length >= 25) {
|
||||
await bankItems(ctx, stats);
|
||||
}
|
||||
|
||||
// If we've used all materials and still need more levels, go get more hides
|
||||
if (getHideCount(ctx) === 0 && getLeatherCount(ctx) === 0 && getCraftingLevel(ctx) < TARGET_LEVEL) {
|
||||
console.log('Need more hides, returning to Lumbridge...');
|
||||
// Walk back towards Lumbridge (toll gate is free to exit)
|
||||
await ctx.bot.walkTo(LOCATIONS.TOLL_GATE.x, LOCATIONS.TOLL_GATE.z);
|
||||
// Walk to Lumbridge side
|
||||
await ctx.bot.walkTo(3260, 3228);
|
||||
}
|
||||
}
|
||||
|
||||
// Final stats
|
||||
const duration = (Date.now() - stats.startTime) / 1000;
|
||||
console.log('\n=== Final Results ===');
|
||||
console.log(`Duration: ${Math.round(duration)}s`);
|
||||
console.log(`Hides collected: ${stats.hidesCollected}`);
|
||||
console.log(`Hides tanned: ${stats.hidesTanned}`);
|
||||
console.log(`Items crafted: ${stats.itemsCrafted}`);
|
||||
console.log(`XP gained: ${stats.xpGained}`);
|
||||
console.log(`Final level: ${getCraftingLevel(ctx)}`);
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `cr${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await craftingLoop(ctx);
|
||||
} catch (e) {
|
||||
console.error(`Script aborted: ${e instanceof Error ? e.message : String(e)}`);
|
||||
throw e;
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 30 * 60 * 1000, // 30 minutes (leather crafting takes time)
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,314 +0,0 @@
|
||||
/**
|
||||
* Firemaking Training Script
|
||||
*
|
||||
* Goal: Train Firemaking from level 1 to level 10+ starting from Lumbridge.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Start at Lumbridge (preset has tinderbox)
|
||||
* 2. Chop trees to get logs
|
||||
* 3. Burn logs using tinderbox
|
||||
* 4. Repeat until level 10
|
||||
*
|
||||
* Notes:
|
||||
* - LUMBRIDGE_SPAWN preset includes a tinderbox
|
||||
* - Trees respawn quickly in Lumbridge
|
||||
* - Fire lighting can fail if standing on a fire or blocked tile
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyLoc, InventoryItem } from '../../sdk/types';
|
||||
|
||||
// Lumbridge tree locations (near castle)
|
||||
const LUMBRIDGE_TREES_AREA = { x: 3200, z: 3230 };
|
||||
|
||||
interface Stats {
|
||||
logsCut: number;
|
||||
logsBurned: number;
|
||||
startFiremakingXp: number;
|
||||
startWoodcuttingXp: number;
|
||||
startTime: number;
|
||||
lastProgressTime: number;
|
||||
}
|
||||
|
||||
// ============ Helper Functions ============
|
||||
|
||||
// Note: ctx.progress() removed - bot actions and movement auto-reset stall timer
|
||||
|
||||
function getFiremakingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Firemaking')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function getFiremakingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Firemaking')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getWoodcuttingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function getWoodcuttingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return null;
|
||||
return { x: state.player.worldX, z: state.player.worldZ };
|
||||
}
|
||||
|
||||
function countLogs(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
return state.inventory.filter(item => /^logs$/i.test(item.name)).reduce((sum, i) => sum + i.count, 0);
|
||||
}
|
||||
|
||||
function hasTinderbox(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
return state.inventory.some(item => /tinderbox/i.test(item.name));
|
||||
}
|
||||
|
||||
function hasAxe(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
// Check inventory and equipment for any axe
|
||||
const inInv = state.inventory.some(item => /axe/i.test(item.name));
|
||||
const equipped = state.equipment.some(item => item && /axe/i.test(item.name));
|
||||
return inInv || equipped;
|
||||
}
|
||||
|
||||
function findTree(ctx: ScriptContext): NearbyLoc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
// Find regular trees (not oak, willow, etc. - those require higher levels)
|
||||
const trees = state.nearbyLocs
|
||||
.filter(loc => /^tree$/i.test(loc.name))
|
||||
.filter(loc => loc.options.some(opt => /^chop/i.test(opt)))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return trees[0] ?? null;
|
||||
}
|
||||
|
||||
function getInventoryCount(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.inventory.length ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss dialogs (level-up dialogs, etc.)
|
||||
*/
|
||||
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
|
||||
let dismissed = 0;
|
||||
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
dismissed++;
|
||||
// progress auto-tracked
|
||||
}
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
// ============ Main Phases ============
|
||||
|
||||
/**
|
||||
* Phase 1: Chop trees until we have some logs
|
||||
*/
|
||||
async function chopTrees(ctx: ScriptContext, stats: Stats, targetLogs: number = 5): Promise<void> {
|
||||
console.log(`Chopping trees until we have ${targetLogs} logs...`);
|
||||
let attempts = 0;
|
||||
let noTreeCount = 0;
|
||||
|
||||
while (countLogs(ctx) < targetLogs && getInventoryCount(ctx) < 28) {
|
||||
attempts++;
|
||||
if (attempts % 10 === 0) // progress auto-tracked
|
||||
|
||||
// Dismiss any dialogs (level-up, etc.)
|
||||
if (await dismissDialogs(ctx, stats) > 0) continue;
|
||||
|
||||
// Find a tree
|
||||
const tree = findTree(ctx);
|
||||
if (!tree) {
|
||||
noTreeCount++;
|
||||
if (noTreeCount % 20 === 0) {
|
||||
console.log('Waiting for tree to respawn...');
|
||||
// Walk around a bit to find more trees
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (pos) {
|
||||
const offsetX = (Math.random() - 0.5) * 10;
|
||||
const offsetZ = (Math.random() - 0.5) * 10;
|
||||
await ctx.sdk.sendWalk(pos.x + offsetX, pos.z + offsetZ, true);
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
noTreeCount = 0;
|
||||
|
||||
// Chop the tree
|
||||
const logsBefore = countLogs(ctx);
|
||||
const result = await ctx.bot.chopTree(tree);
|
||||
// progress auto-tracked
|
||||
|
||||
if (result.success) {
|
||||
const logsAfter = countLogs(ctx);
|
||||
if (logsAfter > logsBefore) {
|
||||
stats.logsCut += logsAfter - logsBefore;
|
||||
console.log(`Got logs! Total: ${stats.logsCut} (inventory: ${logsAfter})`);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
console.log(`Finished chopping: ${countLogs(ctx)} logs in inventory`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Burn all logs
|
||||
*/
|
||||
async function burnLogs(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log('Burning logs...');
|
||||
|
||||
while (countLogs(ctx) > 0) {
|
||||
// Dismiss any dialogs
|
||||
if (await dismissDialogs(ctx, stats) > 0) continue;
|
||||
|
||||
const logsBefore = countLogs(ctx);
|
||||
const fmXpBefore = getFiremakingXp(ctx);
|
||||
|
||||
const result = await ctx.bot.burnLogs();
|
||||
// progress auto-tracked
|
||||
|
||||
if (result.success) {
|
||||
stats.logsBurned++;
|
||||
console.log(`Burned log! FM XP: +${result.xpGained}, Level: ${getFiremakingLevel(ctx)}`);
|
||||
} else {
|
||||
console.warn(`Failed to burn log: ${result.message}`);
|
||||
// If we failed, try moving to a different spot
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (pos && result.message.includes('location')) {
|
||||
console.log('Moving to find a better spot...');
|
||||
const offsetX = (Math.random() - 0.5) * 6;
|
||||
const offsetZ = (Math.random() - 0.5) * 6;
|
||||
await ctx.sdk.sendWalk(pos.x + offsetX, pos.z + offsetZ, true);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're already at target level
|
||||
if (getFiremakingLevel(ctx) >= 10) {
|
||||
console.log('Reached level 10!');
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log final statistics
|
||||
*/
|
||||
function logFinalStats(ctx: ScriptContext, stats: Stats) {
|
||||
const state = ctx.sdk.getState();
|
||||
const firemaking = state?.skills.find(s => s.name === 'Firemaking');
|
||||
const woodcutting = state?.skills.find(s => s.name === 'Woodcutting');
|
||||
|
||||
const fmXpGained = (firemaking?.experience ?? 0) - stats.startFiremakingXp;
|
||||
const wcXpGained = (woodcutting?.experience ?? 0) - stats.startWoodcuttingXp;
|
||||
const duration = (Date.now() - stats.startTime) / 1000;
|
||||
|
||||
console.log('');
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Duration: ${Math.round(duration)}s`);
|
||||
console.log(`--- Woodcutting ---`);
|
||||
console.log(` Level: ${woodcutting?.baseLevel ?? '?'}`);
|
||||
console.log(` XP Gained: ${wcXpGained}`);
|
||||
console.log(` Logs Cut: ${stats.logsCut}`);
|
||||
console.log(`--- Firemaking ---`);
|
||||
console.log(` Level: ${firemaking?.baseLevel ?? '?'}`);
|
||||
console.log(` XP Gained: ${fmXpGained}`);
|
||||
console.log(` Logs Burned: ${stats.logsBurned}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop: chop → burn → repeat until level 10
|
||||
*/
|
||||
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log('=== Firemaking Training Script ===');
|
||||
console.log(`Starting levels: Firemaking ${getFiremakingLevel(ctx)}, Woodcutting ${getWoodcuttingLevel(ctx)}`);
|
||||
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
|
||||
|
||||
// Verify we have the required items
|
||||
if (!hasTinderbox(ctx)) {
|
||||
throw new Error('No tinderbox in inventory!');
|
||||
}
|
||||
if (!hasAxe(ctx)) {
|
||||
throw new Error('No axe in inventory or equipped!');
|
||||
}
|
||||
console.log('Tinderbox and axe confirmed.');
|
||||
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
// progress auto-tracked
|
||||
|
||||
let cycle = 0;
|
||||
while (getFiremakingLevel(ctx) < 10) {
|
||||
cycle++;
|
||||
console.log(`\n--- Cycle ${cycle} ---`);
|
||||
console.log(`Current FM Level: ${getFiremakingLevel(ctx)}`);
|
||||
|
||||
// Phase 1: Chop trees to get logs
|
||||
await chopTrees(ctx, stats, 5);
|
||||
|
||||
// Phase 2: Burn all logs
|
||||
await burnLogs(ctx, stats);
|
||||
|
||||
// progress auto-tracked
|
||||
}
|
||||
|
||||
console.log('\n*** GOAL ACHIEVED: Firemaking Level 10+ ***');
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `FM${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
const stats: Stats = {
|
||||
logsCut: 0,
|
||||
logsBurned: 0,
|
||||
startFiremakingXp: getFiremakingXp(ctx),
|
||||
startWoodcuttingXp: getWoodcuttingXp(ctx),
|
||||
startTime: Date.now(),
|
||||
lastProgressTime: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
await mainLoop(ctx, stats);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`Script aborted: ${e.message}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
logFinalStats(ctx, stats);
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,689 +0,0 @@
|
||||
/**
|
||||
* Fishing + Cooking Speedrun Script (v4 - Range & Bank)
|
||||
*
|
||||
* Goal: Maximize combined Fishing+Cooking level in 10 minutes at Al-Kharid.
|
||||
*
|
||||
* Strategy:
|
||||
* - Fish at Al-Kharid fishing spots until inventory full
|
||||
* - Walk to Al-Kharid range and cook all fish
|
||||
* - Walk to Al-Kharid bank and deposit cooked fish
|
||||
* - Return to fishing spot and repeat
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc, NearbyLoc, InventoryItem } from '../../sdk/types';
|
||||
|
||||
// Al-Kharid locations
|
||||
const LOCATIONS = {
|
||||
FISHING_SPOT: { x: 3267, z: 3148 }, // Safe shrimp fishing
|
||||
RANGE: { x: 3273, z: 3180 }, // Al-Kharid range
|
||||
BANK: { x: 3269, z: 3167 }, // Al-Kharid bank
|
||||
};
|
||||
|
||||
interface Stats {
|
||||
fishCaught: number;
|
||||
fishCooked: number;
|
||||
fishBanked: number;
|
||||
cycles: number;
|
||||
startFishingXp: number;
|
||||
startCookingXp: number;
|
||||
startTime: number;
|
||||
lastProgressTime: number;
|
||||
}
|
||||
|
||||
// ============ Helper Functions ============
|
||||
|
||||
function getFishingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fishing')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function getFishingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fishing')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getCookingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function getCookingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getCombinedLevel(ctx: ScriptContext): number {
|
||||
return getFishingLevel(ctx) + getCookingLevel(ctx);
|
||||
}
|
||||
|
||||
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return null;
|
||||
return { x: state.player.worldX, z: state.player.worldZ };
|
||||
}
|
||||
|
||||
function distanceTo(ctx: ScriptContext, target: { x: number; z: number }): number {
|
||||
const pos = getPlayerPos(ctx);
|
||||
if (!pos) return 999;
|
||||
return Math.abs(pos.x - target.x) + Math.abs(pos.z - target.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest fishing spot (Net option)
|
||||
*/
|
||||
function findFishingSpot(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
// Find any fishing spot with "Net" option
|
||||
const spots = state.nearbyNpcs
|
||||
.filter(npc => /fishing\s*spot/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(opt => /^net$/i.test(opt)))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return spots[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any fishing spot nearby (for debugging)
|
||||
*/
|
||||
function findAnyFishingSpot(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
return state.nearbyNpcs
|
||||
.filter(npc => /fishing\s*spot/i.test(npc.name))
|
||||
.sort((a, b) => a.distance - b.distance)[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count raw fish in inventory
|
||||
*/
|
||||
function countRawFish(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
return state.inventory
|
||||
.filter(item => /^raw\s/i.test(item.name))
|
||||
.reduce((sum, item) => sum + item.count, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all raw fish items in inventory
|
||||
*/
|
||||
function getRawFishItems(ctx: ScriptContext): InventoryItem[] {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return [];
|
||||
|
||||
return state.inventory.filter(item => /^raw\s/i.test(item.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count cooked fish in inventory (shrimp/anchovies without "raw" prefix)
|
||||
*/
|
||||
function countCookedFish(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
return state.inventory
|
||||
.filter(item =>
|
||||
/shrimp|anchov/i.test(item.name) &&
|
||||
!/^raw\s/i.test(item.name) &&
|
||||
!/^burnt\s/i.test(item.name))
|
||||
.reduce((sum, item) => sum + item.count, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cooked fish items in inventory
|
||||
*/
|
||||
function getCookedFishItems(ctx: ScriptContext): InventoryItem[] {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return [];
|
||||
|
||||
return state.inventory.filter(item =>
|
||||
/shrimp|anchov/i.test(item.name) &&
|
||||
!/^raw\s/i.test(item.name) &&
|
||||
!/^burnt\s/i.test(item.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory count (used slots)
|
||||
*/
|
||||
function getInventoryCount(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.inventory.length ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of non-fishing-net items (to check for fish capacity)
|
||||
*/
|
||||
function getAvailableFishSlots(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
// Count items that aren't fishing net
|
||||
const nonNetItems = state.inventory.filter(i => !/fishing\s*net/i.test(i.name)).length;
|
||||
return 27 - nonNetItems; // 27 fish slots (28 total - 1 net)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss dialogs (max count to avoid loops)
|
||||
*/
|
||||
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
|
||||
let dismissed = 0;
|
||||
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
dismissed++;
|
||||
}
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a position and wait to arrive (handles long distances)
|
||||
*/
|
||||
async function walkToPosition(ctx: ScriptContext, stats: Stats, target: { x: number; z: number }, name: string): Promise<boolean> {
|
||||
const startDist = distanceTo(ctx, target);
|
||||
console.log(`Walking to ${name} (${target.x}, ${target.z}), dist: ${startDist}...`);
|
||||
|
||||
// For long distances, use bot.walkTo which handles pathfinding
|
||||
if (startDist > 30) {
|
||||
console.log('Long distance walk, using pathfinding...');
|
||||
const posBefore = getPlayerPos(ctx);
|
||||
try {
|
||||
await ctx.bot.walkTo(target.x, target.z);
|
||||
} catch (e) {
|
||||
console.warn(`Pathfinding error: ${e}`);
|
||||
}
|
||||
const posAfter = getPlayerPos(ctx);
|
||||
const finalDist = distanceTo(ctx, target);
|
||||
console.log(`Moved from (${posBefore?.x},${posBefore?.z}) to (${posAfter?.x},${posAfter?.z}), dist: ${finalDist}`);
|
||||
|
||||
if (finalDist <= 10) {
|
||||
console.log(`Arrived at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we didn't move at all, try dismissing dialogs and waiting
|
||||
if (posBefore?.x === posAfter?.x && posBefore?.z === posAfter?.z) {
|
||||
console.log('Position unchanged - checking for blocking UI...');
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// Direct walking for short distances
|
||||
await ctx.sdk.sendWalk(target.x, target.z, true);
|
||||
|
||||
// Wait to arrive (within 5 tiles)
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
|
||||
// Dismiss any dialogs
|
||||
await dismissDialogs(ctx, stats, 1);
|
||||
|
||||
const dist = distanceTo(ctx, target);
|
||||
if (dist <= 5) {
|
||||
console.log(`Arrived at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Re-send walk periodically
|
||||
if (i > 0 && i % 5 === 0) {
|
||||
await ctx.sdk.sendWalk(target.x, target.z, true);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`Failed to reach ${name} (dist: ${distanceTo(ctx, target)})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============ Main Phases ============
|
||||
|
||||
/**
|
||||
* Phase 1: Fish until inventory is full
|
||||
*/
|
||||
async function fishUntilFull(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log('Phase 1: Fishing until inventory full...');
|
||||
let lastFishCount = countRawFish(ctx);
|
||||
let attempts = 0;
|
||||
let noSpotCount = 0;
|
||||
|
||||
// Fish until no available slots for raw fish
|
||||
while (getAvailableFishSlots(ctx) > 0) {
|
||||
attempts++;
|
||||
|
||||
// Dismiss dialogs
|
||||
if (await dismissDialogs(ctx, stats) > 0) continue;
|
||||
|
||||
// Check for new fish
|
||||
const currentFish = countRawFish(ctx);
|
||||
if (currentFish > lastFishCount) {
|
||||
stats.fishCaught += currentFish - lastFishCount;
|
||||
if (currentFish % 5 === 0 || currentFish - lastFishCount > 1) {
|
||||
console.log(`Fish caught: ${stats.fishCaught} (slots: ${getAvailableFishSlots(ctx)} free)`);
|
||||
}
|
||||
noSpotCount = 0; // Reset counter when we catch fish
|
||||
}
|
||||
lastFishCount = currentFish;
|
||||
|
||||
// Find and use fishing spot
|
||||
const spot = findFishingSpot(ctx);
|
||||
if (!spot) {
|
||||
noSpotCount++;
|
||||
if (noSpotCount % 50 === 0) console.log('Waiting for fishing spot...');
|
||||
|
||||
// If no spot found for too long, try to find any spot and walk to it
|
||||
if (noSpotCount > 100) { // ~10 seconds
|
||||
const playerPos = getPlayerPos(ctx);
|
||||
const distToSpot = distanceTo(ctx, LOCATIONS.FISHING_SPOT);
|
||||
console.log(`Position: (${playerPos?.x}, ${playerPos?.z}), dist to fishing: ${distToSpot}`);
|
||||
|
||||
// If we're far from the fishing area (e.g. teleported to Lumbridge), walk back
|
||||
if (distToSpot > 20) {
|
||||
console.log(`Too far from fishing spot, walking back...`);
|
||||
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
|
||||
} else {
|
||||
// We're close but no spot - check if there's any fishing spot
|
||||
const anySpot = findAnyFishingSpot(ctx);
|
||||
if (anySpot) {
|
||||
console.log(`Found fishing spot at (${anySpot.x}, ${anySpot.z}), options: ${anySpot.options.join(', ')}`);
|
||||
await ctx.sdk.sendWalk(anySpot.x, anySpot.z, true);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
} else {
|
||||
const nearbyNpcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 5) ?? [];
|
||||
console.log(`No spot visible. NPCs: ${nearbyNpcs.map(n => n.name).join(', ') || 'none'}`);
|
||||
// Walk around a bit to find the spot
|
||||
await ctx.sdk.sendWalk(LOCATIONS.FISHING_SPOT.x + (Math.random() > 0.5 ? 3 : -3), LOCATIONS.FISHING_SPOT.z, true);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
noSpotCount = 0;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
continue;
|
||||
}
|
||||
|
||||
noSpotCount = 0;
|
||||
const netOpt = spot.optionsWithIndex.find(o => /^net$/i.test(o.text));
|
||||
if (!netOpt) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
console.log(`Inventory full: ${countRawFish(ctx)} raw fish`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Cook all fish at the range
|
||||
*/
|
||||
async function cookAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
// Walk to range
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.RANGE, 'range')) {
|
||||
console.warn('Could not reach range');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Phase 2: Cooking fish at range...');
|
||||
|
||||
// Find the range
|
||||
const range = ctx.sdk.getState()?.nearbyLocs.find(loc => /range|stove/i.test(loc.name));
|
||||
if (!range) {
|
||||
console.warn('No range found nearby');
|
||||
const locs = ctx.sdk.getState()?.nearbyLocs.slice(0, 10) ?? [];
|
||||
console.log(`Nearby locs: ${locs.map(l => l.name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found: ${range.name} at (${range.x}, ${range.z})`);
|
||||
|
||||
const cookingXpBefore = getCookingXp(ctx);
|
||||
const rawFishBefore = countRawFish(ctx);
|
||||
|
||||
// Cook each raw fish one at a time (game may not have batch cooking interface)
|
||||
while (countRawFish(ctx) > 0) {
|
||||
const rawFish = getRawFishItems(ctx)[0];
|
||||
if (!rawFish) break;
|
||||
|
||||
// Use fish on range
|
||||
await ctx.sdk.sendUseItemOnLoc(rawFish.slot, range.x, range.z, range.id);
|
||||
|
||||
// Wait for cooking to happen (XP gain or raw fish count decrease)
|
||||
// Also handle any interface/dialog that appears
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
const state = ctx.sdk.getState();
|
||||
|
||||
// Handle cooking interface if it appears
|
||||
const firstInterfaceOpt = state?.interface?.options[0];
|
||||
if (state?.interface?.isOpen && firstInterfaceOpt) {
|
||||
console.log('Clicking cook option...');
|
||||
await ctx.sdk.sendClickInterfaceOption(0);
|
||||
// After clicking, wait for batch cooking to complete
|
||||
let noChange = 0;
|
||||
while (noChange < 10 && countRawFish(ctx) > 0) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
await dismissDialogs(ctx, stats, 1);
|
||||
const prev = countRawFish(ctx);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
if (countRawFish(ctx) === prev) noChange++;
|
||||
else noChange = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle dialog
|
||||
if (state?.dialog?.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
|
||||
// Check if this fish was cooked (raw count decreased)
|
||||
if (countRawFish(ctx) < rawFishBefore - stats.fishCooked) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate results
|
||||
const xpGained = getCookingXp(ctx) - cookingXpBefore;
|
||||
const fishCooked = rawFishBefore - countRawFish(ctx);
|
||||
stats.fishCooked += Math.max(0, fishCooked);
|
||||
|
||||
console.log(`Cooked ${fishCooked} fish (XP: +${xpGained})`);
|
||||
console.log(`Done cooking. Cooked fish in inventory: ${countCookedFish(ctx)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3: Clear cooked fish (bank or drop)
|
||||
*/
|
||||
async function clearCookedFish(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const cookedBefore = countCookedFish(ctx);
|
||||
if (cookedBefore === 0) {
|
||||
console.log('No cooked fish to clear');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Phase 3: Clearing ${cookedBefore} cooked fish...`);
|
||||
|
||||
// Try banking first
|
||||
const bankSuccess = await tryBanking(ctx, stats, cookedBefore);
|
||||
|
||||
if (!bankSuccess) {
|
||||
// Banking failed - drop cooked fish instead
|
||||
console.log('Banking failed, dropping cooked fish...');
|
||||
await dropCookedFish(ctx, stats);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to bank cooked fish
|
||||
*/
|
||||
async function tryBanking(ctx: ScriptContext, stats: Stats, cookedBefore: number): Promise<boolean> {
|
||||
// Walk to bank
|
||||
if (!await walkToPosition(ctx, stats, LOCATIONS.BANK, 'bank')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug: log what we see at the bank
|
||||
const state = ctx.sdk.getState();
|
||||
const bankBooths = state?.nearbyLocs.filter(l => /bank/i.test(l.name)) ?? [];
|
||||
const bankers = state?.nearbyNpcs.filter(n => /banker/i.test(n.name)) ?? [];
|
||||
console.log(`At bank: ${bankBooths.length} booths, ${bankers.length} bankers`);
|
||||
|
||||
// Try banker NPC first (more reliable)
|
||||
const banker = bankers.find(n => n.optionsWithIndex.some(o => /bank/i.test(o.text)));
|
||||
if (banker) {
|
||||
const bankOpt = banker.optionsWithIndex.find(o => /^bank$/i.test(o.text));
|
||||
if (bankOpt) {
|
||||
console.log(`Using banker: ${banker.name}, option ${bankOpt.opIndex}: ${bankOpt.text}`);
|
||||
await ctx.sdk.sendInteractNpc(banker.index, bankOpt.opIndex);
|
||||
|
||||
// Wait for interface or dialog
|
||||
const opened = await waitForBankInterface(ctx, stats);
|
||||
if (opened) {
|
||||
return await depositCookedFish(ctx, stats, cookedBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try bank booth with different options
|
||||
const bankBooth = bankBooths[0];
|
||||
if (bankBooth) {
|
||||
console.log(` Booth: ${bankBooth.name} at (${bankBooth.x}, ${bankBooth.z}), options: ${bankBooth.optionsWithIndex.map(o => `${o.opIndex}:${o.text}`).join(', ')}`);
|
||||
|
||||
// Try "Bank" option first, then "Use-quickly", then "Use"
|
||||
const options = [
|
||||
bankBooth.optionsWithIndex.find(o => /^bank$/i.test(o.text)),
|
||||
bankBooth.optionsWithIndex.find(o => /use-quickly/i.test(o.text)),
|
||||
bankBooth.optionsWithIndex.find(o => /^use$/i.test(o.text)),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const opt of options) {
|
||||
if (!opt) continue;
|
||||
console.log(`Trying bank booth option ${opt.opIndex}: ${opt.text}`);
|
||||
await ctx.sdk.sendInteractLoc(bankBooth.x, bankBooth.z, bankBooth.id, opt.opIndex);
|
||||
|
||||
const opened = await waitForBankInterface(ctx, stats);
|
||||
if (opened) {
|
||||
return await depositCookedFish(ctx, stats, cookedBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('Bank interface did not open');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for bank interface to open (handling dialogs)
|
||||
*/
|
||||
async function waitForBankInterface(ctx: ScriptContext, stats: Stats): Promise<boolean> {
|
||||
// Wait for interface OR dialog to appear
|
||||
for (let i = 0; i < 40; i++) { // 8 seconds total
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const currentState = ctx.sdk.getState();
|
||||
|
||||
// Bank interface opened!
|
||||
if (currentState?.interface?.isOpen) {
|
||||
console.log(`Bank interface opened! (id: ${currentState.interface.interfaceId})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Click through dialogs
|
||||
if (currentState?.dialog?.isOpen) {
|
||||
const text = currentState.dialog.text || '';
|
||||
const options = currentState.dialog.options;
|
||||
console.log(`Dialog: "${text.substring(0, 50)}..." options: ${options.map(o => o.text).join(', ')}`);
|
||||
|
||||
// Look for bank-related option or just click first option
|
||||
const bankOption = options.find(o => /bank/i.test(o.text));
|
||||
const firstOption = options[0];
|
||||
if (bankOption) {
|
||||
await ctx.sdk.sendClickDialog(bankOption.index);
|
||||
} else if (firstOption) {
|
||||
await ctx.sdk.sendClickDialog(firstOption.index);
|
||||
} else {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit cooked fish when bank interface is open
|
||||
*/
|
||||
async function depositCookedFish(ctx: ScriptContext, stats: Stats, cookedBefore: number): Promise<boolean> {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState?.interface?.isOpen) {
|
||||
console.warn('Bank interface not open');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deposit all cooked and burnt fish
|
||||
const itemsToDeposit = currentState.inventory.filter(item =>
|
||||
(/shrimp|anchov/i.test(item.name) && !/^raw\s/i.test(item.name)) ||
|
||||
/^burnt\s/i.test(item.name));
|
||||
|
||||
console.log(`Depositing ${itemsToDeposit.length} items...`);
|
||||
for (const item of itemsToDeposit) {
|
||||
console.log(` ${item.name} x${item.count} from slot ${item.slot}`);
|
||||
await ctx.sdk.sendBankDeposit(item.slot, item.count);
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
const cookedAfter = countCookedFish(ctx);
|
||||
const deposited = cookedBefore - cookedAfter;
|
||||
stats.fishBanked += Math.max(0, deposited);
|
||||
|
||||
// Bank interface closes automatically when we walk away
|
||||
console.log(`Banked ${deposited} fish (total: ${stats.fishBanked})`);
|
||||
return deposited > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop all cooked and burnt fish
|
||||
*/
|
||||
async function dropCookedFish(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
const itemsToDrop = state.inventory.filter(item =>
|
||||
(/shrimp|anchov/i.test(item.name) && !/^raw\s/i.test(item.name)) ||
|
||||
/^burnt\s/i.test(item.name));
|
||||
|
||||
let dropped = 0;
|
||||
for (const item of itemsToDrop) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
dropped += item.count;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
console.log(`Dropped ${dropped} cooked/burnt fish`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log final statistics
|
||||
*/
|
||||
function logFinalStats(ctx: ScriptContext, stats: Stats) {
|
||||
const state = ctx.sdk.getState();
|
||||
const fishing = state?.skills.find(s => s.name === 'Fishing');
|
||||
const cooking = state?.skills.find(s => s.name === 'Cooking');
|
||||
|
||||
const fishingXpGained = (fishing?.experience ?? 0) - stats.startFishingXp;
|
||||
const cookingXpGained = (cooking?.experience ?? 0) - stats.startCookingXp;
|
||||
const duration = (Date.now() - stats.startTime) / 1000;
|
||||
|
||||
console.log('');
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Duration: ${Math.round(duration)}s`);
|
||||
console.log(`Cycles: ${stats.cycles}`);
|
||||
console.log(`--- Fishing ---`);
|
||||
console.log(` Level: ${fishing?.baseLevel ?? '?'}`);
|
||||
console.log(` XP Gained: ${fishingXpGained}`);
|
||||
console.log(` Fish Caught: ${stats.fishCaught}`);
|
||||
console.log(`--- Cooking ---`);
|
||||
console.log(` Level: ${cooking?.baseLevel ?? '?'}`);
|
||||
console.log(` XP Gained: ${cookingXpGained}`);
|
||||
console.log(` Fish Cooked: ${stats.fishCooked}`);
|
||||
console.log(` Fish Banked: ${stats.fishBanked}`);
|
||||
console.log(`--- Combined ---`);
|
||||
console.log(` COMBINED LEVEL: ${(fishing?.baseLevel ?? 1) + (cooking?.baseLevel ?? 1)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we're at the fishing spot
|
||||
*/
|
||||
async function ensureAtFishingSpot(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
const pos = getPlayerPos(ctx);
|
||||
const dist = distanceTo(ctx, LOCATIONS.FISHING_SPOT);
|
||||
console.log(`Current position: (${pos?.x}, ${pos?.z}), distance to fishing spot: ${dist}`);
|
||||
|
||||
if (dist > 10) {
|
||||
console.log('Not at fishing spot, walking there...');
|
||||
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop: fish → cook at range → bank → repeat
|
||||
*/
|
||||
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
|
||||
console.log('=== Fishing + Cooking Speedrun (v4 - Range & Bank) ===');
|
||||
console.log(`Starting levels: Fishing ${getFishingLevel(ctx)}, Cooking ${getCookingLevel(ctx)}`);
|
||||
console.log(`Combined: ${getCombinedLevel(ctx)}`);
|
||||
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
|
||||
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Ensure we start at the fishing spot
|
||||
await ensureAtFishingSpot(ctx, stats);
|
||||
|
||||
while (true) {
|
||||
stats.cycles++;
|
||||
console.log(`\n--- Cycle ${stats.cycles} ---`);
|
||||
console.log(`Current levels: Fishing ${getFishingLevel(ctx)}, Cooking ${getCookingLevel(ctx)} = ${getCombinedLevel(ctx)}`);
|
||||
|
||||
// Phase 1: Fish until inventory full
|
||||
await fishUntilFull(ctx, stats);
|
||||
|
||||
// Phase 2: Walk to range and cook
|
||||
await cookAllFish(ctx, stats);
|
||||
|
||||
// Phase 3: Bank or drop cooked fish
|
||||
await clearCookedFish(ctx, stats);
|
||||
|
||||
// Phase 4: Walk back to fishing spot
|
||||
console.log('Phase 4: Returning to fishing spot...');
|
||||
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `FS${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.FISHER_COOK_AT_DRAYNOR);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
const stats: Stats = {
|
||||
fishCaught: 0,
|
||||
fishCooked: 0,
|
||||
fishBanked: 0,
|
||||
cycles: 0,
|
||||
startFishingXp: getFishingXp(ctx),
|
||||
startCookingXp: getCookingXp(ctx),
|
||||
startTime: Date.now(),
|
||||
lastProgressTime: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
await mainLoop(ctx, stats);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`Script aborted: ${e.message}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
logFinalStats(ctx, stats);
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,459 +0,0 @@
|
||||
/**
|
||||
* Fletching GP Maximizer
|
||||
*
|
||||
* Strategy: Normal logs until level 20, then oaks
|
||||
* - Chop normal trees -> fletch longbows -> sell
|
||||
* - At level 20+, switch to oak trees -> oak shortbows/longbows
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets, Items, Locations } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Locations
|
||||
const NORMAL_TREES_AREA = { x: 3200, z: 3230 };
|
||||
const OAK_TREES_AREA = { x: 3203, z: 3243 };
|
||||
const GENERAL_STORE = Locations.LUMBRIDGE_SHOP;
|
||||
|
||||
const LOGS_BEFORE_FLETCH = 6;
|
||||
const MIN_LOGS_TO_FLETCH = 4;
|
||||
|
||||
type FletchProduct = 'arrow shafts' | 'short bow' | 'long bow' | 'oak short bow' | 'oak long bow';
|
||||
|
||||
function getBestProduct(fletchingLevel: number, logType: 'normal' | 'oak'): FletchProduct | null {
|
||||
if (logType === 'oak') {
|
||||
if (fletchingLevel >= 25) return 'oak long bow';
|
||||
if (fletchingLevel >= 20) return 'oak short bow';
|
||||
return null; // Can't fletch oaks below 20
|
||||
}
|
||||
if (fletchingLevel >= 10) return 'long bow';
|
||||
if (fletchingLevel >= 5) return 'short bow';
|
||||
return 'arrow shafts';
|
||||
}
|
||||
|
||||
// Track GP earned
|
||||
interface GPTracker {
|
||||
startingGP: number;
|
||||
currentGP: number;
|
||||
itemsSold: number;
|
||||
logsChopped: number;
|
||||
productsFletched: number;
|
||||
sellCycles: number;
|
||||
gpPerCycle: number[]; // Track GP earned each sell cycle
|
||||
}
|
||||
|
||||
function getFletchingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getWoodcuttingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getGP(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.findInventoryItem(/coins/i);
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
function countLogs(ctx: ScriptContext): { normal: number; oak: number; total: number } {
|
||||
// Logs don't stack - count all log items in inventory
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
const normal = inv.filter(i => /^logs$/i.test(i.name)).length;
|
||||
const oak = inv.filter(i => /^oak logs$/i.test(i.name)).length;
|
||||
return { normal, oak, total: normal + oak };
|
||||
}
|
||||
|
||||
function countSellableItems(ctx: ScriptContext): number {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
// Count arrow shafts (15 per log) and unstrung bows (1 per log)
|
||||
// Include oak shortbow and oak longbow
|
||||
return inv.filter(i =>
|
||||
/arrow shaft|shortbow|longbow/i.test(i.name)
|
||||
).reduce((sum, i) => sum + i.count, 0);
|
||||
}
|
||||
|
||||
function getInventoryFreeSlots(ctx: ScriptContext): number {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
return 28 - inv.length;
|
||||
}
|
||||
|
||||
function logStats(ctx: ScriptContext, tracker: GPTracker, label: string): void {
|
||||
const level = getFletchingLevel(ctx);
|
||||
const gpEarned = tracker.currentGP - tracker.startingGP;
|
||||
const cyclesStr = tracker.gpPerCycle.length > 0
|
||||
? ` | Cycles: [${tracker.gpPerCycle.join(', ')}]`
|
||||
: '';
|
||||
console.log(`[${label}] GP: ${gpEarned} | Fletch: ${level} | Logs: ${tracker.logsChopped} | Fletched: ${tracker.productsFletched} | Sold: ${tracker.itemsSold}${cyclesStr}`);
|
||||
}
|
||||
|
||||
async function fletchingGP(ctx: ScriptContext) {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
// Initialize tracking
|
||||
const tracker: GPTracker = {
|
||||
startingGP: getGP(ctx),
|
||||
currentGP: getGP(ctx),
|
||||
itemsSold: 0,
|
||||
logsChopped: 0,
|
||||
productsFletched: 0,
|
||||
sellCycles: 0,
|
||||
gpPerCycle: [],
|
||||
};
|
||||
|
||||
logStats(ctx, tracker, 'START');
|
||||
|
||||
// First: Find and pick up a knife from the ground
|
||||
const hasKnife = () => !!sdk.findInventoryItem(/knife/i);
|
||||
if (!hasKnife()) {
|
||||
console.log('Looking for knife on ground...');
|
||||
|
||||
// Try to find and pickup knife
|
||||
for (let attempt = 0; attempt < 5 && !hasKnife(); attempt++) {
|
||||
const knife = sdk.findGroundItem(/knife/i);
|
||||
if (knife) {
|
||||
console.log(`Found knife at (${knife.x}, ${knife.z}), picking up...`);
|
||||
const result = await bot.pickupItem(knife);
|
||||
if (result.success) {
|
||||
console.log('Got knife!');
|
||||
break;
|
||||
} else {
|
||||
console.log(`Pickup failed: ${result.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`No knife visible (attempt ${attempt + 1})`);
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
if (!hasKnife()) {
|
||||
console.log('WARNING: No knife found - will try to continue anyway');
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop: Chop -> Fletch -> Sell
|
||||
while (true) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) {
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Eat if HP is low (survive random events)
|
||||
const hpSkill = sdk.getSkill('Hitpoints');
|
||||
const currentHp = hpSkill?.level ?? 10;
|
||||
const maxHp = hpSkill?.baseLevel ?? 10;
|
||||
if (currentHp < maxHp - 3) {
|
||||
const food = sdk.findInventoryItem(/bread|shrimp|fish/i);
|
||||
if (food) {
|
||||
console.log(`HP low (${currentHp}/${maxHp}), eating ${food.name}...`);
|
||||
await bot.eatFood(food);
|
||||
}
|
||||
}
|
||||
|
||||
// Close shop first if open (before checking dialogs)
|
||||
if (state.shop.isOpen) {
|
||||
// First dismiss any dialogs that might be blocking
|
||||
if (state.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(300);
|
||||
}
|
||||
// Try closing shop up to 5 times, then walk away to force close
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
console.log(`Closing shop (attempt ${attempt + 1})...`);
|
||||
await sdk.sendCloseShop();
|
||||
await sleep(600);
|
||||
if (!ctx.sdk.getState()?.shop.isOpen) {
|
||||
console.log('Shop closed successfully');
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If STILL open after 5 attempts, walk away to force close
|
||||
if (ctx.sdk.getState()?.shop.isOpen) {
|
||||
console.log('Shop stuck, walking away to force close...');
|
||||
await bot.walkTo(NORMAL_TREES_AREA.x, NORMAL_TREES_AREA.z);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dismiss level-up dialogs (NOT shop interface)
|
||||
if (state.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(250);
|
||||
continue;
|
||||
}
|
||||
|
||||
const freeSlots = getInventoryFreeSlots(ctx);
|
||||
const logs = countLogs(ctx);
|
||||
const sellableCount = countSellableItems(ctx);
|
||||
const fletchLevel = getFletchingLevel(ctx);
|
||||
|
||||
// Log state every time we have logs (for debugging)
|
||||
if (logs.total >= MIN_LOGS_TO_FLETCH) {
|
||||
console.log(`Decision: ${logs.oak} oak + ${logs.normal} normal logs, ${sellableCount} sellable, ${freeSlots} free slots`);
|
||||
}
|
||||
|
||||
// Decision tree:
|
||||
// 1. If we have sellable items and inventory is getting full, go sell
|
||||
// 2. If we have enough logs, fletch them
|
||||
// 3. Otherwise, chop more trees
|
||||
|
||||
if (sellableCount > 0 && (freeSlots < 8 || sellableCount >= 6)) {
|
||||
// === SELLING PHASE ===
|
||||
console.log(`Selling ${sellableCount} items...`);
|
||||
|
||||
// Walk to general store
|
||||
await bot.walkTo(GENERAL_STORE.x, GENERAL_STORE.z);
|
||||
|
||||
// Open shop
|
||||
const shopResult = await bot.openShop(/shop.*keeper|general/i);
|
||||
if (!shopResult.success) {
|
||||
console.log(`Failed to open shop: ${shopResult.message}`);
|
||||
await sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sell all fletched products - oak longbows first (most valuable)
|
||||
const sellPatterns = [/oak longbow/i, /oak shortbow/i, /longbow/i, /shortbow/i, /arrow shaft/i];
|
||||
const gpBefore = getGP(ctx);
|
||||
let totalItemsSold = 0;
|
||||
|
||||
for (const pattern of sellPatterns) {
|
||||
let attempts = 0;
|
||||
while (attempts < 20) { // Max 20 attempts per pattern
|
||||
attempts++;
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState?.shop.isOpen) break;
|
||||
|
||||
const item = currentState.shop.playerItems.find(i => pattern.test(i.name));
|
||||
if (!item || item.count === 0) break;
|
||||
|
||||
const gpBeforeSell = getGP(ctx);
|
||||
const sellResult = await bot.sellToShop(item, 'all');
|
||||
|
||||
// Check if GP increased (actual success) even if sellResult reports failure
|
||||
const gpAfterSell = getGP(ctx);
|
||||
if (gpAfterSell > gpBeforeSell || sellResult.success) {
|
||||
const gpGained = gpAfterSell - gpBeforeSell;
|
||||
const sold = sellResult.amountSold ?? 1;
|
||||
totalItemsSold += sold;
|
||||
tracker.itemsSold += sold;
|
||||
console.log(`Sold ${item.name} (+${gpGained} GP)`);
|
||||
} else if (sellResult.rejected) {
|
||||
console.log(`Shop won't buy ${item.name}`);
|
||||
break;
|
||||
} else {
|
||||
// No GP gain and not successful, move to next pattern
|
||||
break;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
const gpAfter = getGP(ctx);
|
||||
const cycleGP = gpAfter - gpBefore;
|
||||
tracker.sellCycles++;
|
||||
tracker.gpPerCycle.push(cycleGP);
|
||||
console.log(`Sell cycle #${tracker.sellCycles} complete: +${cycleGP} GP, ${totalItemsSold} items`);
|
||||
|
||||
// Log saturation warning if GP per cycle is dropping significantly
|
||||
if (tracker.gpPerCycle.length >= 2) {
|
||||
const lastCycleGP = tracker.gpPerCycle[tracker.gpPerCycle.length - 2];
|
||||
if (lastCycleGP !== undefined && cycleGP < lastCycleGP * 0.6) {
|
||||
console.log(`WARNING: Shop saturation detected: GP dropped from ${lastCycleGP} to ${cycleGP}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss any level-up dialogs that appeared during selling
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (currentState?.dialog.isOpen) {
|
||||
console.log('Dismissing dialog before closing shop...');
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(300);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Close shop before checking GP
|
||||
await bot.closeShop();
|
||||
await sleep(500);
|
||||
|
||||
// If shop still open, force close
|
||||
if (ctx.sdk.getState()?.shop.isOpen) {
|
||||
console.log('Shop still open after closeShop, forcing close...');
|
||||
await sdk.sendCloseShop();
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// Update GP tracking
|
||||
tracker.currentGP = getGP(ctx);
|
||||
const gpEarned = tracker.currentGP - tracker.startingGP;
|
||||
console.log(`Total GP earned so far: ${gpEarned}`);
|
||||
logStats(ctx, tracker, 'AFTER SELL');
|
||||
|
||||
} else if (logs.total >= MIN_LOGS_TO_FLETCH && (logs.total >= LOGS_BEFORE_FLETCH || freeSlots <= 3)) {
|
||||
// === FLETCHING PHASE ===
|
||||
// First dismiss any dialogs
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(300);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use oak logs only if we can fletch them (level 20+)
|
||||
const useOakLogs = fletchLevel >= 20 && logs.oak > 0;
|
||||
const logType = useOakLogs ? 'oak' : 'normal';
|
||||
|
||||
// Skip if we only have oak logs but can't fletch them
|
||||
if (logs.normal === 0 && !useOakLogs) {
|
||||
console.log(`Can't fletch oak logs at level ${fletchLevel}, need 20+`);
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
const product = getBestProduct(fletchLevel, logType)!;
|
||||
console.log(`Fletching ${logType} logs into ${product} (level ${fletchLevel})...`);
|
||||
|
||||
// Fletch all logs with timeout protection
|
||||
let fletchedThisBatch = 0;
|
||||
let consecutiveFailures = 0;
|
||||
const fletchStart = Date.now();
|
||||
const maxFletchTime = 60_000; // 60 seconds max for fletching
|
||||
|
||||
while (countLogs(ctx).total > 0 && Date.now() - fletchStart < maxFletchTime) {
|
||||
// Dismiss any dialogs first
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentLogs = countLogs(ctx);
|
||||
const currentLevel = getFletchingLevel(ctx);
|
||||
const currentUseOak = currentLevel >= 20 && currentLogs.oak > 0;
|
||||
const currentLogType = currentUseOak ? 'oak' : 'normal';
|
||||
|
||||
// Stop if we only have oaks but can't fletch them
|
||||
if (currentLogs.normal === 0 && !currentUseOak) break;
|
||||
|
||||
const currentProduct = getBestProduct(currentLevel, currentLogType)!;
|
||||
const fletchResult = await bot.fletchLogs(currentProduct);
|
||||
if (fletchResult.success) {
|
||||
fletchedThisBatch++;
|
||||
tracker.productsFletched++;
|
||||
consecutiveFailures = 0;
|
||||
|
||||
// Check if we leveled up
|
||||
const newLevel = getFletchingLevel(ctx);
|
||||
if (newLevel !== fletchLevel) {
|
||||
console.log(`Fletching level up! Now level ${newLevel}`);
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures++;
|
||||
console.log(`Fletch failed (${consecutiveFailures}/3): ${fletchResult.message}`);
|
||||
if (consecutiveFailures >= 3) {
|
||||
console.log(`Too many fletch failures, moving on...`);
|
||||
break;
|
||||
}
|
||||
// Wait a bit before retry
|
||||
await sleep(500);
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
if (fletchedThisBatch > 0) {
|
||||
console.log(`Fletched ${fletchedThisBatch} items`);
|
||||
}
|
||||
|
||||
} else {
|
||||
// === CHOPPING PHASE ===
|
||||
// Normal trees until level 20, then oaks
|
||||
const useOaks = fletchLevel >= 20;
|
||||
const tree = useOaks
|
||||
? sdk.findNearbyLoc(/^oak$/i) ?? sdk.findNearbyLoc(/^tree$/i)
|
||||
: sdk.findNearbyLoc(/^tree$/i);
|
||||
|
||||
if (!tree) {
|
||||
const targetArea = useOaks ? OAK_TREES_AREA : NORMAL_TREES_AREA;
|
||||
console.log(`No trees nearby, walking to ${useOaks ? 'oak' : 'normal'} trees...`);
|
||||
await bot.walkTo(targetArea.x, targetArea.z);
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chop the tree
|
||||
const chopResult = await bot.chopTree(tree);
|
||||
if (chopResult.success) {
|
||||
tracker.logsChopped++;
|
||||
|
||||
// Log progress occasionally
|
||||
if (tracker.logsChopped % 10 === 0) {
|
||||
logStats(ctx, tracker, `CHOP #${tracker.logsChopped}`);
|
||||
}
|
||||
} else {
|
||||
// Tree might be gone, wait and retry
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `fg${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, {
|
||||
position: { x: 3224, z: 3205 }, // Near knife spawn SE of castle
|
||||
inventory: [
|
||||
{ id: Items.BRONZE_AXE, count: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
|
||||
console.log('=== Fletching GP Maximizer ===');
|
||||
console.log('Goal: Maximize GP from fletching and selling longbows in 10 minutes');
|
||||
|
||||
// Wait for state to initialize
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) {
|
||||
console.error('No player state');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
|
||||
|
||||
// Dismiss any startup dialogs
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Run the fletching loop
|
||||
await fletchingGP(ctx);
|
||||
|
||||
console.log('=== Script Complete ===');
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
// Helper
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,332 +0,0 @@
|
||||
/**
|
||||
* Fletching Trainer Script
|
||||
*
|
||||
* Goal: Train Fletching from level 1 to 10+ starting from Lumbridge spawn.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Pick up knife from ground spawn near Lumbridge castle (SE of castle)
|
||||
* 2. Chop trees for logs using bronze axe (included in preset)
|
||||
* 3. Use knife on logs to make arrow shafts (level 1-4)
|
||||
* 4. At level 5+, can make shortbows for more XP per log
|
||||
* 5. Drop arrow shafts/bows when inventory fills
|
||||
*
|
||||
* XP per action:
|
||||
* - Arrow shafts: 5 XP (15 shafts per log)
|
||||
* - Shortbow: 5 XP (level 5 required)
|
||||
* - Longbow: 10 XP (level 10 required)
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Locations
|
||||
const KNIFE_SPAWN = { x: 3224, z: 3202 }; // SE of Lumbridge castle (actual spawn location)
|
||||
const LUMBRIDGE_TREES = { x: 3200, z: 3230 }; // Trees west of Lumbridge castle
|
||||
|
||||
// Configuration
|
||||
const TARGET_LEVEL = 10;
|
||||
|
||||
function getFletchingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getFletchingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function countLogs(ctx: ScriptContext): number {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
return inv.filter(i => /^logs$/i.test(i.name)).length;
|
||||
}
|
||||
|
||||
function hasKnife(ctx: ScriptContext): boolean {
|
||||
return !!ctx.sdk.findInventoryItem(/knife/i);
|
||||
}
|
||||
|
||||
function hasAxe(ctx: ScriptContext): boolean {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
const equip = ctx.sdk.getState()?.equipment ?? [];
|
||||
return inv.some(i => /axe/i.test(i.name)) || equip.some(i => /axe/i.test(i.name));
|
||||
}
|
||||
|
||||
function getInventoryFreeSlots(ctx: ScriptContext): number {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
return 28 - inv.length;
|
||||
}
|
||||
|
||||
async function dropFletchedItems(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
// Drop arrow shafts and bows (keep knife, axe, logs)
|
||||
const itemsToDrop = state.inventory.filter(item =>
|
||||
/arrow shaft|bow/i.test(item.name) && !/shortbow/i.test(item.name) === false
|
||||
);
|
||||
|
||||
// Also drop unstrung bows and arrow shafts
|
||||
const allDroppable = state.inventory.filter(item =>
|
||||
/arrow shaft|shortbow|longbow/i.test(item.name)
|
||||
);
|
||||
|
||||
if (allDroppable.length === 0) return;
|
||||
|
||||
console.log(`Dropping ${allDroppable.length} fletched items...`);
|
||||
|
||||
for (const item of allDroppable) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
await sleep(150);
|
||||
}
|
||||
}
|
||||
|
||||
async function acquireKnife(ctx: ScriptContext): Promise<boolean> {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
// First check if knife is already visible from starting position
|
||||
let knife = sdk.findGroundItem(/knife/i);
|
||||
if (knife) {
|
||||
console.log(`Found knife at (${knife.x}, ${knife.z}) from starting position!`);
|
||||
const result = await bot.pickupItem(knife);
|
||||
if (result.success) {
|
||||
console.log('Got knife!');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk to knife spawn area (SE of Lumbridge castle)
|
||||
console.log(`Walking to knife spawn at (${KNIFE_SPAWN.x}, ${KNIFE_SPAWN.z})...`);
|
||||
const walkResult = await bot.walkTo(KNIFE_SPAWN.x, KNIFE_SPAWN.z);
|
||||
console.log(`Walk result: ${walkResult.message}`);
|
||||
|
||||
// Log current position for debugging
|
||||
const pos = ctx.sdk.getState()?.player;
|
||||
console.log(`Current position after walk: (${pos?.worldX}, ${pos?.worldZ})`);
|
||||
|
||||
// Try to pick up knife from ground - wait for respawn if needed
|
||||
for (let attempt = 0; attempt < 15 && !hasKnife(ctx); attempt++) {
|
||||
knife = sdk.findGroundItem(/knife/i);
|
||||
if (knife) {
|
||||
console.log(`Found knife at (${knife.x}, ${knife.z}), picking up...`);
|
||||
const result = await bot.pickupItem(knife);
|
||||
if (result.success) {
|
||||
console.log('Got knife!');
|
||||
return true;
|
||||
} else {
|
||||
console.log(`Pickup failed: ${result.message}`);
|
||||
}
|
||||
} else {
|
||||
// Log nearby ground items for debugging
|
||||
const groundItems = ctx.sdk.getState()?.groundItems ?? [];
|
||||
if (groundItems.length > 0) {
|
||||
console.log(`Nearby ground items: ${groundItems.map(i => `${i.name} at (${i.x},${i.z})`).join(', ')}`);
|
||||
}
|
||||
console.log(`No knife visible (attempt ${attempt + 1}/15), waiting for respawn...`);
|
||||
}
|
||||
await sleep(2000); // Wait 2s for potential respawn
|
||||
}
|
||||
|
||||
// If no knife on ground, check if we can buy from a shop that sells knives
|
||||
// General stores don't always stock knives, but some specialty shops do
|
||||
if (!hasKnife(ctx)) {
|
||||
console.log('No knife found on ground. Checking for alternative sources...');
|
||||
|
||||
// Check if there's a knife anywhere nearby we missed
|
||||
const allGroundItems = ctx.sdk.getState()?.groundItems ?? [];
|
||||
console.log(`All visible ground items (${allGroundItems.length}): ${allGroundItems.map(i => i.name).join(', ')}`);
|
||||
}
|
||||
|
||||
return hasKnife(ctx);
|
||||
}
|
||||
|
||||
async function chopAndFletch(ctx: ScriptContext): Promise<void> {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
console.log(`Walking to trees at (${LUMBRIDGE_TREES.x}, ${LUMBRIDGE_TREES.z})...`);
|
||||
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
|
||||
|
||||
let logsChopped = 0;
|
||||
let itemsFletched = 0;
|
||||
let stuckCount = 0;
|
||||
const MAX_STUCK = 10;
|
||||
const LOGS_PER_BATCH = 5; // Chop this many logs before fletching
|
||||
|
||||
while (getFletchingLevel(ctx) < TARGET_LEVEL) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) {
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dismiss any level-up dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(300);
|
||||
continue;
|
||||
}
|
||||
|
||||
const freeSlots = getInventoryFreeSlots(ctx);
|
||||
const logs = countLogs(ctx);
|
||||
const level = getFletchingLevel(ctx);
|
||||
|
||||
// Determine best product based on level
|
||||
// Level 1-4: Arrow shafts only
|
||||
// Level 5-9: Shortbows (5 XP each)
|
||||
// Level 10+: Longbows (10 XP each)
|
||||
let product: string;
|
||||
if (level >= 10) {
|
||||
product = 'long bow';
|
||||
} else if (level >= 5) {
|
||||
product = 'short bow';
|
||||
} else {
|
||||
product = 'arrow shafts';
|
||||
}
|
||||
|
||||
// Drop fletched items if inventory is getting full
|
||||
if (freeSlots <= 2) {
|
||||
await dropFletchedItems(ctx);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have logs, fletch them
|
||||
if (logs >= LOGS_PER_BATCH || (logs > 0 && freeSlots <= 5)) {
|
||||
console.log(`Fletching ${logs} logs into ${product} (level ${level})...`);
|
||||
|
||||
let fletchedThisBatch = 0;
|
||||
let consecutiveFailures = 0;
|
||||
|
||||
while (countLogs(ctx) > 0 && consecutiveFailures < 3) {
|
||||
// Dismiss any dialogs
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await sleep(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
const xpBefore = getFletchingXp(ctx);
|
||||
const result = await bot.fletchLogs(product);
|
||||
|
||||
if (result.success) {
|
||||
fletchedThisBatch++;
|
||||
itemsFletched++;
|
||||
consecutiveFailures = 0;
|
||||
|
||||
const newLevel = getFletchingLevel(ctx);
|
||||
if (newLevel > level) {
|
||||
console.log(`LEVEL UP! Fletching is now level ${newLevel}`);
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures++;
|
||||
console.log(`Fletch failed (${consecutiveFailures}/3): ${result.message}`);
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
if (fletchedThisBatch > 0) {
|
||||
console.log(`[Lvl ${getFletchingLevel(ctx)}] Fletched ${fletchedThisBatch} items, Total: ${itemsFletched}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, chop more trees
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
|
||||
if (!tree) {
|
||||
console.log('No tree nearby, walking to tree area...');
|
||||
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
|
||||
stuckCount++;
|
||||
|
||||
if (stuckCount > MAX_STUCK) {
|
||||
throw new Error('Unable to find trees after multiple attempts');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chop the tree
|
||||
const result = await bot.chopTree(tree);
|
||||
|
||||
if (result.success) {
|
||||
logsChopped++;
|
||||
stuckCount = 0;
|
||||
console.log(`[Lvl ${getFletchingLevel(ctx)}] Chopped tree! Logs: ${countLogs(ctx)}, Total chopped: ${logsChopped}`);
|
||||
} else {
|
||||
console.log(`Chop failed: ${result.message}`);
|
||||
stuckCount++;
|
||||
|
||||
if (stuckCount > MAX_STUCK) {
|
||||
throw new Error(`Stuck: ${stuckCount} failed chop attempts`);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
console.log(`=== GOAL ACHIEVED: Fletching level ${getFletchingLevel(ctx)}! ===`);
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
const username = `fl${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
|
||||
console.log('=== Fletching Trainer ===');
|
||||
console.log(`Goal: Level ${TARGET_LEVEL}`);
|
||||
|
||||
// Wait for state to initialize
|
||||
await sleep(2000);
|
||||
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) {
|
||||
console.error('No player state');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
|
||||
console.log(`Current Fletching level: ${getFletchingLevel(ctx)}`);
|
||||
|
||||
// Dismiss any startup dialogs
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Step 1: Get a knife
|
||||
if (!hasKnife(ctx)) {
|
||||
console.log('No knife found, acquiring one...');
|
||||
const gotKnife = await acquireKnife(ctx);
|
||||
if (!gotKnife) {
|
||||
console.error('Failed to acquire knife, cannot continue');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('Already have a knife!');
|
||||
}
|
||||
|
||||
// Verify we have an axe
|
||||
if (!hasAxe(ctx)) {
|
||||
console.error('No axe found! The preset should include a bronze axe.');
|
||||
return;
|
||||
}
|
||||
console.log('Have axe - ready to chop trees');
|
||||
|
||||
// Step 2: Chop trees and fletch until target level
|
||||
console.log('Starting fletching training...');
|
||||
await chopAndFletch(ctx);
|
||||
|
||||
console.log('=== Script Complete ===');
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
// Helper
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -1,338 +0,0 @@
|
||||
/**
|
||||
* Magic Trainer Script
|
||||
*
|
||||
* Goal: Train Magic to level 10+ starting from a fresh Lumbridge spawn.
|
||||
*
|
||||
* Strategy:
|
||||
* - LUMBRIDGE_SPAWN preset provides: Air x25, Mind x15, Water x6, Earth x4, Body x2
|
||||
* - Wind Strike = 1 Air + 1 Mind = 5.5 XP per cast
|
||||
* - 15 Mind runes = 15 casts = 82.5 XP (gets to ~level 2-3)
|
||||
* - Level 10 requires 1,154 XP = ~210 Wind Strikes
|
||||
* - Need to find more runes or supplement with melee
|
||||
*
|
||||
* Plan:
|
||||
* 1. Walk to chicken coop near Lumbridge (3235, 3295)
|
||||
* 2. Cast Wind Strike on chickens until runes run out
|
||||
* 3. Log progress and track XP gains
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Spell component IDs
|
||||
const Spells = {
|
||||
WIND_STRIKE: 1152,
|
||||
WATER_STRIKE: 1154,
|
||||
EARTH_STRIKE: 1156,
|
||||
};
|
||||
|
||||
// Locations
|
||||
const CHICKEN_COOP = { x: 3235, z: 3295 }; // Near Lumbridge
|
||||
|
||||
// XP required for each level
|
||||
const MAGIC_XP_TABLE = [
|
||||
0, 0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, // 0-10
|
||||
1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, // 11-20
|
||||
];
|
||||
|
||||
/**
|
||||
* Find the best target for magic training
|
||||
*/
|
||||
function findTarget(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
// Prefer chickens (weakest), then rats, then goblins
|
||||
const targets = state.nearbyNpcs
|
||||
.filter(npc =>
|
||||
/chicken/i.test(npc.name) ||
|
||||
/rat/i.test(npc.name) ||
|
||||
/goblin/i.test(npc.name)
|
||||
)
|
||||
.filter(npc => !npc.inCombat) // Don't target NPCs already fighting
|
||||
.filter(npc => npc.distance <= 10) // Within reasonable range
|
||||
.sort((a, b) => {
|
||||
// Priority: chicken > rat > goblin
|
||||
const priority = (name: string) => {
|
||||
if (/chicken/i.test(name)) return 0;
|
||||
if (/rat/i.test(name)) return 1;
|
||||
return 2;
|
||||
};
|
||||
const pDiff = priority(a.name) - priority(b.name);
|
||||
if (pDiff !== 0) return pDiff;
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return targets[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rune counts
|
||||
*/
|
||||
function getRuneCounts(ctx: ScriptContext): { air: number; mind: number; water: number; earth: number } {
|
||||
const airRunes = ctx.sdk.findInventoryItem(/^air rune$/i);
|
||||
const mindRunes = ctx.sdk.findInventoryItem(/^mind rune$/i);
|
||||
const waterRunes = ctx.sdk.findInventoryItem(/^water rune$/i);
|
||||
const earthRunes = ctx.sdk.findInventoryItem(/^earth rune$/i);
|
||||
|
||||
return {
|
||||
air: airRunes?.count ?? 0,
|
||||
mind: mindRunes?.count ?? 0,
|
||||
water: waterRunes?.count ?? 0,
|
||||
earth: earthRunes?.count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which spell to cast based on available runes and magic level
|
||||
*/
|
||||
function getBestSpell(ctx: ScriptContext): { spell: number; name: string; xp: number } | null {
|
||||
const runes = getRuneCounts(ctx);
|
||||
const magicLevel = ctx.sdk.getSkill('Magic')?.baseLevel ?? 1;
|
||||
|
||||
// Wind Strike: 1 Air + 1 Mind, level 1, 5.5 XP
|
||||
if (runes.air >= 1 && runes.mind >= 1) {
|
||||
return { spell: Spells.WIND_STRIKE, name: 'Wind Strike', xp: 5.5 };
|
||||
}
|
||||
|
||||
// Water Strike: 1 Water + 1 Air + 1 Mind, level 5, 7.5 XP
|
||||
if (magicLevel >= 5 && runes.water >= 1 && runes.air >= 1 && runes.mind >= 1) {
|
||||
return { spell: Spells.WATER_STRIKE, name: 'Water Strike', xp: 7.5 };
|
||||
}
|
||||
|
||||
// Earth Strike: 2 Earth + 1 Air + 1 Mind, level 9, 9.5 XP
|
||||
if (magicLevel >= 9 && runes.earth >= 2 && runes.air >= 1 && runes.mind >= 1) {
|
||||
return { spell: Spells.EARTH_STRIKE, name: 'Earth Strike', xp: 9.5 };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface MagicStats {
|
||||
startXp: number;
|
||||
startLevel: number;
|
||||
casts: number;
|
||||
hits: number;
|
||||
misses: number; // Splashes
|
||||
lastCastTime: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main magic training loop
|
||||
*/
|
||||
async function magicTrainingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
// Initialize stats
|
||||
const magicSkill = ctx.sdk.getSkill('Magic');
|
||||
const stats: MagicStats = {
|
||||
startXp: magicSkill?.experience ?? 0,
|
||||
startLevel: magicSkill?.baseLevel ?? 1,
|
||||
casts: 0,
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
lastCastTime: 0,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
console.log('=== Magic Trainer ===');
|
||||
console.log(`Goal: Train Magic from level ${stats.startLevel} (need 1,154 XP for level 10)`);
|
||||
|
||||
// Log rune inventory
|
||||
const runes = getRuneCounts(ctx);
|
||||
console.log(`Runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
|
||||
console.log(`Potential casts: ~${Math.min(runes.air, runes.mind)} Wind Strikes (~${Math.min(runes.air, runes.mind) * 5.5} XP)`);
|
||||
|
||||
// Walk to chicken coop
|
||||
console.log('Walking to chicken coop...');
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
|
||||
// Open the gate to get inside the coop
|
||||
console.log('Opening gate...');
|
||||
const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
if (gateResult.success) {
|
||||
console.log(`Gate opened: ${gateResult.message}`);
|
||||
// Walk inside the coop
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
|
||||
} else {
|
||||
console.log(`Gate: ${gateResult.message}`);
|
||||
}
|
||||
|
||||
let noTargetCount = 0;
|
||||
let failedCastCount = 0;
|
||||
const MAX_NO_TARGET_ATTEMPTS = 30; // Exit if no targets for too long
|
||||
const MAX_FAILED_CASTS = 5; // Exit if too many failed casts in a row
|
||||
|
||||
// Main loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any blocking dialogs (level-up, etc.)
|
||||
if (currentState.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've reached level 10
|
||||
const currentMagic = ctx.sdk.getSkill('Magic');
|
||||
const currentLevel = currentMagic?.baseLevel ?? 1;
|
||||
if (currentLevel >= 10) {
|
||||
console.log('*** GOAL REACHED: Magic level 10! ***');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we have runes
|
||||
const spell = getBestSpell(ctx);
|
||||
if (!spell) {
|
||||
console.log('Out of runes! Cannot cast any more spells.');
|
||||
break;
|
||||
}
|
||||
|
||||
// Find a target
|
||||
const target = findTarget(ctx);
|
||||
if (!target) {
|
||||
noTargetCount++;
|
||||
if (noTargetCount >= MAX_NO_TARGET_ATTEMPTS) {
|
||||
console.log(`No targets found after ${noTargetCount} attempts, exiting.`);
|
||||
break;
|
||||
}
|
||||
if (noTargetCount % 5 === 0) {
|
||||
console.log(`No targets nearby (attempt ${noTargetCount}/${MAX_NO_TARGET_ATTEMPTS}), walking around...`);
|
||||
// Walk to chicken coop center
|
||||
await ctx.bot.walkTo(
|
||||
CHICKEN_COOP.x + Math.floor(Math.random() * 6) - 3,
|
||||
CHICKEN_COOP.z + Math.floor(Math.random() * 6) - 3
|
||||
);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
noTargetCount = 0;
|
||||
|
||||
// Don't spam casts - wait between attempts
|
||||
const now = Date.now();
|
||||
if (now - stats.lastCastTime < 2500) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Walk closer if target is far (magic range is ~10 but need clear LOS)
|
||||
if (target.distance > 5) {
|
||||
console.log(`Walking toward ${target.name} at (${target.x}, ${target.z}), dist: ${target.distance}`);
|
||||
// Walk to within ~3 tiles of the target
|
||||
await ctx.bot.walkTo(target.x, target.z);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cast spell on target using high-level API
|
||||
if (stats.casts % 5 === 0 || stats.casts === 0) {
|
||||
const currentRunes = getRuneCounts(ctx);
|
||||
console.log(`Casting ${spell.name} on ${target.name} (cast #${stats.casts + 1}, dist=${target.distance}, runes: air=${currentRunes.air}, mind=${currentRunes.mind})`);
|
||||
}
|
||||
|
||||
const castResult = await ctx.bot.castSpellOnNpc(target, spell.spell);
|
||||
stats.casts++;
|
||||
stats.lastCastTime = now;
|
||||
|
||||
if (castResult.success) {
|
||||
failedCastCount = 0; // Reset on success
|
||||
if (castResult.hit) {
|
||||
stats.hits++;
|
||||
console.log(`HIT! ${castResult.message}`);
|
||||
}
|
||||
// Splash is still success, just no XP
|
||||
} else if (castResult.reason === 'out_of_reach') {
|
||||
// Gate likely closed - try to open it
|
||||
console.log(`Can't reach target - trying to open gate`);
|
||||
const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
console.log(`Gate: ${gateResult.message}`);
|
||||
if (gateResult.success) {
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
|
||||
}
|
||||
failedCastCount++;
|
||||
} else if (castResult.reason === 'no_runes') {
|
||||
console.log('Out of runes!');
|
||||
break;
|
||||
} else {
|
||||
// Unknown failure reason
|
||||
failedCastCount++;
|
||||
console.log(`Cast failed: ${castResult.message} (reason: ${castResult.reason})`);
|
||||
}
|
||||
|
||||
// Exit if too many consecutive failures
|
||||
if (failedCastCount >= MAX_FAILED_CASTS) {
|
||||
console.log(`Too many failed casts (${failedCastCount}), exiting.`);
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Final report
|
||||
logFinalStats(ctx, stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log final training statistics
|
||||
*/
|
||||
function logFinalStats(ctx: ScriptContext, stats: MagicStats): void {
|
||||
const currentMagic = ctx.sdk.getSkill('Magic');
|
||||
const finalXp = currentMagic?.experience ?? 0;
|
||||
const finalLevel = currentMagic?.baseLevel ?? 1;
|
||||
const xpGained = finalXp - stats.startXp;
|
||||
const elapsed = (Date.now() - stats.startTime) / 1000;
|
||||
const xpPerHour = elapsed > 0 ? Math.round((xpGained / elapsed) * 3600) : 0;
|
||||
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Magic: Level ${stats.startLevel} -> ${finalLevel}`);
|
||||
console.log(`XP: ${stats.startXp} -> ${finalXp} (+${xpGained})`);
|
||||
console.log(`Casts: ${stats.casts} (Hits: ${stats.hits}, Splashes: ${stats.casts - stats.hits})`);
|
||||
console.log(`Duration: ${elapsed.toFixed(1)}s`);
|
||||
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
|
||||
|
||||
// Log remaining runes
|
||||
const runes = getRuneCounts(ctx);
|
||||
console.log(`Remaining runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `mg${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await magicTrainingLoop(ctx);
|
||||
} finally {
|
||||
// Log final state
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
const magic = state.skills.find(s => s.name === 'Magic');
|
||||
console.log(`Final Magic: Level ${magic?.baseLevel ?? '?'}, XP ${magic?.experience ?? '?'}`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,535 +0,0 @@
|
||||
/**
|
||||
* Mining Training Script v9
|
||||
*
|
||||
* Goal: Maximize Mining level in 5 minutes
|
||||
*
|
||||
* Strategy:
|
||||
* - Start at Lumbridge (natural spawn) with tutorial-complete gear
|
||||
* - Walk north to SE Varrock mine (no toll needed, no aggressive mobs)
|
||||
* - Mine copper/tin rocks continuously
|
||||
* - Drop ore when inventory is full
|
||||
*
|
||||
* v9 Changes:
|
||||
* - Switch to SE Varrock mine (safer, no scorpions)
|
||||
* - No need to sell items or pay toll
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// SE Varrock mine - has copper and tin, no aggressive monsters
|
||||
const TARGET_MINE = { x: 3285, z: 3365 };
|
||||
|
||||
// Waypoints from Lumbridge to SE Varrock mine
|
||||
const WAYPOINTS_TO_MINE = [
|
||||
{ x: 3222, z: 3230 }, // North from Lumbridge
|
||||
{ x: 3222, z: 3250 }, // Continue north
|
||||
{ x: 3230, z: 3280 }, // Northeast
|
||||
{ x: 3250, z: 3310 }, // Continue NE
|
||||
{ x: 3270, z: 3340 }, // Getting closer
|
||||
{ x: 3285, z: 3360 }, // Near mine
|
||||
TARGET_MINE, // SE Varrock mine
|
||||
];
|
||||
|
||||
// Get mining level and XP
|
||||
function getMiningStats(ctx: ScriptContext): { level: number; xp: number } {
|
||||
const state = ctx.sdk.getState();
|
||||
const mining = state?.skills.find(s => s.name === 'Mining');
|
||||
return {
|
||||
level: mining?.baseLevel ?? 1,
|
||||
xp: mining?.experience ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Log mining progress
|
||||
function logProgress(ctx: ScriptContext, label: string): void {
|
||||
const stats = getMiningStats(ctx);
|
||||
const invCount = ctx.sdk.getState()?.inventory.length ?? 0;
|
||||
console.log(`[${label}] Mining Lv${stats.level} (${stats.xp} XP) | Inv: ${invCount}/28`);
|
||||
}
|
||||
|
||||
// Rock info with ore type from prospecting
|
||||
interface RockInfo {
|
||||
x: number;
|
||||
z: number;
|
||||
id: number;
|
||||
name: string;
|
||||
mineOpIndex: number;
|
||||
prospectOpIndex: number;
|
||||
distance: number;
|
||||
oreType?: string; // Set after prospecting
|
||||
}
|
||||
|
||||
// Find all nearby rocks with Mine option
|
||||
function findRocks(ctx: ScriptContext): RockInfo[] {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return [];
|
||||
|
||||
const rocks: RockInfo[] = [];
|
||||
for (const loc of state.nearbyLocs) {
|
||||
if (/rocks?/i.test(loc.name) && !/rockslide/i.test(loc.name)) {
|
||||
const mineOpt = loc.optionsWithIndex.find(o => /mine/i.test(o.text));
|
||||
const prospectOpt = loc.optionsWithIndex.find(o => /prospect/i.test(o.text));
|
||||
if (mineOpt && prospectOpt) {
|
||||
rocks.push({
|
||||
x: loc.x,
|
||||
z: loc.z,
|
||||
id: loc.id,
|
||||
name: loc.name,
|
||||
mineOpIndex: mineOpt.opIndex,
|
||||
prospectOpIndex: prospectOpt.opIndex,
|
||||
distance: loc.distance
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort by distance
|
||||
return rocks.sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
// Prospect a rock to find ore type
|
||||
async function prospectRock(ctx: ScriptContext, rock: RockInfo): Promise<string | null> {
|
||||
const { sdk } = ctx;
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
|
||||
await sdk.sendInteractLoc(rock.x, rock.z, rock.id, rock.prospectOpIndex);
|
||||
|
||||
// Wait for prospect message
|
||||
try {
|
||||
const state = await sdk.waitForCondition(s => {
|
||||
for (const msg of s.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
// Look for "This rock contains X" messages
|
||||
if (text.includes('rock contains') || text.includes('ore')) {
|
||||
return true;
|
||||
}
|
||||
if (text.includes('no ore') || text.includes('nothing')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, 5000);
|
||||
|
||||
// Extract ore type from message
|
||||
for (const msg of state.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
if (text.includes('copper')) return 'copper';
|
||||
if (text.includes('tin')) return 'tin';
|
||||
if (text.includes('iron')) return 'iron';
|
||||
if (text.includes('gold')) return 'gold';
|
||||
if (text.includes('silver')) return 'silver';
|
||||
if (text.includes('coal')) return 'coal';
|
||||
if (text.includes('mithril')) return 'mithril';
|
||||
if (text.includes('adamant')) return 'adamant';
|
||||
if (text.includes('rune') || text.includes('runite')) return 'runite';
|
||||
if (text.includes('no ore') || text.includes('nothing')) return 'empty';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Known mineable ores by level
|
||||
const MINEABLE_AT_LEVEL: Record<number, string[]> = {
|
||||
1: ['copper', 'tin'],
|
||||
15: ['iron'],
|
||||
20: ['silver'],
|
||||
30: ['coal'],
|
||||
40: ['gold'],
|
||||
55: ['mithril'],
|
||||
70: ['adamant'],
|
||||
85: ['runite']
|
||||
};
|
||||
|
||||
function canMineOre(oreType: string, miningLevel: number): boolean {
|
||||
for (const [reqLevel, ores] of Object.entries(MINEABLE_AT_LEVEL)) {
|
||||
if (ores.includes(oreType) && miningLevel >= parseInt(reqLevel)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drop all ore from inventory
|
||||
async function dropOre(ctx: ScriptContext): Promise<number> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
let dropped = 0;
|
||||
for (const item of state.inventory) {
|
||||
// Drop copper, tin, iron ore (and any other ore)
|
||||
if (/ore/i.test(item.name)) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
dropped++;
|
||||
}
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
// Check if inventory is full
|
||||
function isInventoryFull(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
return (state?.inventory.length ?? 0) >= 28;
|
||||
}
|
||||
|
||||
// Walk using small direct steps (bypasses server pathfinder issues)
|
||||
async function walkSmallSteps(
|
||||
ctx: ScriptContext,
|
||||
targetX: number,
|
||||
targetZ: number,
|
||||
stepSize: number = 12
|
||||
): Promise<{ x: number; z: number }> {
|
||||
const { sdk } = ctx;
|
||||
|
||||
const MAX_STEPS = 20;
|
||||
for (let step = 0; step < MAX_STEPS; step++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) break;
|
||||
|
||||
const currentX = state.player.worldX;
|
||||
const currentZ = state.player.worldZ;
|
||||
const dist = Math.sqrt(Math.pow(targetX - currentX, 2) + Math.pow(targetZ - currentZ, 2));
|
||||
|
||||
if (dist <= 3) {
|
||||
return { x: currentX, z: currentZ };
|
||||
}
|
||||
|
||||
// Calculate next step towards target
|
||||
const ratio = Math.min(stepSize / dist, 1);
|
||||
const nextX = Math.round(currentX + (targetX - currentX) * ratio);
|
||||
const nextZ = Math.round(currentZ + (targetZ - currentZ) * ratio);
|
||||
|
||||
// Use direct walk command (client-side pathfinding)
|
||||
await sdk.sendWalk(nextX, nextZ, true);
|
||||
|
||||
// Wait for movement
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Check if we moved
|
||||
const newState = ctx.sdk.getState();
|
||||
if (newState?.player) {
|
||||
const movedDist = Math.sqrt(
|
||||
Math.pow(newState.player.worldX - currentX, 2) +
|
||||
Math.pow(newState.player.worldZ - currentZ, 2)
|
||||
);
|
||||
if (movedDist < 1) {
|
||||
// Stuck - try a slightly different direction
|
||||
const jitterX = nextX + Math.round((Math.random() - 0.5) * 4);
|
||||
const jitterZ = nextZ + Math.round((Math.random() - 0.5) * 4);
|
||||
await sdk.sendWalk(jitterX, jitterZ, true);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalState = ctx.sdk.getState();
|
||||
return {
|
||||
x: finalState?.player?.worldX ?? 0,
|
||||
z: finalState?.player?.worldZ ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Walk to a destination using waypoints with small steps
|
||||
// Returns early if rocks are found along the way
|
||||
async function walkWithWaypoints(
|
||||
ctx: ScriptContext,
|
||||
waypoints: Array<{ x: number; z: number }>,
|
||||
label: string
|
||||
): Promise<{ foundRocks: boolean }> {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
for (let i = 0; i < waypoints.length; i++) {
|
||||
const wp = waypoints[i];
|
||||
if (!wp) continue;
|
||||
const state = ctx.sdk.getState();
|
||||
const playerPos = state?.player ? `(${state.player.worldX}, ${state.player.worldZ})` : '?';
|
||||
console.log(`Walking to waypoint ${i + 1}/${waypoints.length}: (${wp.x}, ${wp.z}) from ${playerPos}`);
|
||||
|
||||
// Don't stop early for rocks - we need to reach the copper/tin area
|
||||
// (The mine has gold/coal near the entrance, copper/tin further in)
|
||||
|
||||
// Try bot.walkTo first (uses server pathfinder)
|
||||
const result = await bot.walkTo(wp.x, wp.z);
|
||||
|
||||
let newState = ctx.sdk.getState();
|
||||
let currentX = newState?.player?.worldX ?? 0;
|
||||
let currentZ = newState?.player?.worldZ ?? 0;
|
||||
let dist = Math.sqrt(Math.pow(wp.x - currentX, 2) + Math.pow(wp.z - currentZ, 2));
|
||||
|
||||
// If walkTo failed and we're still far, try small steps
|
||||
if (!result.success && dist > 5) {
|
||||
console.log(`Server pathfinder failed, trying small steps...`);
|
||||
const pos = await walkSmallSteps(ctx, wp.x, wp.z);
|
||||
currentX = pos.x;
|
||||
currentZ = pos.z;
|
||||
dist = Math.sqrt(Math.pow(wp.x - currentX, 2) + Math.pow(wp.z - currentZ, 2));
|
||||
|
||||
// Continue walking to target - don't stop for rocks
|
||||
}
|
||||
|
||||
console.log(`Waypoint ${i + 1}: now at (${currentX}, ${currentZ}), ${dist.toFixed(0)} tiles from target`);
|
||||
|
||||
// Brief pause between waypoints
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
const finalState = ctx.sdk.getState();
|
||||
const finalPos = finalState?.player ? `(${finalState.player.worldX}, ${finalState.player.worldZ})` : '?';
|
||||
console.log(`${label} - final position: ${finalPos}`);
|
||||
return { foundRocks: false };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `mn${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
// Log initial state
|
||||
logProgress(ctx, 'START');
|
||||
let oresMined = 0;
|
||||
let lastXp = getMiningStats(ctx).xp;
|
||||
|
||||
// Check for pickaxe
|
||||
let pickaxe = sdk.findInventoryItem(/pickaxe/i);
|
||||
if (!pickaxe) {
|
||||
// Check if already equipped
|
||||
pickaxe = sdk.findEquipmentItem(/pickaxe/i);
|
||||
if (!pickaxe) {
|
||||
console.log('ERROR: No pickaxe in inventory or equipped!');
|
||||
return;
|
||||
}
|
||||
console.log(`Found ${pickaxe.name} already equipped`);
|
||||
} else {
|
||||
console.log(`Found ${pickaxe.name} in inventory - equipping it...`);
|
||||
const equipResult = await bot.equipItem(pickaxe);
|
||||
if (equipResult.success) {
|
||||
console.log('Pickaxe equipped!');
|
||||
} else {
|
||||
console.log(`Failed to equip pickaxe: ${equipResult.message} - will try mining anyway`);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk to SE Varrock mine (safer than Al Kharid - no scorpions)
|
||||
console.log('Walking to SE Varrock mine...');
|
||||
await walkWithWaypoints(ctx, WAYPOINTS_TO_MINE, 'ARRIVED AT MINE');
|
||||
|
||||
// Check current position and look for rocks
|
||||
const posState = ctx.sdk.getState();
|
||||
const currentPos = posState?.player
|
||||
? { x: posState.player.worldX, z: posState.player.worldZ }
|
||||
: TARGET_MINE;
|
||||
|
||||
// Update mine location to where we actually ended up (for drift detection)
|
||||
const actualMineLocation = { ...currentPos };
|
||||
|
||||
// Look for rocks in the area - if none found, try walking around
|
||||
let rocks = findRocks(ctx);
|
||||
if (rocks.length === 0) {
|
||||
console.log(`No rocks at (${currentPos.x}, ${currentPos.z}), searching nearby...`);
|
||||
// Try walking to nearby locations to find rocks
|
||||
const searchOffsets = [
|
||||
{ x: -10, z: 0 }, { x: 10, z: 0 },
|
||||
{ x: 0, z: -10 }, { x: 0, z: 10 },
|
||||
{ x: -15, z: -15 }, { x: 15, z: -15 },
|
||||
];
|
||||
for (const offset of searchOffsets) {
|
||||
const searchX = currentPos.x + offset.x;
|
||||
const searchZ = currentPos.z + offset.z;
|
||||
console.log(`Searching at (${searchX}, ${searchZ})...`);
|
||||
await walkSmallSteps(ctx, searchX, searchZ, 8);
|
||||
|
||||
rocks = findRocks(ctx);
|
||||
if (rocks.length > 0) {
|
||||
const newPos = ctx.sdk.getState()?.player;
|
||||
if (newPos) {
|
||||
actualMineLocation.x = newPos.worldX;
|
||||
actualMineLocation.z = newPos.worldZ;
|
||||
}
|
||||
console.log(`Found ${rocks.length} rocks at (${actualMineLocation.x}, ${actualMineLocation.z})!`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logProgress(ctx, 'READY TO MINE');
|
||||
|
||||
// Cache of rock ID -> ore type (from prospecting)
|
||||
const oreTypeCache = new Map<number, string>();
|
||||
let consecutiveFailures = 0;
|
||||
const MAX_FAILURES = 5;
|
||||
|
||||
// Main mining loop
|
||||
while (true) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentStats = getMiningStats(ctx);
|
||||
|
||||
// Check for XP gain (successful mine)
|
||||
if (currentStats.xp > lastXp) {
|
||||
const xpGained = currentStats.xp - lastXp;
|
||||
oresMined++;
|
||||
lastXp = currentStats.xp;
|
||||
consecutiveFailures = 0; // Reset on success
|
||||
|
||||
if (oresMined % 5 === 0) {
|
||||
logProgress(ctx, `MINED #${oresMined}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss any dialogs (level-up messages)
|
||||
if (state.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if inventory is full - drop ore
|
||||
if (isInventoryFull(ctx)) {
|
||||
console.log('Inventory full, dropping ore...');
|
||||
const dropped = await dropOre(ctx);
|
||||
console.log(`Dropped ${dropped} ores`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find rocks
|
||||
const rocks = findRocks(ctx);
|
||||
if (rocks.length === 0) {
|
||||
// Return to mine center
|
||||
const player = state.player;
|
||||
const distFromMine = Math.sqrt(
|
||||
Math.pow(player.worldX - actualMineLocation.x, 2) +
|
||||
Math.pow(player.worldZ - actualMineLocation.z, 2)
|
||||
);
|
||||
if (distFromMine > 5) {
|
||||
console.log(`No rocks nearby, returning to mine center (${distFromMine.toFixed(0)} tiles away)...`);
|
||||
await walkSmallSteps(ctx, actualMineLocation.x, actualMineLocation.z, 8);
|
||||
} else {
|
||||
console.log('No rocks nearby, waiting for respawn...');
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a mineable rock - only prospect rocks within reasonable range
|
||||
const MAX_PROSPECT_DIST = 8;
|
||||
let targetRock: RockInfo | null = null;
|
||||
|
||||
for (const rock of rocks) {
|
||||
// Check cache first
|
||||
const cachedOre = oreTypeCache.get(rock.id);
|
||||
if (cachedOre) {
|
||||
if (cachedOre === 'empty') continue;
|
||||
if (!canMineOre(cachedOre, currentStats.level)) {
|
||||
continue; // Skip rocks we can't mine
|
||||
}
|
||||
targetRock = rock;
|
||||
targetRock.oreType = cachedOre;
|
||||
break;
|
||||
}
|
||||
|
||||
// Only prospect nearby rocks (don't walk too far)
|
||||
if (rock.distance > MAX_PROSPECT_DIST) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unknown rock - prospect it
|
||||
console.log(`Prospecting rock id=${rock.id} dist=${rock.distance}...`);
|
||||
const oreType = await prospectRock(ctx, rock);
|
||||
if (oreType) {
|
||||
console.log(`Rock ${rock.id} = ${oreType}`);
|
||||
oreTypeCache.set(rock.id, oreType);
|
||||
|
||||
if (oreType !== 'empty' && canMineOre(oreType, currentStats.level)) {
|
||||
targetRock = rock;
|
||||
targetRock.oreType = oreType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRock) {
|
||||
consecutiveFailures++;
|
||||
console.log(`No mineable rocks found (failure ${consecutiveFailures}/${MAX_FAILURES})`);
|
||||
|
||||
if (consecutiveFailures >= MAX_FAILURES) {
|
||||
console.log('ERROR: No mineable rocks at this location! Need copper/tin at level 1.');
|
||||
console.log('Cached ore types: ' + Array.from(oreTypeCache.entries()).map(([id, ore]) => `${id}=${ore}`).join(', '));
|
||||
return; // Exit script - wrong mine location
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mine the rock
|
||||
console.log(`Mining ${targetRock.oreType} rock id=${targetRock.id} dist=${targetRock.distance}`);
|
||||
const startXp = currentStats.xp;
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
|
||||
const mineResult = await sdk.sendInteractLoc(
|
||||
targetRock.x, targetRock.z, targetRock.id, targetRock.mineOpIndex
|
||||
);
|
||||
|
||||
if (!mineResult.success) {
|
||||
console.log(`Mine action failed: ${mineResult.message}`);
|
||||
consecutiveFailures++;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for mining to complete
|
||||
try {
|
||||
await sdk.waitForCondition(state => {
|
||||
// Success: XP gained
|
||||
const miningXp = state.skills.find(s => s.name === 'Mining')?.experience ?? 0;
|
||||
if (miningXp > startXp) return true;
|
||||
|
||||
// Rock depleted
|
||||
const rockNow = state.nearbyLocs.find(l =>
|
||||
l.x === targetRock!.x && l.z === targetRock!.z
|
||||
);
|
||||
if (!rockNow || rockNow.id !== targetRock!.id) return true;
|
||||
|
||||
// Dismiss dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
sdk.sendClickDialog(0).catch(() => {});
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 8000);
|
||||
|
||||
consecutiveFailures = 0;
|
||||
} catch {
|
||||
consecutiveFailures++;
|
||||
console.log(`Mining timeout (failure ${consecutiveFailures}/${MAX_FAILURES})`);
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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.
|
||||
@@ -1,331 +0,0 @@
|
||||
/**
|
||||
* Prayer Trainer Script
|
||||
*
|
||||
* Goal: Train Prayer from level 1 to level 10+
|
||||
*
|
||||
* Strategy:
|
||||
* - Walk to chicken coop near Lumbridge (east of castle)
|
||||
* - Kill chickens (they drop bones)
|
||||
* - Pick up bones and bury them
|
||||
* - Repeat until Prayer level 10
|
||||
*
|
||||
* Prayer XP per bone buried: 4.5 XP
|
||||
* XP needed for level 10: 1,154 XP
|
||||
* Bones needed: ~257 bones (1154 / 4.5)
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Chicken coop is east of Lumbridge castle, near the farm
|
||||
// The coop is fenced - need to enter through the gate
|
||||
const CHICKEN_COOP_ENTRANCE = { x: 3237, z: 3295 }; // Just outside gate
|
||||
const CHICKEN_COOP_INSIDE = { x: 3232, z: 3295 }; // Inside the coop
|
||||
|
||||
// Prayer level goal
|
||||
const TARGET_PRAYER_LEVEL = 5;
|
||||
|
||||
// Stats tracking
|
||||
interface PrayerStats {
|
||||
bonesCollected: number;
|
||||
bonesBuried: number;
|
||||
chickensKilled: number;
|
||||
startXp: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best chicken to attack.
|
||||
* Prioritizes chickens not in combat and closest to player.
|
||||
*/
|
||||
function findBestChicken(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const chickens = state.nearbyNpcs
|
||||
.filter(npc => /^chicken$/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(o => /attack/i.test(o)))
|
||||
// Filter out chickens already in combat
|
||||
.filter(npc => !npc.inCombat || npc.targetIndex === -1)
|
||||
.sort((a, b) => {
|
||||
// Prefer chickens not in combat
|
||||
if (a.inCombat !== b.inCombat) {
|
||||
return a.inCombat ? 1 : -1;
|
||||
}
|
||||
// Then by distance
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return chickens[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for combat to end (chicken dies).
|
||||
* Chickens are level 1 and die very quickly.
|
||||
*/
|
||||
async function waitForCombatEnd(
|
||||
ctx: ScriptContext,
|
||||
targetNpc: NearbyNpc,
|
||||
stats: PrayerStats
|
||||
): Promise<'kill' | 'fled' | 'lost_target'> {
|
||||
const maxWaitMs = 15000; // 15 seconds max for a chicken
|
||||
const startTime = Date.now();
|
||||
|
||||
// Initial delay for combat to start
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 'lost_target';
|
||||
|
||||
// Dismiss any level-up dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the chicken we're fighting
|
||||
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
|
||||
|
||||
if (!target) {
|
||||
// Chicken disappeared - it died
|
||||
stats.chickensKilled++;
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
// Check if chicken has 0 HP (dead)
|
||||
if (target.maxHp > 0 && target.hp === 0) {
|
||||
stats.chickensKilled++;
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss any open dialog (level-up, etc.)
|
||||
*/
|
||||
async function dismissDialog(ctx: ScriptContext): Promise<boolean> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bury all bones in inventory.
|
||||
*/
|
||||
async function buryAllBones(ctx: ScriptContext, stats: PrayerStats): Promise<void> {
|
||||
// Dismiss any dialogs before starting
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!(await dismissDialog(ctx))) break;
|
||||
}
|
||||
|
||||
// Re-fetch inventory after dismissing dialogs (state may have changed)
|
||||
let inventory = ctx.sdk.getInventory();
|
||||
let bones = inventory.filter(i => /^bones$/i.test(i.name));
|
||||
|
||||
for (const bone of bones) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) break;
|
||||
|
||||
// Dismiss any dialogs that appeared (level-up, etc.)
|
||||
if (state.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue; // Re-check state after dismissing
|
||||
}
|
||||
|
||||
// Re-check bone still exists (may have been buried in dialog handling)
|
||||
const currentInv = ctx.sdk.getInventory();
|
||||
const currentBone = currentInv.find(i => i.slot === bone.slot && /^bones$/i.test(i.name));
|
||||
if (!currentBone) continue;
|
||||
|
||||
// Find "Bury" option on bone
|
||||
const buryOpt = currentBone.optionsWithIndex.find(o => /bury/i.test(o.text));
|
||||
if (buryOpt) {
|
||||
await ctx.sdk.sendUseItem(currentBone.slot, buryOpt.opIndex);
|
||||
stats.bonesBuried++;
|
||||
|
||||
// Wait for bury animation and XP to register
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
|
||||
// Log progress periodically
|
||||
if (stats.bonesBuried % 10 === 0) {
|
||||
const prayerSkill = ctx.sdk.getState()?.skills.find(s => s.name === 'Prayer');
|
||||
console.log(`Buried ${stats.bonesBuried} bones - Prayer: Level ${prayerSkill?.baseLevel} (${prayerSkill?.experience} XP)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main prayer training loop
|
||||
*/
|
||||
async function prayerTrainingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
const prayerSkill = state.skills.find(s => s.name === 'Prayer');
|
||||
const stats: PrayerStats = {
|
||||
bonesCollected: 0,
|
||||
bonesBuried: 0,
|
||||
chickensKilled: 0,
|
||||
startXp: prayerSkill?.experience ?? 0,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
console.log('=== Prayer Trainer ===');
|
||||
console.log(`Goal: Train Prayer to level ${TARGET_PRAYER_LEVEL}`);
|
||||
console.log(`Starting Prayer: Level ${prayerSkill?.baseLevel ?? 1} (${prayerSkill?.experience ?? 0} XP)`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
|
||||
// Dismiss any initial dialogs
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Equip starting gear for faster kills
|
||||
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
|
||||
if (sword) {
|
||||
console.log('Equipping bronze sword...');
|
||||
await ctx.bot.equipItem(sword);
|
||||
}
|
||||
|
||||
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
|
||||
if (shield) {
|
||||
console.log('Equipping wooden shield...');
|
||||
await ctx.bot.equipItem(shield);
|
||||
}
|
||||
|
||||
// Walk to chicken coop entrance and enter through gate
|
||||
console.log(`Walking to chicken coop entrance at (${CHICKEN_COOP_ENTRANCE.x}, ${CHICKEN_COOP_ENTRANCE.z})...`);
|
||||
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
|
||||
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
|
||||
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
|
||||
|
||||
// Open the gate to enter the chicken coop
|
||||
console.log('Opening chicken coop gate...');
|
||||
const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
if (!gateResult.success && gateResult.reason !== 'already_open') {
|
||||
console.warn(`Gate issue: ${gateResult.message}`);
|
||||
}
|
||||
|
||||
// Walk inside the coop
|
||||
console.log(`Walking inside coop to (${CHICKEN_COOP_INSIDE.x}, ${CHICKEN_COOP_INSIDE.z})...`);
|
||||
await ctx.bot.walkTo(CHICKEN_COOP_INSIDE.x, CHICKEN_COOP_INSIDE.z);
|
||||
|
||||
// Main loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've reached target level
|
||||
const prayer = currentState.skills.find(s => s.name === 'Prayer');
|
||||
if (prayer && prayer.baseLevel >= TARGET_PRAYER_LEVEL) {
|
||||
console.log(`*** Goal achieved! Prayer level ${prayer.baseLevel} ***`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any dialogs (level-up, etc.) - click multiple times to clear multi-page dialogs
|
||||
if (currentState.dialog.isOpen) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) break;
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check inventory for bones - bury them as soon as we have any
|
||||
const inventory = ctx.sdk.getInventory();
|
||||
const bonesInInv = inventory.filter(i => /^bones$/i.test(i.name));
|
||||
|
||||
// Bury bones immediately when we have 1+ (don't wait to accumulate)
|
||||
// This is faster than waiting and prevents inventory from filling
|
||||
if (bonesInInv.length >= 1) {
|
||||
if (bonesInInv.length > 1) {
|
||||
console.log(`Burying ${bonesInInv.length} bones...`);
|
||||
}
|
||||
await buryAllBones(ctx, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pick up nearby bones (prioritize closest)
|
||||
const groundBones = ctx.sdk.getGroundItems()
|
||||
.filter(i => /^bones$/i.test(i.name))
|
||||
.filter(i => i.distance <= 5)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
if (groundBones.length > 0) {
|
||||
const bone = groundBones[0]!;
|
||||
const result = await ctx.bot.pickupItem(bone);
|
||||
if (result.success) {
|
||||
stats.bonesCollected++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a chicken to attack
|
||||
const chicken = findBestChicken(ctx);
|
||||
if (!chicken) {
|
||||
console.log('No chickens nearby - walking back inside coop...');
|
||||
await ctx.bot.walkTo(CHICKEN_COOP_INSIDE.x, CHICKEN_COOP_INSIDE.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attack the chicken
|
||||
const attackResult = await ctx.bot.attackNpc(chicken);
|
||||
if (!attackResult.success) {
|
||||
console.warn(`Attack failed: ${attackResult.message}`);
|
||||
// Try opening gate if blocked
|
||||
if (attackResult.reason === 'out_of_reach') {
|
||||
await ctx.bot.openDoor(/gate/i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for chicken to die
|
||||
await waitForCombatEnd(ctx, chicken, stats);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const username = `pr${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await prayerTrainingLoop(ctx);
|
||||
} finally {
|
||||
// Log final stats
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
const prayer = state.skills.find(s => s.name === 'Prayer');
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Prayer: Level ${prayer?.baseLevel ?? '?'} (${prayer?.experience ?? '?'} XP)`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 3 * 60 * 1000, // 3 minutes for testing
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,459 +0,0 @@
|
||||
/**
|
||||
* Ranged Trainer Script
|
||||
*
|
||||
* Goal: Train Ranged to level 10+ using the shortbow and bronze arrows from LUMBRIDGE_SPAWN.
|
||||
*
|
||||
* Strategy:
|
||||
* - Equip shortbow and bronze arrows
|
||||
* - Kill chickens at Lumbridge chicken coop (level 1, easy targets)
|
||||
* - Open gate to enter coop
|
||||
* - Pick up bronze arrows from loot to conserve ammo (with retry limits)
|
||||
* - Continue until Ranged level 10
|
||||
*
|
||||
* LUMBRIDGE_SPAWN preset includes:
|
||||
* - Shortbow (1)
|
||||
* - Bronze arrows (25)
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Training location - Lumbridge chicken coop (inside)
|
||||
const CHICKEN_COOP = { x: 3235, z: 3295 };
|
||||
|
||||
// Ranged attack style index (for shortbow, all styles train ranged)
|
||||
const RANGED_STYLE = 0; // Accurate
|
||||
|
||||
// Track combat statistics
|
||||
interface RangedStats {
|
||||
kills: number;
|
||||
arrowsUsed: number;
|
||||
arrowsRecovered: number;
|
||||
startXp: number;
|
||||
startTime: number;
|
||||
lastProgressTime: number;
|
||||
}
|
||||
|
||||
// Track failed pickup locations to avoid retrying (position -> retry count)
|
||||
const failedPickups = new Map<string, number>();
|
||||
const MAX_PICKUP_RETRIES = 2;
|
||||
|
||||
/**
|
||||
* Find the best chicken to attack.
|
||||
* Prioritize chickens that aren't in combat and are close.
|
||||
*/
|
||||
function findBestTarget(ctx: ScriptContext): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const targets = state.nearbyNpcs
|
||||
.filter(npc => /^chicken$/i.test(npc.name))
|
||||
.filter(npc => npc.options.some(o => /attack/i.test(o)))
|
||||
// Prefer NPCs not in combat
|
||||
.filter(npc => {
|
||||
if (npc.targetIndex === -1) return true;
|
||||
return !npc.inCombat;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Prefer NPCs not in combat
|
||||
if (a.inCombat !== b.inCombat) {
|
||||
return a.inCombat ? 1 : -1;
|
||||
}
|
||||
// Then by distance (but ranged can attack from further)
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
|
||||
return targets[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count bronze arrows in inventory + equipped
|
||||
*/
|
||||
function countArrows(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 0;
|
||||
|
||||
// Check inventory
|
||||
const invArrows = state.inventory
|
||||
.filter(i => /bronze arrow/i.test(i.name))
|
||||
.reduce((sum, i) => sum + i.count, 0);
|
||||
|
||||
// Check equipment (ammo slot is index 10)
|
||||
const equippedArrows = state.equipment
|
||||
.filter(e => /bronze arrow/i.test(e.name))
|
||||
.reduce((sum, e) => sum + e.count, 0);
|
||||
|
||||
return invArrows + equippedArrows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current ranged level
|
||||
*/
|
||||
function getRangedLevel(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
return state?.skills.find(s => s.name === 'Ranged')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current ranged XP
|
||||
*/
|
||||
function getRangedXp(ctx: ScriptContext): number {
|
||||
const state = ctx.sdk.getState();
|
||||
return state?.skills.find(s => s.name === 'Ranged')?.experience ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for combat to end (NPC dies or we need to heal)
|
||||
*/
|
||||
async function waitForCombatEnd(
|
||||
ctx: ScriptContext,
|
||||
targetNpc: NearbyNpc,
|
||||
stats: RangedStats
|
||||
): Promise<'kill' | 'fled' | 'lost_target' | 'need_heal'> {
|
||||
let combatStarted = false;
|
||||
let ticksSinceCombatEnded = 0;
|
||||
let loopCount = 0;
|
||||
|
||||
const startXp = getRangedXp(ctx);
|
||||
const maxWaitMs = 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Initial delay to let combat start
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
loopCount++;
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return 'lost_target';
|
||||
|
||||
// Check for dialog (level up, etc)
|
||||
if (state.dialog.isOpen) {
|
||||
console.log('Dismissing dialog during combat...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check XP gains as combat indicator
|
||||
const currentXp = getRangedXp(ctx);
|
||||
const xpGained = currentXp - startXp;
|
||||
if (xpGained > 0) {
|
||||
combatStarted = true;
|
||||
}
|
||||
|
||||
// Find our target NPC
|
||||
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
|
||||
|
||||
if (!target) {
|
||||
// NPC disappeared - count as kill if we gained XP
|
||||
if (combatStarted || xpGained > 0 || loopCount >= 2) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
// Check NPC health - if 0, it died
|
||||
if (target.maxHp > 0 && target.hp === 0) {
|
||||
stats.kills++;
|
||||
return 'kill';
|
||||
}
|
||||
|
||||
// Track combat via combatCycle
|
||||
const currentTick = state.tick;
|
||||
const npcInCombat = target.combatCycle > currentTick;
|
||||
const playerInCombat = state.player?.combat?.inCombat ?? false;
|
||||
const inActiveCombat = playerInCombat || npcInCombat || xpGained > 0;
|
||||
|
||||
if (inActiveCombat) {
|
||||
combatStarted = true;
|
||||
ticksSinceCombatEnded = 0;
|
||||
} else if (combatStarted) {
|
||||
ticksSinceCombatEnded++;
|
||||
if (ticksSinceCombatEnded >= 4) {
|
||||
return 'fled';
|
||||
}
|
||||
} else if (loopCount >= 8) {
|
||||
// Combat never started after ~4 seconds
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'lost_target';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log current statistics
|
||||
*/
|
||||
function logStats(ctx: ScriptContext, stats: RangedStats): void {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
const ranged = state.skills.find(s => s.name === 'Ranged');
|
||||
const currentXp = ranged?.experience ?? 0;
|
||||
const xpGained = currentXp - stats.startXp;
|
||||
const arrows = countArrows(ctx);
|
||||
|
||||
const elapsedMs = Date.now() - stats.startTime;
|
||||
const elapsedMinutes = Math.round(elapsedMs / 60_000);
|
||||
const xpPerHour = elapsedMs > 60_000 ? Math.round(xpGained / (elapsedMs / 3_600_000)) : 0;
|
||||
|
||||
console.log(`--- Stats after ${elapsedMinutes}m / ${stats.kills} kills ---`);
|
||||
console.log(`Ranged: Level ${ranged?.baseLevel} (${currentXp} XP, +${xpGained})`);
|
||||
console.log(`Arrows: ${arrows} remaining`);
|
||||
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main ranged training loop
|
||||
*/
|
||||
async function rangedTrainingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
const now = Date.now();
|
||||
const stats: RangedStats = {
|
||||
kills: 0,
|
||||
arrowsUsed: 0,
|
||||
arrowsRecovered: 0,
|
||||
startXp: getRangedXp(ctx),
|
||||
startTime: now,
|
||||
lastProgressTime: now,
|
||||
};
|
||||
|
||||
console.log('=== Ranged Trainer - Chicken Coop Strategy ===');
|
||||
console.log(`Goal: Reach Ranged level 10`);
|
||||
console.log(`Starting Ranged Level: ${getRangedLevel(ctx)}`);
|
||||
console.log(`Starting XP: ${stats.startXp}`);
|
||||
console.log(`Starting Arrows: ${countArrows(ctx)}`);
|
||||
|
||||
// Equip shortbow if not already equipped
|
||||
const shortbow = ctx.sdk.findInventoryItem(/shortbow/i);
|
||||
if (shortbow) {
|
||||
console.log('Equipping shortbow...');
|
||||
await ctx.bot.equipItem(shortbow);
|
||||
}
|
||||
|
||||
// Equip bronze arrows if not already equipped
|
||||
const arrows = ctx.sdk.findInventoryItem(/bronze arrow/i);
|
||||
if (arrows) {
|
||||
console.log('Equipping bronze arrows...');
|
||||
await ctx.bot.equipItem(arrows);
|
||||
}
|
||||
|
||||
// Set ranged attack style
|
||||
console.log('Setting ranged attack style...');
|
||||
await ctx.sdk.sendSetCombatStyle(RANGED_STYLE);
|
||||
|
||||
// Dismiss any blocking UI
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Walk to chicken coop and open gate if needed
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
|
||||
// // Open the gate to enter - retry if needed
|
||||
// for (let attempt = 0; attempt < 3; attempt++) {
|
||||
// const gateResult = await ctx.bot.openDoor(/gate/i);
|
||||
// if (gateResult.success) {
|
||||
// console.log('Opened gate to chicken coop');
|
||||
// await new Promise(r => setTimeout(r, 600)); // Wait for gate to open
|
||||
// break;
|
||||
// }
|
||||
// console.log(`Gate open attempt ${attempt + 1} failed, retrying...`);
|
||||
// await new Promise(r => setTimeout(r, 300));
|
||||
// }
|
||||
|
||||
// // Walk inside the coop
|
||||
// console.log('Walking inside coop...');
|
||||
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
|
||||
// Verify we're inside - if not, try again
|
||||
// const pos = ctx.sdk.getState();
|
||||
// if (pos && (pos.player?.worldX !== CHICKEN_COOP.x || pos.player?.worldZ !== CHICKEN_COOP.z)) {
|
||||
// console.log(`Not at target (${pos.player?.worldX}, ${pos.player?.worldZ}), retrying entry...`);
|
||||
// await ctx.bot.openDoor(/gate/i);
|
||||
// await new Promise(r => setTimeout(r, 600));
|
||||
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
// }
|
||||
|
||||
let lastStatsLog = 0;
|
||||
|
||||
// Main training loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we've reached level 10
|
||||
const rangedLevel = getRangedLevel(ctx);
|
||||
if (rangedLevel >= 10) {
|
||||
console.log(`*** GOAL REACHED: Ranged Level ${rangedLevel}! ***`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check arrows remaining
|
||||
const arrowCount = countArrows(ctx);
|
||||
if (arrowCount <= 0) {
|
||||
throw new Error('Out of arrows!');
|
||||
}
|
||||
|
||||
// Dismiss any blocking dialogs (level-up, etc) - critical to check frequently!
|
||||
if (currentState.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Log stats periodically (every 5 kills or 2 minutes)
|
||||
const timeSinceLastLog = Date.now() - lastStatsLog;
|
||||
const shouldLogByKills = stats.kills > 0 && stats.kills % 5 === 0 && stats.kills !== lastStatsLog;
|
||||
const shouldLogByTime = timeSinceLastLog >= 2 * 60_000;
|
||||
|
||||
if (shouldLogByKills || shouldLogByTime || lastStatsLog === 0) {
|
||||
lastStatsLog = stats.kills > 0 ? stats.kills : Date.now();
|
||||
logStats(ctx, stats);
|
||||
}
|
||||
|
||||
// Pick up arrows from the ground (conserve ammo!) - but only ones we can reach
|
||||
// Skip arrows that we've already failed to pick up too many times
|
||||
const groundArrows = ctx.sdk.getGroundItems()
|
||||
.filter(i => /bronze arrow/i.test(i.name))
|
||||
.filter(i => i.distance <= 4) // Only close ones to avoid fence issues
|
||||
.filter(i => {
|
||||
const key = `${i.x},${i.z}`;
|
||||
const retries = failedPickups.get(key) ?? 0;
|
||||
return retries < MAX_PICKUP_RETRIES;
|
||||
})
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
if (groundArrows.length > 0) {
|
||||
const arrowPile = groundArrows[0]!;
|
||||
const key = `${arrowPile.x},${arrowPile.z}`;
|
||||
console.log(`Picking up ${arrowPile.count ?? 1} bronze arrows at (${arrowPile.x}, ${arrowPile.z})...`);
|
||||
const result = await ctx.bot.pickupItem(arrowPile);
|
||||
if (result.success) {
|
||||
stats.arrowsRecovered += arrowPile.count ?? 1;
|
||||
// Clear retry count on success
|
||||
failedPickups.delete(key);
|
||||
// Re-equip arrows if they went to inventory
|
||||
const invArrows = ctx.sdk.findInventoryItem(/bronze arrow/i);
|
||||
if (invArrows) {
|
||||
await ctx.bot.equipItem(invArrows);
|
||||
}
|
||||
} else if (result.reason === 'cant_reach' || result.reason === 'timeout') {
|
||||
// Increment retry count for this position
|
||||
const retries = failedPickups.get(key) ?? 0;
|
||||
failedPickups.set(key, retries + 1);
|
||||
console.log(`Arrow pickup failed (retry ${retries + 1}/${MAX_PICKUP_RETRIES}): ${result.reason}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a chicken to attack
|
||||
const target = findBestTarget(ctx);
|
||||
if (!target) {
|
||||
console.log('No chickens nearby - walking to chicken coop...');
|
||||
// Try to open gate and enter
|
||||
await ctx.bot.openDoor(/gate/i);
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For ranged, we can attack from further away
|
||||
// Walk closer if target is too far (ranged has ~7 tile range)
|
||||
if (target.distance > 7) {
|
||||
console.log(`Chicken too far (${target.distance} tiles), walking to coop...`);
|
||||
// Try to open gate first if needed
|
||||
await ctx.bot.openDoor(/gate/i);
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
// Walk to coop
|
||||
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already fighting
|
||||
const playerCombat = currentState.player?.combat;
|
||||
if (playerCombat?.inCombat && playerCombat.targetIndex === target.index) {
|
||||
await waitForCombatEnd(ctx, target, stats);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dismiss any blocking UI (level-up dialogs, etc.) before attacking
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Attack the cow
|
||||
const arrowsBefore = countArrows(ctx);
|
||||
const attackResult = await ctx.bot.attackNpc(target);
|
||||
if (!attackResult.success) {
|
||||
console.warn(`Attack failed: ${attackResult.message}`);
|
||||
// If timeout, check for dialog that may have appeared during attack attempt
|
||||
if (attackResult.reason === 'timeout') {
|
||||
const s = ctx.sdk.getState();
|
||||
if (s?.dialog.isOpen) {
|
||||
console.log('Dismissing dialog that appeared during attack...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
// Try opening gate and re-entering if blocked
|
||||
// if (attackResult.reason === 'out_of_reach' || attackResult.reason === 'timeout') {
|
||||
// console.log('Cannot reach target - trying to enter coop...');
|
||||
// await ctx.bot.openDoor(/gate/i);
|
||||
// await new Promise(r => setTimeout(r, 400));
|
||||
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
||||
// }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for combat to complete
|
||||
const combatResult = await waitForCombatEnd(ctx, target, stats);
|
||||
|
||||
// Track arrows used
|
||||
const arrowsAfter = countArrows(ctx);
|
||||
if (arrowsAfter < arrowsBefore) {
|
||||
stats.arrowsUsed += arrowsBefore - arrowsAfter;
|
||||
}
|
||||
|
||||
if (combatResult === 'kill') {
|
||||
console.log(`Kill #${stats.kills}! (Ranged: Lv${getRangedLevel(ctx)}, Arrows: ${arrowsAfter})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const username = `rg${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await rangedTrainingLoop(ctx);
|
||||
} finally {
|
||||
// Log final stats
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
const ranged = state.skills.find(s => s.name === 'Ranged');
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Ranged: Level ${ranged?.baseLevel} (${ranged?.experience} XP)`);
|
||||
console.log(`Arrows remaining: ${countArrows(ctx)}`);
|
||||
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 30 * 60 * 1000, // 30 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -1,720 +0,0 @@
|
||||
/**
|
||||
* Smithing Training Script v4
|
||||
*
|
||||
* Goal: Train Smithing to level 10+ from a fresh Lumbridge spawn
|
||||
*
|
||||
* Strategy (v4):
|
||||
* 1. Sell shortbow for coins (toll money)
|
||||
* 2. Walk to SE Varrock mine copper/tin area (around 3289, 3363)
|
||||
* 3. Mine copper/tin ore (balanced)
|
||||
* 4. Walk to Al-Kharid and pay toll
|
||||
* 5. Smelt bronze bars at Al-Kharid furnace
|
||||
* 6. Repeat until level 10
|
||||
*
|
||||
* v4 Changes:
|
||||
* - Back to SE Varrock mine (has copper/tin, Al-Kharid has iron/gold/scorpions)
|
||||
* - Better copper/tin detection by checking specific rock locations
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Locations
|
||||
const LUMBRIDGE_SHOP = { x: 3211, z: 3247 };
|
||||
const AL_KHARID_GATE = { x: 3268, z: 3228 };
|
||||
const AL_KHARID_FURNACE = { x: 3274, z: 3186 };
|
||||
const AL_KHARID_INSIDE = { x: 3277, z: 3227 };
|
||||
|
||||
// SE Varrock mine - copper/tin area is in the central-east section
|
||||
// Copper rocks around (3287-3290, 3361-3366)
|
||||
// Tin rocks around (3282-3286, 3364-3368)
|
||||
const SE_VARROCK_MINE_COPPERTIN = { x: 3286, z: 3365 };
|
||||
|
||||
// Waypoints from Lumbridge to SE Varrock mine copper/tin area
|
||||
// Note: We use all waypoints to ensure we reach the correct spot
|
||||
const WAYPOINTS_TO_MINE = [
|
||||
{ x: 3222, z: 3250 }, // North of Lumbridge
|
||||
{ x: 3235, z: 3290 }, // Continue north
|
||||
{ x: 3255, z: 3320 }, // Approaching mine
|
||||
{ x: 3275, z: 3350 }, // Near mine entrance
|
||||
SE_VARROCK_MINE_COPPERTIN, // Final destination: 3286, 3365
|
||||
];
|
||||
|
||||
// Get smithing stats
|
||||
function getSmithingStats(ctx: ScriptContext): { level: number; xp: number } {
|
||||
const state = ctx.sdk.getState();
|
||||
const smithing = state?.skills.find(s => s.name === 'Smithing');
|
||||
return {
|
||||
level: smithing?.baseLevel ?? 1,
|
||||
xp: smithing?.experience ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Get current coin count
|
||||
function getCoins(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
// Check if inside Al Kharid
|
||||
function isInsideAlKharid(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
return (state?.player?.worldX ?? 0) >= 3270;
|
||||
}
|
||||
|
||||
// Count ore in inventory
|
||||
function countOre(ctx: ScriptContext): { copper: number; tin: number; bronzeBars: number } {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
return {
|
||||
copper: inv.filter(i => /copper ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
||||
tin: inv.filter(i => /tin ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
||||
bronzeBars: inv.filter(i => /bronze bar/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
||||
};
|
||||
}
|
||||
|
||||
// Get coins by selling items at Lumbridge shop
|
||||
async function getCoinsFromShop(ctx: ScriptContext): Promise<boolean> {
|
||||
console.log('Selling items for toll money...');
|
||||
|
||||
await ctx.bot.walkTo(LUMBRIDGE_SHOP.x, LUMBRIDGE_SHOP.z);
|
||||
|
||||
const openResult = await ctx.bot.openShop(/shop keeper/i);
|
||||
if (!openResult.success) {
|
||||
console.log(`Failed to open shop: ${openResult.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sell shortbow (worth ~20gp)
|
||||
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 1);
|
||||
if (sellResult.success) {
|
||||
console.log(sellResult.message);
|
||||
}
|
||||
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
const coins = getCoins(ctx);
|
||||
console.log(`Have ${coins}gp after selling`);
|
||||
return coins >= 10;
|
||||
}
|
||||
|
||||
// Walk to SE Varrock mine
|
||||
// The pathfinder should handle routing automatically
|
||||
async function walkToMine(ctx: ScriptContext): Promise<void> {
|
||||
console.log('Walking to SE Varrock mine...');
|
||||
|
||||
const currentPos = ctx.sdk.getState()?.player;
|
||||
console.log(`From (${currentPos?.worldX}, ${currentPos?.worldZ}) to (${SE_VARROCK_MINE_COPPERTIN.x}, ${SE_VARROCK_MINE_COPPERTIN.z})`);
|
||||
|
||||
// Try direct walk to mine
|
||||
let result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
|
||||
|
||||
if (!result.success) {
|
||||
console.log(`Direct walk failed: ${result.message}`);
|
||||
|
||||
// Try via an intermediate point (around Varrock east)
|
||||
console.log('Trying via Varrock east gate area...');
|
||||
const intermediate = { x: 3275, z: 3340 };
|
||||
result = await ctx.bot.walkTo(intermediate.x, intermediate.z);
|
||||
if (result.success) {
|
||||
result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dismiss any dialogs
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
const pos = ctx.sdk.getState()?.player;
|
||||
console.log(`At mine: (${pos?.worldX}, ${pos?.worldZ})`);
|
||||
}
|
||||
|
||||
// Prospect a rock to find what ore it contains
|
||||
async function prospectRock(ctx: ScriptContext, rock: { x: number; z: number; id: number; optionsWithIndex: Array<{ text: string; opIndex: number }> }): Promise<string | null> {
|
||||
const prospectOpt = rock.optionsWithIndex.find(o => /prospect/i.test(o.text));
|
||||
if (!prospectOpt) return null;
|
||||
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
|
||||
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, prospectOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800)); // Brief wait for action
|
||||
|
||||
try {
|
||||
const state = await ctx.sdk.waitForCondition(s => {
|
||||
for (const msg of s.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
if (text.includes('rock contains') || text.includes('ore') || text.includes('no ore') || text.includes('nothing')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, 3000); // Reduced from 5000
|
||||
|
||||
// Extract ore type from message
|
||||
for (const msg of state.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
if (text.includes('copper')) return 'copper';
|
||||
if (text.includes('tin')) return 'tin';
|
||||
if (text.includes('iron')) return 'iron';
|
||||
if (text.includes('gold')) return 'gold';
|
||||
if (text.includes('coal')) return 'coal';
|
||||
if (text.includes('no ore') || text.includes('nothing')) return 'empty';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find and mine a rock (copper or tin)
|
||||
// Use prospecting to filter out iron/gold, but don't try to balance
|
||||
async function mineRock(ctx: ScriptContext): Promise<boolean> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
|
||||
// Dismiss dialogs first
|
||||
if (state.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// Find rocks with Mine option - sort by distance
|
||||
const rocks = state.nearbyLocs.filter(loc =>
|
||||
/rocks?/i.test(loc.name) && !/rockslide/i.test(loc.name) &&
|
||||
loc.optionsWithIndex.some(o => /mine/i.test(o.text))
|
||||
).sort((a, b) => a.distance - b.distance);
|
||||
|
||||
if (rocks.length === 0) {
|
||||
console.log('No rocks nearby');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try prospecting and mining rocks
|
||||
for (const rock of rocks.slice(0, 5)) {
|
||||
// Prospect to check ore type
|
||||
const oreType = await prospectRock(ctx, rock);
|
||||
|
||||
// Skip non-copper/tin or empty rocks
|
||||
if (!oreType || oreType === 'empty') continue;
|
||||
if (oreType !== 'copper' && oreType !== 'tin') {
|
||||
console.log(`Skip ${oreType} rock`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mine the rock immediately after prospecting
|
||||
const mineOpt = rock.optionsWithIndex.find(o => /mine/i.test(o.text));
|
||||
if (!mineOpt) continue;
|
||||
|
||||
const xpBefore = ctx.sdk.getState()?.skills.find(s => s.name === 'Mining')?.experience ?? 0;
|
||||
const invCountBefore = ctx.sdk.getState()?.inventory.length ?? 0;
|
||||
|
||||
console.log(`Mining ${oreType} at (${rock.x}, ${rock.z})...`);
|
||||
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, mineOpt.opIndex);
|
||||
|
||||
// Wait for mining to complete
|
||||
try {
|
||||
await ctx.sdk.waitForCondition(state => {
|
||||
// Dismiss any dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
ctx.sdk.sendClickDialog(0).catch(() => {});
|
||||
}
|
||||
|
||||
// Check for XP gain or inventory change
|
||||
const miningXp = state.skills.find(s => s.name === 'Mining')?.experience ?? 0;
|
||||
return miningXp > xpBefore || state.inventory.length > invCountBefore;
|
||||
}, 8000);
|
||||
|
||||
const currentOre = countOre(ctx);
|
||||
console.log(`Mined! (${currentOre.copper} copper, ${currentOre.tin} tin)`);
|
||||
return true;
|
||||
} catch {
|
||||
console.log('Mining timeout');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mine ore until we have enough copper and tin
|
||||
async function mineOre(ctx: ScriptContext, targetPairs: number): Promise<void> {
|
||||
console.log(`Mining ${targetPairs} pairs of ore...`);
|
||||
|
||||
let consecutiveFails = 0;
|
||||
|
||||
while (consecutiveFails < 5) {
|
||||
const ore = countOre(ctx);
|
||||
const pairs = Math.min(ore.copper, ore.tin);
|
||||
|
||||
if (pairs >= targetPairs) {
|
||||
console.log(`Have ${pairs} ore pairs, done mining`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check inventory space
|
||||
const invCount = ctx.sdk.getState()?.inventory.length ?? 0;
|
||||
if (invCount >= 26) { // Leave some space
|
||||
console.log('Inventory nearly full');
|
||||
break;
|
||||
}
|
||||
|
||||
// Log current ore counts
|
||||
console.log(`Mining... (copper: ${ore.copper}, tin: ${ore.tin})`);
|
||||
|
||||
// Just mine any copper or tin we find - don't try to balance
|
||||
// The preference logic was causing issues where rocks respawn as different ore
|
||||
const result = await mineRock(ctx);
|
||||
|
||||
if (!result) {
|
||||
consecutiveFails++;
|
||||
console.log(`Mining failed (${consecutiveFails}/5)`);
|
||||
|
||||
// Walk around to find rocks
|
||||
const currentPos = ctx.sdk.getState()?.player;
|
||||
if (currentPos) {
|
||||
const offsetX = (Math.random() - 0.5) * 15;
|
||||
const offsetZ = (Math.random() - 0.5) * 15;
|
||||
await ctx.bot.walkTo(
|
||||
Math.round(SE_VARROCK_MINE_COPPERTIN.x + offsetX),
|
||||
Math.round(SE_VARROCK_MINE_COPPERTIN.z + offsetZ)
|
||||
);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
} else {
|
||||
consecutiveFails = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Pay toll and enter Al Kharid
|
||||
async function enterAlKharid(ctx: ScriptContext): Promise<boolean> {
|
||||
if (isInsideAlKharid(ctx)) {
|
||||
console.log('Already in Al Kharid');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Walking to toll gate...');
|
||||
await ctx.bot.walkTo(AL_KHARID_GATE.x, AL_KHARID_GATE.z);
|
||||
|
||||
// Find and interact with gate
|
||||
const state = ctx.sdk.getState();
|
||||
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 10);
|
||||
|
||||
if (!gate) {
|
||||
console.log('Gate not found!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const openOpt = gate.optionsWithIndex.find(o => /pay|open/i.test(o.text));
|
||||
if (!openOpt) {
|
||||
console.log('No open option on gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Interacting with gate...');
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Handle dialog - pay toll
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
continue;
|
||||
}
|
||||
|
||||
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Paying toll...');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Dismiss remaining dialogs
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) break;
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Walk through gate
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await ctx.bot.walkTo(AL_KHARID_INSIDE.x, AL_KHARID_INSIDE.z);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
if (isInsideAlKharid(ctx)) {
|
||||
console.log('Entered Al Kharid!');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return isInsideAlKharid(ctx);
|
||||
}
|
||||
|
||||
// Smelt bronze bars at furnace
|
||||
async function smeltBars(ctx: ScriptContext): Promise<number> {
|
||||
console.log('Walking to furnace...');
|
||||
await ctx.bot.walkTo(AL_KHARID_FURNACE.x, AL_KHARID_FURNACE.z);
|
||||
|
||||
let barsSmelted = 0;
|
||||
|
||||
while (true) {
|
||||
const ore = countOre(ctx);
|
||||
const pairs = Math.min(ore.copper, ore.tin);
|
||||
|
||||
if (pairs === 0) {
|
||||
console.log('No more ore pairs');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any dialogs
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find furnace
|
||||
const furnace = state?.nearbyLocs.find(l => /furnace/i.test(l.name));
|
||||
if (!furnace) {
|
||||
console.log('Furnace not found');
|
||||
break;
|
||||
}
|
||||
|
||||
// Find copper ore in inventory
|
||||
const copper = ctx.sdk.findInventoryItem(/copper ore/i);
|
||||
if (!copper) {
|
||||
console.log('No copper ore');
|
||||
break;
|
||||
}
|
||||
|
||||
const smithingXpBefore = getSmithingStats(ctx).xp;
|
||||
|
||||
// Use copper ore on furnace
|
||||
console.log('Using ore on furnace...');
|
||||
await ctx.sdk.sendUseItemOnLoc(copper.slot, furnace.x, furnace.z, furnace.id);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Handle the smelting interface/dialog
|
||||
// The smelting interface should appear - click to make bronze bar
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
|
||||
// Check if dialog opened (smelting menu)
|
||||
if (s?.dialog.isOpen) {
|
||||
// Look for bronze bar option
|
||||
const bronzeOpt = s.dialog.options.find(o => /bronze/i.test(o.text));
|
||||
if (bronzeOpt) {
|
||||
console.log('Selecting bronze bar...');
|
||||
await ctx.sdk.sendClickDialog(bronzeOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
// Look for "Make" or "Ok" button
|
||||
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
|
||||
if (makeOpt) {
|
||||
await ctx.sdk.sendClickDialog(makeOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// Wait for smelting to complete (XP gain)
|
||||
try {
|
||||
await ctx.sdk.waitForCondition(state => {
|
||||
if (state.dialog.isOpen) {
|
||||
ctx.sdk.sendClickDialog(0).catch(() => {});
|
||||
}
|
||||
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
|
||||
return smithingXp > smithingXpBefore;
|
||||
}, 10000);
|
||||
|
||||
barsSmelted++;
|
||||
console.log(`Smelted bar #${barsSmelted}`);
|
||||
} catch {
|
||||
console.log('Smelting timeout');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
return barsSmelted;
|
||||
}
|
||||
|
||||
// Find and use anvil to smith items
|
||||
async function smithItems(ctx: ScriptContext): Promise<number> {
|
||||
// Check for anvil near furnace first
|
||||
let anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
||||
|
||||
if (!anvil) {
|
||||
// Try walking around Al Kharid to find anvil
|
||||
console.log('Looking for anvil in Al Kharid...');
|
||||
|
||||
// Known Al Kharid anvil location (near general store)
|
||||
const alKharidAnvil = { x: 3290, z: 3179 };
|
||||
await ctx.bot.walkTo(alKharidAnvil.x, alKharidAnvil.z);
|
||||
|
||||
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
||||
}
|
||||
|
||||
if (!anvil) {
|
||||
console.log('No anvil found, trying Varrock...');
|
||||
// Walk to Varrock west anvil
|
||||
await ctx.bot.walkTo(3188, 3427);
|
||||
|
||||
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
||||
}
|
||||
|
||||
if (!anvil) {
|
||||
console.log('Cannot find anvil!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`Found anvil at (${anvil.x}, ${anvil.z})`);
|
||||
|
||||
// Check for hammer
|
||||
const hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
||||
if (!hammer) {
|
||||
console.log('No hammer in inventory!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
let itemsSmithed = 0;
|
||||
|
||||
while (true) {
|
||||
const ore = countOre(ctx);
|
||||
if (ore.bronzeBars === 0) {
|
||||
console.log('No bronze bars left');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any dialogs
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find bronze bar
|
||||
const bar = ctx.sdk.findInventoryItem(/bronze bar/i);
|
||||
if (!bar) {
|
||||
console.log('No bronze bar');
|
||||
break;
|
||||
}
|
||||
|
||||
const smithingXpBefore = getSmithingStats(ctx).xp;
|
||||
|
||||
// Use bar on anvil
|
||||
console.log('Using bar on anvil...');
|
||||
await ctx.sdk.sendUseItemOnLoc(bar.slot, anvil.x, anvil.z, anvil.id);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Handle smithing interface
|
||||
// Smithing interface (id 994) uses iop not buttonType
|
||||
// We need to click a specific component to make items
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
|
||||
// Check dialog
|
||||
if (s?.dialog.isOpen) {
|
||||
// Look for bronze dagger (1 bar, level 1)
|
||||
const daggerOpt = s.dialog.options.find(o => /dagger/i.test(o.text));
|
||||
if (daggerOpt) {
|
||||
console.log('Selecting dagger...');
|
||||
await ctx.sdk.sendClickDialog(daggerOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
// Try clicking first "Make" or "Ok" option
|
||||
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
|
||||
if (makeOpt) {
|
||||
await ctx.sdk.sendClickDialog(makeOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
// Try first option
|
||||
if (s.dialog.options.length > 0) {
|
||||
await ctx.sdk.sendClickDialog(s.dialog.options[0]?.index ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check interface state
|
||||
if (s?.interface?.isOpen) {
|
||||
console.log(`Interface open: ${s.interface.interfaceId}, options: ${s.interface.options.map(o => o.text).join(', ')}`);
|
||||
|
||||
// Try clicking first option
|
||||
if (s.interface.options.length > 0) {
|
||||
await ctx.sdk.sendClickInterfaceOption(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// Wait for smithing (XP gain)
|
||||
try {
|
||||
await ctx.sdk.waitForCondition(state => {
|
||||
if (state.dialog.isOpen) {
|
||||
ctx.sdk.sendClickDialog(0).catch(() => {});
|
||||
}
|
||||
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
|
||||
return smithingXp > smithingXpBefore;
|
||||
}, 10000);
|
||||
|
||||
itemsSmithed++;
|
||||
console.log(`Smithed item #${itemsSmithed}`);
|
||||
} catch {
|
||||
console.log('Smithing timeout');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
return itemsSmithed;
|
||||
}
|
||||
|
||||
// Drop items to make space
|
||||
async function dropItems(ctx: ScriptContext, pattern: RegExp): Promise<void> {
|
||||
const items = ctx.sdk.getState()?.inventory.filter(i => pattern.test(i.name)) ?? [];
|
||||
for (const item of items) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Equip pickaxe if not already equipped
|
||||
async function equipPickaxe(ctx: ScriptContext): Promise<boolean> {
|
||||
const equipped = ctx.sdk.findEquipmentItem(/pickaxe/i);
|
||||
if (equipped) {
|
||||
console.log('Pickaxe already equipped');
|
||||
return true;
|
||||
}
|
||||
|
||||
const pickaxe = ctx.sdk.findInventoryItem(/pickaxe/i);
|
||||
if (!pickaxe) {
|
||||
console.log('No pickaxe in inventory!');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Equipping ${pickaxe.name}...`);
|
||||
const result = await ctx.bot.equipItem(pickaxe);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
const username = `sm${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
|
||||
console.log('=== Smithing Training Script v2 ===');
|
||||
|
||||
const startStats = getSmithingStats(ctx);
|
||||
console.log(`Starting Smithing: Level ${startStats.level} (${startStats.xp} XP)`);
|
||||
|
||||
// Equip pickaxe first
|
||||
await equipPickaxe(ctx);
|
||||
|
||||
// Check for hammer (needed for smithing at anvil)
|
||||
let hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
||||
if (!hammer) {
|
||||
console.log('No hammer - will only be able to smelt bars');
|
||||
// Smelting still gives XP, so we can train without a hammer
|
||||
}
|
||||
|
||||
// Main loop
|
||||
let iterations = 0;
|
||||
while (iterations < 20) { // Safety limit
|
||||
iterations++;
|
||||
const stats = getSmithingStats(ctx);
|
||||
|
||||
if (stats.level >= 10) {
|
||||
console.log(`Goal achieved! Smithing level ${stats.level}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`\n=== Iteration ${iterations}: Smithing Level ${stats.level} (${stats.xp} XP) ===`);
|
||||
|
||||
// Step 1: Get coins if we don't have enough (for toll)
|
||||
if (getCoins(ctx) < 10 && !isInsideAlKharid(ctx)) {
|
||||
await getCoinsFromShop(ctx);
|
||||
}
|
||||
|
||||
// Step 2: Mine ore if we don't have enough
|
||||
const ore = countOre(ctx);
|
||||
console.log(`Inventory: ${ore.copper} copper, ${ore.tin} tin, ${ore.bronzeBars} bars`);
|
||||
|
||||
const pairs = Math.min(ore.copper, ore.tin);
|
||||
if (pairs < 5) {
|
||||
await walkToMine(ctx);
|
||||
await mineOre(ctx, 8); // Mine 8 pairs
|
||||
}
|
||||
|
||||
// Step 3: Travel to Al-Kharid for smelting
|
||||
if (!isInsideAlKharid(ctx)) {
|
||||
const entered = await enterAlKharid(ctx);
|
||||
if (!entered) {
|
||||
console.log('Failed to enter Al Kharid, retrying...');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Smelt bars
|
||||
const oreAfterMining = countOre(ctx);
|
||||
const pairsToSmelt = Math.min(oreAfterMining.copper, oreAfterMining.tin);
|
||||
if (pairsToSmelt > 0) {
|
||||
console.log(`Smelting ${pairsToSmelt} bronze bars...`);
|
||||
const smelted = await smeltBars(ctx);
|
||||
console.log(`Smelted ${smelted} bars`);
|
||||
}
|
||||
|
||||
// Step 5: Smith items (if we have a hammer)
|
||||
hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
||||
const barsCount = countOre(ctx);
|
||||
if (hammer && barsCount.bronzeBars > 0) {
|
||||
console.log(`Smithing with ${barsCount.bronzeBars} bars...`);
|
||||
const smithed = await smithItems(ctx);
|
||||
console.log(`Smithed ${smithed} items`);
|
||||
}
|
||||
|
||||
// Drop smithed items to make inventory space
|
||||
await dropItems(ctx, /bronze (dagger|sword|axe|mace|med helm|bolts)/i);
|
||||
}
|
||||
|
||||
const endStats = getSmithingStats(ctx);
|
||||
console.log(`\n=== Complete: Level ${endStats.level} (${endStats.xp} XP) ===`);
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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)
|
||||
|
||||
@@ -1,639 +0,0 @@
|
||||
/**
|
||||
* Thieving GP Maximizer Script
|
||||
*
|
||||
* Goal: Maximize coins earned through thieving in 5 minutes.
|
||||
*
|
||||
* Strategy v11:
|
||||
* - Level 1-9: Pickpocket Men at Lumbridge (3 GP each)
|
||||
* - Level 10+: Walk to Farmers (9 GP each) - proven strategy from v7
|
||||
* - Larger zone radius (40 tiles) to avoid constant zone re-entry
|
||||
* - Walk TO Farmer spawn when no target found (not random)
|
||||
*
|
||||
* v7: 339 GP (Farmers with door handling) - BASELINE
|
||||
* v8: 206 GP (REGRESSION - Men in Al-Kharid)
|
||||
* v9: 36 GP (FAILURE - Warriors not found)
|
||||
* v10: 195 GP (REGRESSION - constant zone re-walking)
|
||||
* v11 Target: 350+ GP with Farmers only, optimized zone handling
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
import type { NearbyNpc } from '../../sdk/types';
|
||||
|
||||
// Thieving stats tracking
|
||||
interface ThievingStats {
|
||||
pickpocketAttempts: number;
|
||||
successfulPickpockets: number;
|
||||
failedPickpockets: number;
|
||||
coinsEarned: number;
|
||||
startCoins: number;
|
||||
startThievingXp: number;
|
||||
foodEaten: number;
|
||||
stunTimeMs: number;
|
||||
}
|
||||
|
||||
// Stun duration in ticks (~4.5 seconds = 7-8 game ticks)
|
||||
const STUN_TICKS = 8;
|
||||
const STUN_MS = 4800;
|
||||
|
||||
// v10: Simple zone switching - Men then Farmers (proven reliable)
|
||||
const ZONES = {
|
||||
lumbridge: { x: 3222, z: 3218, targetPattern: /^(man|woman)$/i, levelReq: 1, coins: 3 },
|
||||
// Farmer area near crop patches (proven in v7: 339 GP)
|
||||
farmers: { x: 3167, z: 3283, targetPattern: /^farmer$/i, levelReq: 10, coins: 9 },
|
||||
};
|
||||
|
||||
// Key locations
|
||||
const LOCATIONS = {
|
||||
lumbridgeGeneralStore: { x: 3211, z: 3247 },
|
||||
alKharidTollGate: { x: 3268, z: 3228 },
|
||||
alKharidInside: { x: 3277, z: 3227 },
|
||||
kebabSeller: { x: 3273, z: 3180 }, // Karim the kebab seller (dialog, not shop)
|
||||
};
|
||||
|
||||
// State tracking for one-time setup
|
||||
interface SetupState {
|
||||
soldShortbow: boolean;
|
||||
paidToll: boolean;
|
||||
boughtKebabs: boolean;
|
||||
tollAttempted: boolean; // Track if we've tried to enter Al-Kharid
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to destination, opening any doors/gates encountered along the way
|
||||
*/
|
||||
async function walkToWithDoors(ctx: ScriptContext, destX: number, destZ: number, maxAttempts: number = 5): Promise<boolean> {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return false;
|
||||
|
||||
// Check if we're close enough
|
||||
const dx = Math.abs(state.player.worldX - destX);
|
||||
const dz = Math.abs(state.player.worldZ - destZ);
|
||||
if (dx <= 5 && dz <= 5) {
|
||||
return true; // Arrived!
|
||||
}
|
||||
|
||||
// Try walking
|
||||
const walkResult = await ctx.bot.walkTo(destX, destZ);
|
||||
|
||||
// Check if we made progress
|
||||
const stateAfter = ctx.sdk.getState();
|
||||
if (stateAfter?.player) {
|
||||
const dxAfter = Math.abs(stateAfter.player.worldX - destX);
|
||||
const dzAfter = Math.abs(stateAfter.player.worldZ - destZ);
|
||||
if (dxAfter <= 5 && dzAfter <= 5) {
|
||||
return true; // Arrived!
|
||||
}
|
||||
}
|
||||
|
||||
// If walk failed or we're stuck, look for a door/gate to open
|
||||
const doors = ctx.sdk.getNearbyLocs()
|
||||
.filter(loc => /door|gate/i.test(loc.name))
|
||||
.filter(loc => loc.options.some(o => /^open$/i.test(o)))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
if (doors.length > 0) {
|
||||
const door = doors[0]!;
|
||||
console.log(`Opening ${door.name} at (${door.x}, ${door.z})...`);
|
||||
const openResult = await ctx.bot.openDoor(door);
|
||||
if (openResult.success) {
|
||||
console.log(`Opened ${door.name}`);
|
||||
continue; // Try walking again
|
||||
} else {
|
||||
console.warn(`Failed to open ${door.name}: ${openResult.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no doors found and walk failed, we might be stuck
|
||||
if (!walkResult.success) {
|
||||
console.warn(`Walk failed: ${walkResult.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in Al-Kharid (x >= 3270)
|
||||
*/
|
||||
function isInAlKharid(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
return (state?.player?.worldX ?? 0) >= 3270;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sell shortbow at Lumbridge general store for 20gp (for toll + extra)
|
||||
* From script_best_practices.md: shortbow sells for 20gp, better than bronze sword (10gp)
|
||||
*/
|
||||
async function sellShortbow(ctx: ScriptContext): Promise<boolean> {
|
||||
const shortbow = ctx.sdk.findInventoryItem(/^shortbow$/i);
|
||||
if (!shortbow) {
|
||||
console.log('No shortbow to sell');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Selling shortbow for toll money (20gp)...');
|
||||
|
||||
// Walk to general store
|
||||
await walkToWithDoors(ctx, LOCATIONS.lumbridgeGeneralStore.x, LOCATIONS.lumbridgeGeneralStore.z);
|
||||
|
||||
// Open shop
|
||||
const shopResult = await ctx.bot.openShop(/shop keeper|shop assistant/i);
|
||||
if (!shopResult.success) {
|
||||
console.warn(`Failed to open shop: ${shopResult.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sell the shortbow
|
||||
const sellResult = await ctx.bot.sellToShop(/shortbow/i);
|
||||
if (!sellResult.success) {
|
||||
console.warn(`Failed to sell shortbow: ${sellResult.message}`);
|
||||
}
|
||||
|
||||
// Close shop
|
||||
await ctx.bot.closeShop();
|
||||
|
||||
console.log('Sold shortbow for 20gp');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay toll and enter Al-Kharid (from script_best_practices.md)
|
||||
*/
|
||||
async function payTollAndEnterAlKharid(ctx: ScriptContext): Promise<boolean> {
|
||||
const coins = countCoins(ctx);
|
||||
if (coins < 10) {
|
||||
console.warn(`Not enough coins for toll (have ${coins}, need 10)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Walking to Al-Kharid toll gate...');
|
||||
await walkToWithDoors(ctx, LOCATIONS.alKharidTollGate.x, LOCATIONS.alKharidTollGate.z);
|
||||
|
||||
// Find and click the gate
|
||||
const state = ctx.sdk.getState();
|
||||
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name));
|
||||
if (!gate) {
|
||||
console.warn('Cannot find toll gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
const openOpt = gate.optionsWithIndex.find(o => /open|pay/i.test(o.text));
|
||||
if (!openOpt) {
|
||||
console.warn('Cannot find open option on gate');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Opening toll gate...');
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Handle dialog - click through until "Yes" option appears
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
continue;
|
||||
}
|
||||
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (yesOpt) {
|
||||
console.log('Paying 10gp toll...');
|
||||
await ctx.sdk.sendClickDialog(yesOpt.index);
|
||||
break;
|
||||
}
|
||||
await ctx.sdk.sendClickDialog(0); // Click to continue
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Dismiss any remaining dialogs
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) break;
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Wait and walk through aggressively
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// Check if already in
|
||||
if (isInAlKharid(ctx)) {
|
||||
console.log('Entered Al-Kharid!');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Walking through toll gate (attempt ${i + 1})...`);
|
||||
await ctx.bot.walkTo(LOCATIONS.alKharidInside.x, LOCATIONS.alKharidInside.z);
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
}
|
||||
|
||||
return isInAlKharid(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy kebabs from Al-Kharid kebab seller (uses DIALOG, not shop!)
|
||||
* From script_best_practices.md - Karim uses dialog system
|
||||
*/
|
||||
async function buyKebabs(ctx: ScriptContext, quantity: number = 5): Promise<boolean> {
|
||||
console.log(`Buying ${quantity} kebabs via dialog...`);
|
||||
|
||||
// Walk to kebab seller (3273, 3180)
|
||||
await walkToWithDoors(ctx, 3273, 3180);
|
||||
|
||||
const kebabsBefore = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
// Check if we have enough coins (1gp per kebab)
|
||||
const coins = countCoins(ctx);
|
||||
if (coins < 1) {
|
||||
console.warn('Not enough coins to buy kebab');
|
||||
break;
|
||||
}
|
||||
|
||||
// Find kebab seller (Karim)
|
||||
const seller = ctx.sdk.findNearbyNpc(/kebab/i);
|
||||
if (!seller) {
|
||||
console.warn('Cannot find kebab seller');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Talk to kebab seller
|
||||
const talkOpt = seller.optionsWithIndex.find(o => /talk/i.test(o.text));
|
||||
if (!talkOpt) {
|
||||
console.warn('Cannot find talk option on kebab seller');
|
||||
return false;
|
||||
}
|
||||
|
||||
await ctx.sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Handle dialog - click through and select "Yes please."
|
||||
let bought = false;
|
||||
for (let j = 0; j < 15; j++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
const buyOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
||||
if (buyOpt) {
|
||||
await ctx.sdk.sendClickDialog(buyOpt.index); // Buy kebab (1gp)
|
||||
bought = true;
|
||||
} else {
|
||||
await ctx.sdk.sendClickDialog(0); // Click to continue
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
// Check if dialog closed after buying
|
||||
if (bought) {
|
||||
const sAfter = ctx.sdk.getState();
|
||||
if (!sAfter?.dialog.isOpen) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss any remaining dialogs
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const s = ctx.sdk.getState();
|
||||
if (!s?.dialog.isOpen) break;
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
const kebabsAfter = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
|
||||
const bought = kebabsAfter - kebabsBefore;
|
||||
console.log(`Bought ${bought} kebabs (now have ${kebabsAfter})`);
|
||||
return bought > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the best zone for current thieving level
|
||||
* v10: Simple - Men then Farmers (proven reliable from v7)
|
||||
*/
|
||||
function getBestZone(thievingLevel: number) {
|
||||
// Level 10+: Farmers (9 GP) - proven in v7 with 339 GP
|
||||
if (thievingLevel >= ZONES.farmers.levelReq) {
|
||||
return ZONES.farmers;
|
||||
}
|
||||
// Level 1-9: Lumbridge Men (3 GP)
|
||||
return ZONES.lumbridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in a zone
|
||||
* v11: Larger radius (40 tiles) to avoid constant zone re-entry
|
||||
*/
|
||||
function isInZone(ctx: ScriptContext, zone: typeof ZONES.lumbridge): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return false;
|
||||
const dx = Math.abs(state.player.worldX - zone.x);
|
||||
const dz = Math.abs(state.player.worldZ - zone.z);
|
||||
return dx <= 40 && dz <= 40; // Large radius - stay in zone once entered
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearest pickpocket target matching the pattern
|
||||
*/
|
||||
function findPickpocketTarget(ctx: ScriptContext, pattern: RegExp): NearbyNpc | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
const targets = state.nearbyNpcs
|
||||
.filter(npc => pattern.test(npc.name))
|
||||
.filter(npc => npc.options.some(o => /pickpocket/i.test(o)))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return targets[0] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if we should eat food based on HP
|
||||
*/
|
||||
function shouldEat(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return false;
|
||||
|
||||
const hp = state.skills.find(s => s.name === 'Hitpoints');
|
||||
if (!hp) return false;
|
||||
|
||||
// v6: Be more conservative - only eat at HP <= 3 to preserve food
|
||||
// Success rate improves with thieving level, so later attempts are safer
|
||||
return hp.level <= 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have food available
|
||||
*/
|
||||
function hasFood(ctx: ScriptContext): boolean {
|
||||
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
|
||||
return food !== null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Count coins in inventory
|
||||
*/
|
||||
function countCoins(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to pickpocket an NPC
|
||||
* Returns result and coins gained
|
||||
*/
|
||||
async function attemptPickpocket(
|
||||
ctx: ScriptContext,
|
||||
target: NearbyNpc,
|
||||
stats: ThievingStats
|
||||
): Promise<'success' | 'failed' | 'stunned' | 'error'> {
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
const coinsBefore = countCoins(ctx);
|
||||
|
||||
// Find the Pickpocket option
|
||||
const pickpocketOpt = target.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
|
||||
if (!pickpocketOpt) {
|
||||
console.warn(`No pickpocket option on ${target.name}`);
|
||||
return 'error';
|
||||
}
|
||||
|
||||
// Send the pickpocket action
|
||||
const result = await ctx.sdk.sendInteractNpc(target.index, pickpocketOpt.opIndex);
|
||||
if (!result.success) {
|
||||
console.warn(`Pickpocket action failed: ${result.message}`);
|
||||
return 'error';
|
||||
}
|
||||
|
||||
stats.pickpocketAttempts++;
|
||||
|
||||
// Wait for result - either coins increase or we get stunned
|
||||
// Check for game messages about success/failure
|
||||
try {
|
||||
await ctx.sdk.waitForCondition(state => {
|
||||
// Check for success message or coins increasing
|
||||
for (const msg of state.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
// Success messages
|
||||
if (text.includes('you pick') || text.includes("you steal")) {
|
||||
return true;
|
||||
}
|
||||
// Failure/stun messages
|
||||
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if coins increased
|
||||
const currentCoins = state.inventory.find(i => /^coins$/i.test(i.name))?.count ?? 0;
|
||||
if (currentCoins > coinsBefore) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 5000);
|
||||
|
||||
// Check what happened
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) return 'error';
|
||||
|
||||
const coinsAfter = countCoins(ctx);
|
||||
if (coinsAfter > coinsBefore) {
|
||||
// Success!
|
||||
const gained = coinsAfter - coinsBefore;
|
||||
stats.successfulPickpockets++;
|
||||
stats.coinsEarned += gained;
|
||||
console.log(`Pickpocketed ${target.name}! +${gained} coins (total: ${stats.coinsEarned})`);
|
||||
return 'success';
|
||||
}
|
||||
|
||||
// Check for failure message
|
||||
for (const msg of currentState.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
|
||||
stats.failedPickpockets++;
|
||||
console.log(`Failed pickpocket - stunned for ${STUN_MS}ms`);
|
||||
// Wait out the stun duration
|
||||
await new Promise(r => setTimeout(r, STUN_MS));
|
||||
stats.stunTimeMs += STUN_MS;
|
||||
return 'stunned';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown result
|
||||
return 'failed';
|
||||
} catch {
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thieving loop
|
||||
*/
|
||||
async function thievingLoop(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) throw new Error('No initial state');
|
||||
|
||||
// Initialize stats
|
||||
const stats: ThievingStats = {
|
||||
pickpocketAttempts: 0,
|
||||
successfulPickpockets: 0,
|
||||
failedPickpockets: 0,
|
||||
coinsEarned: 0,
|
||||
startCoins: countCoins(ctx),
|
||||
startThievingXp: state.skills.find(s => s.name === 'Thieving')?.experience ?? 0,
|
||||
foodEaten: 0,
|
||||
stunTimeMs: 0,
|
||||
};
|
||||
|
||||
// v8: Setup state for Al-Kharid access
|
||||
const setup: SetupState = {
|
||||
soldShortbow: false,
|
||||
paidToll: false,
|
||||
boughtKebabs: false,
|
||||
tollAttempted: false,
|
||||
};
|
||||
|
||||
console.log('=== Thieving GP Maximizer v11 Started ===');
|
||||
console.log(`Starting coins: ${stats.startCoins}`);
|
||||
console.log(`Starting thieving XP: ${stats.startThievingXp}`);
|
||||
|
||||
// Main loop
|
||||
while (true) {
|
||||
const currentState = ctx.sdk.getState();
|
||||
if (!currentState) {
|
||||
console.warn('Lost game state');
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any dialogs first
|
||||
if (currentState.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check HP and eat if needed (conservative - only eat at HP <= 3)
|
||||
if (shouldEat(ctx)) {
|
||||
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
|
||||
if (food) {
|
||||
console.log(`HP low - eating ${food.name}`);
|
||||
await ctx.bot.eatFood(food);
|
||||
stats.foodEaten++;
|
||||
continue;
|
||||
}
|
||||
// No food - if in Al-Kharid, try to buy more kebabs
|
||||
if (setup.paidToll && isInAlKharid(ctx)) {
|
||||
const coins = countCoins(ctx);
|
||||
if (coins >= 5) {
|
||||
console.log('Out of food in Al-Kharid - buying more kebabs...');
|
||||
await buyKebabs(ctx, 5);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// No food and can't buy - continue anyway
|
||||
}
|
||||
|
||||
// v10: Simple zone switching based on level
|
||||
// Level 1-9: Men (3 GP), Level 10+: Farmers (9 GP)
|
||||
const thievingLevel = currentState.skills.find(s => s.name === 'Thieving')?.baseLevel ?? 1;
|
||||
|
||||
// Determine best zone based on level (no setup needed!)
|
||||
const bestZone = getBestZone(thievingLevel);
|
||||
|
||||
// Move to best zone if not there
|
||||
if (!isInZone(ctx, bestZone)) {
|
||||
const zoneName = bestZone === ZONES.farmers ? 'Farmers' : 'Lumbridge';
|
||||
console.log(`Level ${thievingLevel}: Moving to ${zoneName} (${bestZone.coins} GP each)...`);
|
||||
const arrived = await walkToWithDoors(ctx, bestZone.x, bestZone.z);
|
||||
if (!arrived) {
|
||||
console.warn('Failed to reach zone, trying again...');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find target in current zone
|
||||
const target = findPickpocketTarget(ctx, bestZone.targetPattern);
|
||||
|
||||
if (!target) {
|
||||
// No targets nearby - walk TO the zone center (not random)
|
||||
// This is where the target spawns, so walking there helps
|
||||
console.log(`No targets nearby - walking to ${bestZone === ZONES.farmers ? 'Farmer spawn' : 'zone'}...`);
|
||||
await ctx.bot.walkTo(bestZone.x, bestZone.z);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt pickpocket
|
||||
const result = await attemptPickpocket(ctx, target, stats);
|
||||
|
||||
// Log periodic stats every 10 attempts
|
||||
if (stats.pickpocketAttempts % 10 === 0 && stats.pickpocketAttempts > 0) {
|
||||
logStats(ctx, stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log current thieving statistics
|
||||
*/
|
||||
function logStats(ctx: ScriptContext, stats: ThievingStats): void {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
const currentThievingXp = state.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
|
||||
const xpGained = currentThievingXp - stats.startThievingXp;
|
||||
const successRate = stats.pickpocketAttempts > 0
|
||||
? ((stats.successfulPickpockets / stats.pickpocketAttempts) * 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
console.log(`--- Stats after ${stats.pickpocketAttempts} attempts ---`);
|
||||
console.log(`Success rate: ${successRate}% (${stats.successfulPickpockets}/${stats.pickpocketAttempts})`);
|
||||
console.log(`Coins earned: ${stats.coinsEarned} GP`);
|
||||
console.log(`Thieving XP gained: +${xpGained}`);
|
||||
console.log(`Food eaten: ${stats.foodEaten}`);
|
||||
console.log(`Time stunned: ${(stats.stunTimeMs / 1000).toFixed(1)}s`);
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
const username = `tg${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
try {
|
||||
await thievingLoop(ctx);
|
||||
} finally {
|
||||
// Log final stats
|
||||
const state = ctx.sdk.getState();
|
||||
if (state) {
|
||||
const startThievingXp = 0; // We start at 0
|
||||
const thieving = state.skills.find(s => s.name === 'Thieving');
|
||||
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
||||
|
||||
console.log('=== Final Results ===');
|
||||
console.log(`Thieving: Level ${thieving?.baseLevel ?? 1} (${thieving?.experience ?? 0} XP)`);
|
||||
console.log(`Total coins in inventory: ${coins?.count ?? 0}`);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* Thieving Training Script v1
|
||||
*
|
||||
* Goal: Train Thieving to level 10+ from a fresh Lumbridge spawn
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Find Men/Women NPCs in Lumbridge
|
||||
* 2. Pickpocket them repeatedly
|
||||
* 3. Handle stuns (brief delay after failed pickpocket)
|
||||
* 4. Continue until level 10
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Get thieving stats
|
||||
function getThievingStats(ctx: ScriptContext): { level: number; xp: number } {
|
||||
const state = ctx.sdk.getState();
|
||||
const thieving = state?.skills.find(s => s.name === 'Thieving');
|
||||
return {
|
||||
level: thieving?.baseLevel ?? 1,
|
||||
xp: thieving?.experience ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Find a Man or Woman NPC to pickpocket
|
||||
function findTarget(ctx: ScriptContext): { npc: { index: number; name: string; optionsWithIndex: Array<{ text: string; opIndex: number }> }; pickpocketOpt: { text: string; opIndex: number } } | null {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
// Look for Man or Woman NPCs
|
||||
for (const npc of state.nearbyNpcs) {
|
||||
if (/^(man|woman)$/i.test(npc.name)) {
|
||||
const pickpocketOpt = npc.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
|
||||
if (pickpocketOpt) {
|
||||
return { npc, pickpocketOpt };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pickpocket a target NPC
|
||||
async function pickpocket(ctx: ScriptContext): Promise<boolean> {
|
||||
const target = findTarget(ctx);
|
||||
if (!target) {
|
||||
console.log('No pickpocket target found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const { npc, pickpocketOpt } = target;
|
||||
const xpBefore = getThievingStats(ctx).xp;
|
||||
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
||||
|
||||
console.log(`Pickpocketing ${npc.name}...`);
|
||||
await ctx.sdk.sendInteractNpc(npc.index, pickpocketOpt.opIndex);
|
||||
|
||||
// Wait for XP gain (success) or stun message (failure)
|
||||
try {
|
||||
const finalState = await ctx.sdk.waitForCondition(state => {
|
||||
// Check for XP gain (success)
|
||||
const thievingXp = state.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
|
||||
if (thievingXp > xpBefore) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for stun/failure messages
|
||||
for (const msg of state.gameMessages) {
|
||||
if (msg.tick > startTick) {
|
||||
const text = msg.text.toLowerCase();
|
||||
if (text.includes('stunned') || text.includes('stun') ||
|
||||
text.includes('caught') || text.includes('failed') ||
|
||||
text.includes("what do you think you're doing")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss level-up dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
ctx.sdk.sendClickDialog(0).catch(() => {});
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 8000);
|
||||
|
||||
// Check what happened
|
||||
const thievingXpAfter = finalState.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
|
||||
if (thievingXpAfter > xpBefore) {
|
||||
console.log('Pickpocket success!');
|
||||
return true;
|
||||
}
|
||||
|
||||
// We got stunned - wait for stun to wear off
|
||||
console.log('Stunned! Waiting...');
|
||||
await new Promise(r => setTimeout(r, 5000)); // Stun lasts ~5 seconds
|
||||
return true; // Still count as progress since we attempted
|
||||
|
||||
} catch {
|
||||
console.log('Pickpocket timeout');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `th${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
|
||||
console.log('=== Thieving Training Script v1 ===');
|
||||
|
||||
const startStats = getThievingStats(ctx);
|
||||
console.log(`Starting Thieving: Level ${startStats.level} (${startStats.xp} XP)`);
|
||||
|
||||
let consecutiveFails = 0;
|
||||
|
||||
while (true) {
|
||||
const stats = getThievingStats(ctx);
|
||||
|
||||
if (stats.level >= 10) {
|
||||
console.log(`Goal achieved! Thieving level ${stats.level}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Dismiss any blocking dialogs
|
||||
const state = ctx.sdk.getState();
|
||||
if (state?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we have a target nearby
|
||||
const target = findTarget(ctx);
|
||||
if (!target) {
|
||||
console.log('No targets nearby, walking to Lumbridge center...');
|
||||
// Walk to Lumbridge center where Men/Women spawn
|
||||
await ctx.bot.walkTo(3222, 3218);
|
||||
consecutiveFails++;
|
||||
|
||||
if (consecutiveFails >= 5) {
|
||||
console.log('Too many consecutive fails, aborting');
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt pickpocket
|
||||
const success = await pickpocket(ctx);
|
||||
if (success) {
|
||||
consecutiveFails = 0;
|
||||
} else {
|
||||
consecutiveFails++;
|
||||
}
|
||||
|
||||
// Small delay between attempts
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
}
|
||||
|
||||
const endStats = getThievingStats(ctx);
|
||||
console.log(`\n=== Complete: Level ${endStats.level} (${endStats.xp} XP) ===`);
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* Varrock Travel Script
|
||||
*
|
||||
* Goal: Navigate from Lumbridge to Varrock using bot.walkTo()
|
||||
*
|
||||
* Uses waypoint-based navigation for long distances (>30 tiles).
|
||||
* Route goes through the cow field area and up to Varrock.
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Varrock West Bank area (ground floor, reliable destination)
|
||||
const VARROCK = { x: 3185, z: 3436 };
|
||||
|
||||
// Waypoints from Lumbridge to Varrock (~210 tiles total)
|
||||
// Each segment is 20-30 tiles to avoid pathfinding issues
|
||||
const WAYPOINTS = [
|
||||
{ x: 3232, z: 3245, name: 'North of Lumbridge' }, // First step north
|
||||
{ x: 3250, z: 3270, name: 'Near cow field' }, // East to avoid castle
|
||||
{ x: 3260, z: 3300, name: 'North of cow field' }, // Continue north
|
||||
{ x: 3255, z: 3330, name: 'Champion Guild area' }, // Past champions guild
|
||||
{ x: 3245, z: 3360, name: 'South Varrock' }, // Approaching Varrock
|
||||
{ x: 3230, z: 3390, name: 'Varrock south entrance' }, // South of Varrock
|
||||
{ x: 3210, z: 3420, name: 'Near Varrock square' }, // Into Varrock
|
||||
{ x: 3185, z: 3436, name: 'Varrock West Bank' }, // Final destination
|
||||
];
|
||||
|
||||
// Arrival tolerance in tiles
|
||||
const WAYPOINT_TOLERANCE = 8;
|
||||
const FINAL_TOLERANCE = 10;
|
||||
|
||||
/**
|
||||
* Check if we've reached Varrock (north of z=3400)
|
||||
*/
|
||||
function isInVarrock(ctx: ScriptContext): boolean {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return false;
|
||||
return state.player.worldZ >= 3400;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distance between player and a point
|
||||
*/
|
||||
function distanceTo(ctx: ScriptContext, x: number, z: number): number {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) return Infinity;
|
||||
const dx = x - state.player.worldX;
|
||||
const dz = z - state.player.worldZ;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a waypoint with retries
|
||||
*/
|
||||
async function walkToWaypoint(
|
||||
ctx: ScriptContext,
|
||||
x: number,
|
||||
z: number,
|
||||
name: string,
|
||||
tolerance: number
|
||||
): Promise<boolean> {
|
||||
const MAX_ATTEMPTS = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
const dist = distanceTo(ctx, x, z);
|
||||
if (dist <= tolerance) {
|
||||
console.log(`Already at ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Walking to ${name} (${x}, ${z}) - attempt ${attempt}/${MAX_ATTEMPTS}`);
|
||||
const result = await ctx.bot.walkTo(x, z, tolerance);
|
||||
|
||||
const newDist = distanceTo(ctx, x, z);
|
||||
if (newDist <= tolerance) {
|
||||
console.log(`Reached ${name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
console.warn(`Walk attempt ${attempt} failed: ${result.message}`);
|
||||
}
|
||||
|
||||
// Small delay before retry
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
const finalDist = distanceTo(ctx, x, z);
|
||||
console.warn(`Could not reach ${name} - distance: ${finalDist.toFixed(0)} tiles`);
|
||||
return finalDist <= tolerance * 2; // Allow some leeway to continue
|
||||
}
|
||||
|
||||
/**
|
||||
* Main travel function using waypoints
|
||||
*/
|
||||
async function travelToVarrock(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) throw new Error('No initial state');
|
||||
|
||||
const startX = state.player.worldX;
|
||||
const startZ = state.player.worldZ;
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`Start: (${startX}, ${startZ})`);
|
||||
console.log(`Destination: Varrock (${VARROCK.x}, ${VARROCK.z})`);
|
||||
console.log(`Total waypoints: ${WAYPOINTS.length}`);
|
||||
|
||||
if (isInVarrock(ctx)) {
|
||||
console.log('Already in Varrock!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Follow waypoints
|
||||
for (let i = 0; i < WAYPOINTS.length; i++) {
|
||||
const wp = WAYPOINTS[i];
|
||||
if (!wp) continue;
|
||||
|
||||
// Skip waypoints we're already past
|
||||
if (distanceTo(ctx, wp.x, wp.z) <= WAYPOINT_TOLERANCE) {
|
||||
console.log(`Skipping ${wp.name} - already there`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isLast = i === WAYPOINTS.length - 1;
|
||||
const tolerance = isLast ? FINAL_TOLERANCE : WAYPOINT_TOLERANCE;
|
||||
|
||||
const success = await walkToWaypoint(ctx, wp.x, wp.z, wp.name, tolerance);
|
||||
|
||||
if (!success && !isInVarrock(ctx)) {
|
||||
// Log position and continue trying
|
||||
const pos = ctx.sdk.getState()?.player;
|
||||
console.warn(`Stuck at (${pos?.worldX}, ${pos?.worldZ}) - trying next waypoint`);
|
||||
}
|
||||
|
||||
// Check if we've reached Varrock early
|
||||
if (isInVarrock(ctx)) {
|
||||
console.log('Reached Varrock area!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const endState = ctx.sdk.getState();
|
||||
const endX = endState?.player?.worldX ?? 0;
|
||||
const endZ = endState?.player?.worldZ ?? 0;
|
||||
const finalDist = distanceTo(ctx, VARROCK.x, VARROCK.z);
|
||||
|
||||
console.log(`End position: (${endX}, ${endZ})`);
|
||||
console.log(`Distance to target: ${finalDist.toFixed(0)} tiles`);
|
||||
console.log(`Time: ${elapsed.toFixed(1)}s`);
|
||||
|
||||
if (!isInVarrock(ctx) && finalDist > FINAL_TOLERANCE * 2) {
|
||||
throw new Error(`Failed to reach Varrock - stuck at (${endX}, ${endZ})`);
|
||||
}
|
||||
|
||||
console.log('Successfully reached Varrock!');
|
||||
}
|
||||
|
||||
// Run the script
|
||||
async function main() {
|
||||
const username = `vt${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
await travelToVarrock(ctx);
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 3 * 60 * 1000, // 3 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -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)
|
||||
@@ -1,273 +0,0 @@
|
||||
/**
|
||||
* Woodcutting Trainer Script
|
||||
*
|
||||
* Goal: Train Woodcutting to level 10+ starting from a fresh Lumbridge spawn.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Walk to Bob's Axes in Lumbridge
|
||||
* 2. Buy a bronze axe (16gp - fresh accounts have 25gp)
|
||||
* 3. Walk to trees near Lumbridge
|
||||
* 4. Chop trees until level 10
|
||||
* 5. Drop logs when inventory fills
|
||||
*/
|
||||
|
||||
import { runScript, type ScriptContext } from '../../sdk/runner';
|
||||
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
||||
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
||||
|
||||
// Locations
|
||||
const BOBS_AXES = { x: 3230, z: 3203 };
|
||||
const LUMBRIDGE_TREES = { x: 3200, z: 3230 }; // Trees west of Lumbridge castle
|
||||
|
||||
// Configuration
|
||||
const TARGET_LEVEL = 10;
|
||||
const BRONZE_AXE_COST = 16;
|
||||
|
||||
function getWoodcuttingLevel(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
|
||||
}
|
||||
|
||||
function getWoodcuttingXp(ctx: ScriptContext): number {
|
||||
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.experience ?? 0;
|
||||
}
|
||||
|
||||
function countLogs(ctx: ScriptContext): number {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
return inv.filter(i => /logs/i.test(i.name)).length;
|
||||
}
|
||||
|
||||
function hasAxe(ctx: ScriptContext): boolean {
|
||||
const inv = ctx.sdk.getState()?.inventory ?? [];
|
||||
const equip = ctx.sdk.getState()?.equipment ?? [];
|
||||
return inv.some(i => /axe/i.test(i.name)) || equip.some(i => /axe/i.test(i.name));
|
||||
}
|
||||
|
||||
function getCoins(ctx: ScriptContext): number {
|
||||
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
return coins?.count ?? 0;
|
||||
}
|
||||
|
||||
async function dropAllLogs(ctx: ScriptContext): Promise<void> {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) return;
|
||||
|
||||
const logsItems = state.inventory.filter(item => /logs/i.test(item.name));
|
||||
console.log(`Dropping ${logsItems.length} logs...`);
|
||||
|
||||
for (const item of logsItems) {
|
||||
await ctx.sdk.sendDropItem(item.slot);
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
}
|
||||
|
||||
async function buyAxeFromBob(ctx: ScriptContext): Promise<boolean> {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
// Check if we have enough coins
|
||||
const gp = getCoins(ctx);
|
||||
console.log(`Current GP: ${gp}`);
|
||||
|
||||
if (gp < BRONZE_AXE_COST) {
|
||||
console.error(`Not enough GP for bronze axe (need ${BRONZE_AXE_COST}, have ${gp})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk to Bob's Axes
|
||||
console.log(`Walking to Bob's Axes at (${BOBS_AXES.x}, ${BOBS_AXES.z})...`);
|
||||
await bot.walkTo(BOBS_AXES.x, BOBS_AXES.z);
|
||||
|
||||
// Dismiss any dialogs
|
||||
await bot.dismissBlockingUI();
|
||||
|
||||
// Log nearby NPCs for debugging
|
||||
const state = ctx.sdk.getState();
|
||||
console.log(`Position: (${state?.player?.worldX}, ${state?.player?.worldZ})`);
|
||||
const nearbyNpcs = state?.nearbyNpcs?.slice(0, 5) ?? [];
|
||||
console.log(`Nearby NPCs: ${nearbyNpcs.map(n => n.name).join(', ')}`);
|
||||
|
||||
// Open Bob's shop
|
||||
console.log('Opening shop...');
|
||||
let shopResult = await bot.openShop(/bob/i);
|
||||
|
||||
if (!shopResult.success) {
|
||||
// Try finding any NPC with trade option
|
||||
console.log('Bob not found by name, looking for trade option...');
|
||||
const npcWithTrade = state?.nearbyNpcs.find(n =>
|
||||
n.optionsWithIndex.some(o => /trade/i.test(o.text))
|
||||
);
|
||||
|
||||
if (npcWithTrade) {
|
||||
console.log(`Found tradeable NPC: ${npcWithTrade.name}`);
|
||||
const tradeOpt = npcWithTrade.optionsWithIndex.find(o => /trade/i.test(o.text));
|
||||
if (tradeOpt) {
|
||||
await sdk.sendInteractNpc(npcWithTrade.index, tradeOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if shop opened
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const shopState = ctx.sdk.getState()?.shop;
|
||||
|
||||
if (!shopState?.isOpen) {
|
||||
console.error('Failed to open shop');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log shop inventory
|
||||
console.log(`Shop open with ${shopState.shopItems.length} items`);
|
||||
const axes = shopState.shopItems.filter(i => /axe/i.test(i.name));
|
||||
for (const axe of axes) {
|
||||
console.log(` ${axe.name} - ${axe.buyPrice}gp (stock: ${axe.count})`);
|
||||
}
|
||||
|
||||
// Buy bronze axe
|
||||
console.log('Buying bronze axe...');
|
||||
const buyResult = await bot.buyFromShop(/bronze axe/i, 1);
|
||||
console.log(`Buy result: ${buyResult.message}`);
|
||||
|
||||
// Close shop by walking away
|
||||
await sdk.sendWalk(3225, 3210, true);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Verify we got the axe
|
||||
const gotAxe = hasAxe(ctx);
|
||||
if (gotAxe) {
|
||||
console.log('SUCCESS: Got bronze axe!');
|
||||
} else {
|
||||
console.error('Failed to buy bronze axe');
|
||||
}
|
||||
|
||||
return gotAxe;
|
||||
}
|
||||
|
||||
async function chopTrees(ctx: ScriptContext): Promise<void> {
|
||||
const { bot, sdk } = ctx;
|
||||
|
||||
console.log(`Walking to trees at (${LUMBRIDGE_TREES.x}, ${LUMBRIDGE_TREES.z})...`);
|
||||
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
|
||||
|
||||
let lastXp = getWoodcuttingXp(ctx);
|
||||
let logsChopped = 0;
|
||||
let stuckCount = 0;
|
||||
const MAX_STUCK = 10;
|
||||
|
||||
while (getWoodcuttingLevel(ctx) < TARGET_LEVEL) {
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state) {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dismiss any level-up dialogs
|
||||
if (state.dialog.isOpen) {
|
||||
console.log('Dismissing dialog...');
|
||||
await sdk.sendClickDialog(0);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drop logs if inventory is getting full
|
||||
if (state.inventory.length >= 26) {
|
||||
await dropAllLogs(ctx);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find nearest tree
|
||||
const tree = sdk.findNearbyLoc(/^tree$/i);
|
||||
|
||||
if (!tree) {
|
||||
console.log('No tree nearby, walking to tree area...');
|
||||
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
|
||||
stuckCount++;
|
||||
|
||||
if (stuckCount > MAX_STUCK) {
|
||||
throw new Error('Unable to find trees after multiple attempts');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chop the tree
|
||||
const result = await bot.chopTree(tree);
|
||||
|
||||
const currentXp = getWoodcuttingXp(ctx);
|
||||
if (currentXp > lastXp) {
|
||||
logsChopped++;
|
||||
stuckCount = 0; // Reset stuck counter on success
|
||||
console.log(`[Lvl ${getWoodcuttingLevel(ctx)}] Chopped tree! Logs: ${countLogs(ctx)}, Total: ${logsChopped}, XP: ${currentXp}`);
|
||||
lastXp = currentXp;
|
||||
} else if (!result.success) {
|
||||
console.log(`Chop failed: ${result.message}`);
|
||||
stuckCount++;
|
||||
|
||||
if (stuckCount > MAX_STUCK) {
|
||||
throw new Error(`Stuck: ${stuckCount} failed chop attempts`);
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between actions
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
console.log(`=== GOAL ACHIEVED: Woodcutting level ${getWoodcuttingLevel(ctx)}! ===`);
|
||||
}
|
||||
|
||||
// Main script
|
||||
async function main() {
|
||||
// Create fresh account
|
||||
const username = `wc${Math.random().toString(36).slice(2, 7)}`;
|
||||
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
||||
|
||||
// Launch browser
|
||||
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
||||
|
||||
try {
|
||||
await runScript(async (ctx) => {
|
||||
|
||||
console.log('=== Woodcutting Trainer ===');
|
||||
console.log(`Goal: Level ${TARGET_LEVEL}`);
|
||||
|
||||
// Wait for state to initialize
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
const state = ctx.sdk.getState();
|
||||
if (!state?.player) {
|
||||
console.error('No player state');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
|
||||
console.log(`Current Woodcutting level: ${getWoodcuttingLevel(ctx)}`);
|
||||
|
||||
// Dismiss any startup dialogs
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
|
||||
// Step 1: Get an axe if we don't have one
|
||||
if (!hasAxe(ctx)) {
|
||||
console.log('No axe found, buying from Bob...');
|
||||
const gotAxe = await buyAxeFromBob(ctx);
|
||||
if (!gotAxe) {
|
||||
console.error('Failed to acquire axe, cannot continue');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('Already have an axe!');
|
||||
}
|
||||
|
||||
// Step 2: Chop trees until target level
|
||||
console.log('Starting woodcutting training...');
|
||||
await chopTrees(ctx);
|
||||
|
||||
console.log('=== Script Complete ===');
|
||||
}, {
|
||||
connection: { bot: session.bot, sdk: session.sdk },
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
+3
-6
@@ -197,12 +197,9 @@ await sdk.waitForConnection(60000);
|
||||
|
||||
## Example Script
|
||||
|
||||
See `scripts/example-remote.ts` for a complete example.
|
||||
See `bots/_template/script.ts` for a complete starter example, or create a new
|
||||
bot with `bun bots/create-bot.ts <username>` and run its generated script:
|
||||
|
||||
```bash
|
||||
# Run locally
|
||||
bun scripts/example-remote.ts
|
||||
|
||||
# Connect to remote server
|
||||
GATEWAY_HOST=game.example.com BOT_USERNAME=player1 bun scripts/example-remote.ts
|
||||
bun bots/<username>/script.ts
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Logic test for sdk.clickDialogByText() — no bot required.
|
||||
*
|
||||
* Verifies the helper resolves dialog options by visible text and dispatches
|
||||
* the server-assigned (1-based) index, sidestepping the array-position
|
||||
* footgun that exists with raw sendClickDialog().
|
||||
*/
|
||||
|
||||
import { BotSDK } from '../index';
|
||||
import type { ActionResult, BotWorldState, DialogOption } from '../types';
|
||||
|
||||
let failures = 0;
|
||||
|
||||
function check(label: string, ok: boolean, detail?: string): void {
|
||||
if (ok) {
|
||||
console.log(` ok ${label}`);
|
||||
} else {
|
||||
failures++;
|
||||
console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
function makeOption(index: number, text: string): DialogOption {
|
||||
return { index, text, componentId: 1000 + index, buttonType: 0 };
|
||||
}
|
||||
|
||||
function makeState(opts: { dialogOpen: boolean; options?: DialogOption[] }): BotWorldState {
|
||||
// Only the dialog field matters — cast the rest. clickDialogByText only
|
||||
// touches state.dialog.
|
||||
return {
|
||||
dialog: {
|
||||
isOpen: opts.dialogOpen,
|
||||
options: opts.options ?? [],
|
||||
isWaiting: false,
|
||||
},
|
||||
} as unknown as BotWorldState;
|
||||
}
|
||||
|
||||
function mountSdk(state: BotWorldState | null): { sdk: BotSDK; calls: number[] } {
|
||||
const sdk = new BotSDK({ botUsername: 'test' });
|
||||
(sdk as any).state = state;
|
||||
|
||||
// Stub the protocol-level call so we can record what the helper dispatches.
|
||||
const calls: number[] = [];
|
||||
(sdk as any).sendClickDialog = async (option: number): Promise<ActionResult> => {
|
||||
calls.push(option);
|
||||
return { success: true, message: 'stub-clicked' };
|
||||
};
|
||||
|
||||
return { sdk, calls };
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
console.log('clickDialogByText():');
|
||||
|
||||
// 1. No dialog open → returns no_dialog
|
||||
{
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: false }));
|
||||
const r = await sdk.clickDialogByText(/yes/i);
|
||||
check('returns no_dialog when dialog is closed',
|
||||
!r.success && r.reason === 'no_dialog' && calls.length === 0,
|
||||
JSON.stringify(r));
|
||||
}
|
||||
|
||||
// 2. Null state → returns no_dialog
|
||||
{
|
||||
const { sdk, calls } = mountSdk(null);
|
||||
const r = await sdk.clickDialogByText(/yes/i);
|
||||
check('returns no_dialog when state is null',
|
||||
!r.success && r.reason === 'no_dialog' && calls.length === 0,
|
||||
JSON.stringify(r));
|
||||
}
|
||||
|
||||
// 3. Pattern matches first option → dispatches its server index (1, NOT 0)
|
||||
// This is the core footgun fix: array position 0 maps to server index 1.
|
||||
{
|
||||
const options = [
|
||||
makeOption(1, 'Yes, ok.'),
|
||||
makeOption(2, 'No thanks.'),
|
||||
];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText(/yes/i);
|
||||
check('dispatches server-assigned index 1 for first option (NOT array position 0)',
|
||||
r.success && calls.length === 1 && calls[0] === 1,
|
||||
`result=${JSON.stringify(r)} calls=${JSON.stringify(calls)}`);
|
||||
}
|
||||
|
||||
// 4. Pattern matches a later option → dispatches that option's index
|
||||
{
|
||||
const options = [
|
||||
makeOption(1, 'Yes, ok.'),
|
||||
makeOption(2, 'Is there anything down this alleyway?'),
|
||||
makeOption(3, 'No thanks.'),
|
||||
];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText('alleyway');
|
||||
check('dispatches index 2 for the alleyway option',
|
||||
r.success && calls.length === 1 && calls[0] === 2,
|
||||
`calls=${JSON.stringify(calls)}`);
|
||||
}
|
||||
|
||||
// 5. String patterns are case-insensitive
|
||||
{
|
||||
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText('YES');
|
||||
check('string pattern is case-insensitive',
|
||||
r.success && calls[0] === 1,
|
||||
`calls=${JSON.stringify(calls)}`);
|
||||
}
|
||||
|
||||
// 6. Caller-supplied RegExp flags are respected (e.g. case-sensitive when
|
||||
// explicitly constructed without /i)
|
||||
{
|
||||
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText(/YES/); // no /i, won't match "Yes"
|
||||
check('explicit RegExp without /i is honoured (no match)',
|
||||
!r.success && r.reason === 'no_match' && calls.length === 0,
|
||||
`result=${JSON.stringify(r)}`);
|
||||
}
|
||||
|
||||
// 7. No match → returns no_match with available options listed
|
||||
{
|
||||
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText(/maybe/i);
|
||||
check('returns no_match when no option matches',
|
||||
!r.success && r.reason === 'no_match' && calls.length === 0
|
||||
&& r.message.includes('Yes, ok.') && r.message.includes('No thanks.'),
|
||||
JSON.stringify(r));
|
||||
}
|
||||
|
||||
// 8. Empty options list → returns no_match (with "(none)")
|
||||
{
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options: [] }));
|
||||
const r = await sdk.clickDialogByText(/anything/);
|
||||
check('returns no_match when option list is empty',
|
||||
!r.success && r.reason === 'no_match' && calls.length === 0
|
||||
&& r.message.includes('(none)'),
|
||||
JSON.stringify(r));
|
||||
}
|
||||
|
||||
// 9. Non-contiguous server indices are passed through verbatim. (Real
|
||||
// dialogs always assign 1..N contiguously today, but the SDK should
|
||||
// never assume that — it must trust DialogOption.index.)
|
||||
{
|
||||
const options = [
|
||||
makeOption(7, 'Yes, ok.'),
|
||||
makeOption(42, 'Tell me more.'),
|
||||
];
|
||||
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
|
||||
const r = await sdk.clickDialogByText(/tell me/i);
|
||||
check('passes through non-contiguous server-assigned indices',
|
||||
r.success && calls[0] === 42,
|
||||
`calls=${JSON.stringify(calls)}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
if (failures === 0) {
|
||||
console.log('PASSED — all 9 cases');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`FAILED — ${failures} case(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(e => {
|
||||
console.error('Fatal:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,8 @@ coverage/
|
||||
# these are artifacts generated from other repos
|
||||
public/loader.jar
|
||||
public/runescape*.jar
|
||||
# built clients are served from webclient/out; a copy here shadows fresh builds
|
||||
public/client/*.js
|
||||
public/client/*.js.map
|
||||
|
||||
### Windows ###
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user