mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
Make the agent API self-describing and add regression CI (#43)
* Improve agent API docs and validation * Drop the wiki search tool Removes wiki/search.ts and its test. The index only covered names, aliases, summaries and Sources lines, so it had worse recall than grep on body text (e.g. "cow hide" missed skills/crafting.md), and normalize() stripped underscores so map-square queries matched every neighbouring square (m50_50 returned m50_53 NPCs). The computed mapSquareBounds were only indexed as text, so coordinate lookups — the one thing grep can't do — never worked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Regenerate the wiki from server/content Adds the coordinate-samples column to NPC location tables and picks up content drift the checked-in pages had missed (807 NPC, 117 shop, 1573 item pages; 335 of them new). Examples: Aubury now stocks 1000 chaos/death runes rather than 500, and shop rows point at "Vial of water"/"Bolts" instead of the stale "Vial"/"Bolt" names. Also stops all three generators from crashing on their final step. Each one read wiki/README.md unconditionally, but that file has never existed, so the index update threw ENOENT on every run. Skip it when absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b20b027f01
commit
e5fb1e9d07
1164 files changed
+10778
-3855
No files matched your search
@@ -0,0 +1,29 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install root dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install webclient dependencies
|
||||
working-directory: server/webclient
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Type-check, test, and verify generated docs
|
||||
run: bun run check
|
||||
@@ -41,6 +41,20 @@ bun bots/create-bot.ts {username}
|
||||
bun bots/{username}/script.ts
|
||||
```
|
||||
|
||||
## Agent API and knowledge
|
||||
|
||||
The exact generated API surface is documented in
|
||||
[`sdk/API.md`](sdk/API.md). High-level `bot.*` methods attempt to observe
|
||||
method-specific game effects; low-level `sdk.send*` methods confirm
|
||||
browser-client dispatch and do not prove the server applied the effect.
|
||||
|
||||
MCP `execute_code` snippets receive `bot` and `sdk` globals. Standalone files
|
||||
under `bots/<name>/` instead use `runScript(async ({ bot, sdk }) => { ... })`;
|
||||
see [`learnings/README.md`](learnings/README.md) for copyable examples.
|
||||
The current executor evaluates JavaScript-compatible async bodies directly, so
|
||||
use the TypeScript API reference for type information but omit type-only syntax
|
||||
from an `execute_code` body.
|
||||
|
||||
Chat is shown by default. Note that seeing other players' chat exposes the bot to scamming and prompt-injection attempts; opt out with `SHOW_CHAT=false` in the bot.env file (or `bun bots/create-bot.ts <name> --no-chat`).
|
||||
|
||||
Warning: The demo server is offered as a convenience, and we do not guarantee uptime or data persistence. Hold your accounts lightly, and consider hosting your own server instance. Please do not manually play on the demo server.
|
||||
@@ -62,7 +76,9 @@ This server has a few modifications from the original game to make development a
|
||||
rs-sdk runs against an enhanced web-based client (`botclient`) which connects to the LostCity 2004scape server emulator.
|
||||
|
||||
There is a gateway server which accepts connections from botclient and SDK instances, and forwards messages between them based on username.
|
||||
Once connected to the gateway, the botclient will relay game state to the SDK, and execute low-level actions (e.g. `walkTo(x,y)`) sent from the SDK through the gateway.
|
||||
Once connected to the gateway, the botclient relays game state to the SDK and
|
||||
dispatches low-level actions such as `sendWalk(x, z)` from the SDK. Dispatch
|
||||
success is not confirmation that the game server applied the intended effect.
|
||||
|
||||
This means that the SDK can't talk directly to the game server, but must go through the botclient. It will attempt to launch the botclient on startup if one is not already running.
|
||||
|
||||
@@ -92,6 +108,19 @@ cd server/gateway && bun run gateway
|
||||
|
||||
The gateway listens on `ws://localhost:7780` by default (configurable via `AGENT_PORT` env var).
|
||||
|
||||
## Development checks
|
||||
|
||||
Install the root and webclient dependencies, then run the same checks as CI:
|
||||
|
||||
```sh
|
||||
bun install --frozen-lockfile
|
||||
(cd server/webclient && bun install --frozen-lockfile)
|
||||
bun run check
|
||||
```
|
||||
|
||||
`bun run docs:api` regenerates the API reference. CI fails when that document
|
||||
does not match the public TypeScript class surface.
|
||||
|
||||
|
||||
### 2. Connect a bot to the local gateway
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item an
|
||||
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.*` - High-level actions that attempt to observe method-specific evidence (chopTree, walkTo, attackNpc, etc.). Check a result when the method returns one.
|
||||
- `sdk.*` - State queries and low-level browser-client dispatches. A successful `send*` result does not prove the game server applied the effect.
|
||||
|
||||
### bot.* Quick Reference
|
||||
|
||||
@@ -184,7 +184,21 @@ For the complete method reference, see **[sdk/API.md](sdk/API.md)** (auto-genera
|
||||
| `waitForChat(opts?)` | Wait for a message (`{from, matching, timeout}`) |
|
||||
| `waitForCondition(pred)` | Wait for state predicate |
|
||||
| `waitForTicks(n)` | Wait n game ticks |
|
||||
| `scanNearbyLocs(radius?)` | Extended-range loc scan |
|
||||
| `scanNearbyLocs(radius?)` | Async extended-range loc scan; must be awaited |
|
||||
|
||||
See `sdk/API.md` for exact async signatures, parameter defaults, and return
|
||||
types.
|
||||
|
||||
### Code environments
|
||||
|
||||
MCP `execute_code` exposes `bot` and `sdk` directly. Standalone
|
||||
`bots/{username}/*.ts` files must import `runScript` and receive
|
||||
`{ bot, sdk }` in its callback. Do not paste runner-context expressions into
|
||||
`execute_code`.
|
||||
|
||||
Only one control connection should own a bot. Use observe mode for read-only
|
||||
monitoring, avoid overlapping action programs on the same bot, and inspect
|
||||
`sdk.getStateAge()` before acting after a disconnect or long pause.
|
||||
|
||||
---
|
||||
|
||||
@@ -251,4 +265,4 @@ wiki/
|
||||
**Wrong target** - Use more specific regex patterns: `/^tree$/i` not `/tree/i` (which matches "tree stump").
|
||||
|
||||
|
||||
Start small and build up!
|
||||
Start small and build up!
|
||||
@@ -0,0 +1,46 @@
|
||||
# Agent learning snippets
|
||||
|
||||
Unless a section explicitly says otherwise, snippets in this directory are
|
||||
written for the MCP `execute_code` environment. That environment provides
|
||||
`bot` and `sdk` as globals and supports top-level `await`. Although the API
|
||||
reference uses TypeScript notation, keep `execute_code` bodies
|
||||
JavaScript-compatible and omit type-only syntax:
|
||||
|
||||
```typescript
|
||||
const state = sdk.getState();
|
||||
const result = await bot.walkTo(3222, 3218);
|
||||
return { result, state: sdk.getState() };
|
||||
```
|
||||
|
||||
Standalone files under `bots/<name>/` use a different wrapper. Import
|
||||
`runScript`, then destructure the same names once:
|
||||
|
||||
```typescript
|
||||
import { runScript } from '../../sdk/runner';
|
||||
|
||||
await runScript(async ({ bot, sdk }) => {
|
||||
await bot.walkTo(3222, 3218);
|
||||
return sdk.getState();
|
||||
});
|
||||
```
|
||||
|
||||
For a long or background run, execute the standalone file with
|
||||
`bun bots/<name>/script.ts`. Do not launch a detached bare snippet: `runScript`
|
||||
owns connection setup, timeout reporting, signal handling, and shutdown.
|
||||
|
||||
Do not paste context-prefixed runner expressions into `execute_code`.
|
||||
|
||||
## Action semantics
|
||||
|
||||
- Prefer high-level `bot.*` helpers. They attempt to observe method-specific
|
||||
evidence. Check the result when a method returns one.
|
||||
- Low-level `sdk.send*` calls confirm browser-client dispatch only. A successful
|
||||
result does not prove that the game server applied the action. Observe the
|
||||
intended state, XP, inventory, dialog, or message change before continuing.
|
||||
- Await every method returning `Promise`, including `scanNearbyLocs()` and
|
||||
`scanGroundItems()`.
|
||||
- `getInventory().length` counts occupied slots. An `InventoryItem.count` is the
|
||||
quantity in that slot; `findInventoryItem()` returns only the first matching
|
||||
slot.
|
||||
|
||||
See [`../sdk/API.md`](../sdk/API.md) for exact signatures.
|
||||
+26
-24
@@ -7,21 +7,21 @@ Successful patterns for bank interactions.
|
||||
|
||||
```typescript
|
||||
// Open bank (finds banker NPC or booth automatically)
|
||||
const openResult = await ctx.bot.openBank();
|
||||
const openResult = await bot.openBank();
|
||||
if (!openResult.success) {
|
||||
console.log(`Failed to open bank: ${openResult.message}`);
|
||||
}
|
||||
|
||||
// Deposit item by name
|
||||
const depositResult = await ctx.bot.depositItem(/coins/i); // deposits all
|
||||
const depositResult = await ctx.bot.depositItem(/sword/i, 1); // deposits 1
|
||||
const depositCoins = await bot.depositItem(/coins/i); // deposits all
|
||||
const depositSword = await bot.depositItem(/sword/i, 1); // deposits 1
|
||||
|
||||
// Withdraw item by bank slot
|
||||
const withdrawResult = await ctx.bot.withdrawItem(0); // withdraws 1 from slot 0
|
||||
const withdrawResult = await ctx.bot.withdrawItem(0, -1); // withdraws all from slot 0
|
||||
const withdrawOne = await bot.withdrawItem(0); // withdraws 1 from slot 0
|
||||
const withdrawAll = await bot.withdrawItem(0, -1); // withdraws all from slot 0
|
||||
|
||||
// Close bank
|
||||
await ctx.bot.closeBank();
|
||||
await bot.closeBank();
|
||||
```
|
||||
|
||||
|
||||
@@ -30,16 +30,16 @@ await ctx.bot.closeBank();
|
||||
|
||||
```typescript
|
||||
// Deposit specific item
|
||||
const ore = ctx.sdk.getState()?.inventory.find(i => /ore$/i.test(i.name));
|
||||
const ore = sdk.getState()?.inventory.find(i => /ore$/i.test(i.name));
|
||||
if (ore) {
|
||||
await ctx.sdk.sendBankDeposit(ore.slot, ore.count);
|
||||
await sdk.sendBankDeposit(ore.slot, ore.count);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Deposit all of a type
|
||||
const ores = ctx.sdk.getState()?.inventory.filter(i => /ore$/i.test(i.name)) ?? [];
|
||||
const ores = sdk.getState()?.inventory.filter(i => /ore$/i.test(i.name)) ?? [];
|
||||
for (const ore of ores) {
|
||||
await ctx.sdk.sendBankDeposit(ore.slot, ore.count);
|
||||
await sdk.sendBankDeposit(ore.slot, ore.count);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
```
|
||||
@@ -50,11 +50,11 @@ Use `-1` as the quantity to deposit all of an item type.
|
||||
|
||||
```typescript
|
||||
// High-level (recommended)
|
||||
await ctx.bot.depositItem(/bones/i, -1); // Deposits ALL bones (even if in 5 separate slots)
|
||||
await ctx.bot.depositItem(/coins/i, -1); // Deposits ALL coins (stacked)
|
||||
await bot.depositItem(/bones/i, -1); // Deposits ALL bones (even if in 5 separate slots)
|
||||
await bot.depositItem(/coins/i, -1); // Deposits ALL coins (stacked)
|
||||
|
||||
// Low-level
|
||||
await ctx.sdk.sendBankDeposit(slot, -1); // Deposits ALL items of that type from ANY slot
|
||||
await sdk.sendBankDeposit(slot, -1); // Deposits ALL items of that type from ANY slot
|
||||
```
|
||||
|
||||
|
||||
@@ -62,17 +62,17 @@ await ctx.sdk.sendBankDeposit(slot, -1); // Deposits ALL items of that type fro
|
||||
|
||||
```typescript
|
||||
// bankSlot is the position in the bank, not inventory
|
||||
await ctx.sdk.sendBankWithdraw(bankSlot, count);
|
||||
await sdk.sendBankWithdraw(bankSlot, count);
|
||||
```
|
||||
|
||||
## Closing the Bank
|
||||
|
||||
```typescript
|
||||
// High-level (recommended)
|
||||
await ctx.bot.closeBank();
|
||||
await bot.closeBank();
|
||||
|
||||
// Low-level (works for any modal interface)
|
||||
await ctx.sdk.sendCloseModal();
|
||||
await sdk.sendCloseModal();
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
```
|
||||
|
||||
@@ -88,29 +88,31 @@ await new Promise(r => setTimeout(r, 500));
|
||||
## Full Banking Loop Pattern
|
||||
|
||||
```typescript
|
||||
async function bankTrip(ctx, itemPattern, bankCoords, returnCoords) {
|
||||
async function bankTrip(itemPattern, bankCoords, returnCoords) {
|
||||
// Walk to bank
|
||||
await ctx.bot.walkTo(bankCoords.x, bankCoords.z);
|
||||
await bot.walkTo(bankCoords.x, bankCoords.z);
|
||||
|
||||
// Open bank (automatically finds banker/booth)
|
||||
const openResult = await ctx.bot.openBank();
|
||||
const openResult = await bot.openBank();
|
||||
if (!openResult.success) {
|
||||
console.log(`Failed to open bank: ${openResult.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deposit all items matching pattern (one call deposits ALL, even non-stackable)
|
||||
await ctx.bot.depositItem(itemPattern, -1);
|
||||
await bot.depositItem(itemPattern, -1);
|
||||
|
||||
// Close bank and return
|
||||
await ctx.bot.closeBank();
|
||||
await ctx.bot.walkTo(returnCoords.x, returnCoords.z);
|
||||
await bot.closeBank();
|
||||
await bot.walkTo(returnCoords.x, returnCoords.z);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Key Learnings from Cow hide Banking
|
||||
1. **Gate exit threshold**: Use `z < 3268` not `z < 3265` for cow field
|
||||
2. **Always open gate first**: `openDoor(/gate/i)` before walking through
|
||||
3. **Use sendWalk for gate traversal**: More reliable than pathfinder
|
||||
2. **Let `walkTo` handle the gate first**: It attempts to open doors on its route
|
||||
3. **Recover explicitly**: If movement fails at the south gate `(3253, 3266)`,
|
||||
inspect its current options, call `openDoor(gate)` when `Open` is present,
|
||||
then retry `walkTo`
|
||||
4. **Banking works at Varrock West**: (3185, 3436) confirmed working
|
||||
+51
-48
@@ -8,54 +8,54 @@ Use `bot.attackNpc()` for cleaner code, or raw SDK for more control:
|
||||
|
||||
```typescript
|
||||
// Porcelain method (recommended)
|
||||
await ctx.bot.attackNpc(/cow/i);
|
||||
const attack = await bot.attackNpc(/cow/i);
|
||||
if (!attack.success) console.warn(attack.reason, attack.message);
|
||||
|
||||
// Raw SDK method
|
||||
const npc = state.nearbyNpcs.find(n => /cow/i.test(n.name));
|
||||
const attackOpt = npc.optionsWithIndex.find(o => /attack/i.test(o.text));
|
||||
await ctx.sdk.sendInteractNpc(npc.index, attackOpt.opIndex);
|
||||
// Raw SDK dispatch (observe combat state afterwards)
|
||||
const state = sdk.getState();
|
||||
const npc = state?.nearbyNpcs.find(n => /cow/i.test(n.name));
|
||||
const attackOpt = npc?.optionsWithIndex.find(o => /attack/i.test(o.text));
|
||||
if (npc && attackOpt) {
|
||||
await sdk.sendInteractNpc(npc.index, attackOpt.opIndex);
|
||||
await sdk.waitForCondition(s => s.player?.combat.inCombat === true, 5_000);
|
||||
}
|
||||
```
|
||||
|
||||
## Combat Style Cycling
|
||||
|
||||
Rotate styles for balanced training:
|
||||
Combat-style indices depend on the equipped weapon. Resolve them from the
|
||||
current state instead of hard-coding `0..3`:
|
||||
|
||||
```typescript
|
||||
// Combat style indices
|
||||
const STYLES = {
|
||||
ATTACK: 0, // Train Attack
|
||||
STRENGTH: 1, // Train Strength
|
||||
STRENGTH2: 2, // Also Strength (some weapons)
|
||||
DEFENCE: 3, // Train Defence
|
||||
};
|
||||
|
||||
// Cycle every 30 seconds or on level-up
|
||||
let lastStyleChange = Date.now();
|
||||
const CYCLE_INTERVAL = 30_000;
|
||||
|
||||
if (Date.now() - lastStyleChange > CYCLE_INTERVAL) {
|
||||
currentStyle = (currentStyle + 1) % 4;
|
||||
if (currentStyle === 2) currentStyle = 3; // Skip duplicate strength
|
||||
await ctx.sdk.sendSetCombatStyle(currentStyle);
|
||||
lastStyleChange = Date.now();
|
||||
const targetSkill = 'Defence';
|
||||
const style = sdk.getState()?.combatStyle?.styles.find(
|
||||
candidate => candidate.trainedSkill === targetSkill,
|
||||
);
|
||||
if (!style) {
|
||||
throw new Error(`No ${targetSkill} style is available for the equipped weapon`);
|
||||
}
|
||||
await sdk.sendSetCombatStyle(style.index);
|
||||
await sdk.waitForCondition(
|
||||
state => state.combatStyle?.currentStyle === style.index,
|
||||
5_000,
|
||||
);
|
||||
```
|
||||
|
||||
## Checking Combat State
|
||||
|
||||
```typescript
|
||||
// Optional chaining needed - combat can be undefined
|
||||
const inCombat = state.combat?.inCombat ?? false;
|
||||
const state = sdk.getState();
|
||||
const inCombat = state?.player?.combat.inCombat ?? false;
|
||||
|
||||
// Or check if we're animating (attacking)
|
||||
const isAttacking = state.player?.animId !== -1;
|
||||
// Animation is a useful heuristic, but is not specific to combat.
|
||||
const isAnimating = (state?.player?.animId ?? -1) !== -1;
|
||||
```
|
||||
|
||||
## Safe Training Locations
|
||||
|
||||
| Location | Coordinates | Targets | Notes |
|
||||
|----------|-------------|---------|-------|
|
||||
| Lumbridge cows | (3253, 3290) | Cows | Safe, good for all levels. Gate at (3253, 3270) |
|
||||
| Lumbridge cows | (3253, 3290) | Cows | Safe, good for all levels. South gate at (3253, 3266) |
|
||||
| Lumbridge goblins | (3252, 3230) | Goblins, rats | Mixed enemies. Cluster runs NE from here — nothing at the old (3240, 3220) waypoint |
|
||||
| Lumbridge chickens | (3237, 3295) | Chickens | Very safe, feathers drop |
|
||||
| Al Kharid warriors | ~(3293, 3170) | `Al-Kharid warrior` (lvl 9) | Faster XP, kebabs nearby for food, can hit hard via multicombat vs low combat levels. |
|
||||
@@ -64,11 +64,11 @@ const isAttacking = state.player?.animId !== -1;
|
||||
|
||||
The cow field is fenced with a gate on the south side:
|
||||
- **Field center**: ~(3253, 3290)
|
||||
- **Gate position**: (3253, 3270)
|
||||
- **South gate position**: (3253, 3266)
|
||||
- **Inside cow pen**: x between 3242-3265, z between 3255-3298
|
||||
|
||||
```typescript
|
||||
function isInsideCowPen(x: number, z: number): boolean {
|
||||
function isInsideCowPen(x, z) {
|
||||
return x >= 3242 && x <= 3265 && z >= 3255 && z <= 3298;
|
||||
}
|
||||
```
|
||||
@@ -79,11 +79,11 @@ Cow field and chicken coop have fenced gates:
|
||||
|
||||
```typescript
|
||||
// Check for gate blocking path
|
||||
const gate = state.nearbyLocs.find(l => /gate/i.test(l.name));
|
||||
const gate = 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.bot.openDoor(gate);
|
||||
await bot.openDoor(gate);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -93,8 +93,8 @@ if (gate) {
|
||||
After killing an NPC, find the next one quickly:
|
||||
|
||||
```typescript
|
||||
async function findTarget(ctx, pattern: RegExp) {
|
||||
const state = ctx.sdk.getState();
|
||||
async function findTarget(pattern) {
|
||||
const state = sdk.getState();
|
||||
if (!state) return null;
|
||||
|
||||
return state.nearbyNpcs
|
||||
@@ -110,10 +110,10 @@ async function findTarget(ctx, pattern: RegExp) {
|
||||
|
||||
```typescript
|
||||
// WRONG - nearbyLocs is for static objects (trees, rocks, etc.)
|
||||
const loot = state.nearbyLocs.filter(i => /hide/i.test(i.name)); // Won't work!
|
||||
const wrongLoot = sdk.getState()?.nearbyLocs.filter(i => /hide/i.test(i.name)); // Won't work!
|
||||
|
||||
// CORRECT - scanGroundItems() for drops
|
||||
const groundItems = await ctx.sdk.scanGroundItems();
|
||||
const groundItems = await sdk.scanGroundItems();
|
||||
const loot = groundItems.filter(i => /hide|bones|coins/i.test(i.name));
|
||||
```
|
||||
|
||||
@@ -123,14 +123,14 @@ Pick up a few items (e.g. 3), then return to combat. Prevents getting stuck in i
|
||||
|
||||
```typescript
|
||||
const MAX_PICKUPS = 3;
|
||||
const groundItems = await ctx.sdk.scanGroundItems();
|
||||
const groundItems = await sdk.scanGroundItems();
|
||||
const loot = groundItems
|
||||
.filter(i => /hide|bones/i.test(i.name))
|
||||
.filter(i => i.distance < 5)
|
||||
.slice(0, MAX_PICKUPS);
|
||||
|
||||
for (const item of loot) {
|
||||
await ctx.bot.pickupItem(item);
|
||||
await bot.pickupItem(item);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
// Back to combat
|
||||
@@ -143,7 +143,7 @@ Timeouts and errors are frequent in crowded areas. Wrap attacks in try/catch:
|
||||
```typescript
|
||||
// This pattern enabled consistent 10-minute runs
|
||||
try {
|
||||
await ctx.bot.attackNpc(/cow/i);
|
||||
await bot.attackNpc(/cow/i);
|
||||
} catch (err) {
|
||||
console.log(`Attack timed out, trying next cow`);
|
||||
continue; // Don't crash - just find another target
|
||||
@@ -163,7 +163,7 @@ try {
|
||||
Browser glitches sometimes return invalid positions. Validate state before acting:
|
||||
|
||||
```typescript
|
||||
const player = ctx.sdk.getState()?.player;
|
||||
const player = sdk.getState()?.player;
|
||||
if (!player || player.worldX === 0 || player.worldZ === 0) {
|
||||
console.log('Invalid state - waiting for sync');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
@@ -182,21 +182,24 @@ if (Math.abs(player.worldX - lastX) > 500) {
|
||||
For balanced progression, automatically train whichever stat is lowest:
|
||||
|
||||
```typescript
|
||||
function getLowestCombatStat(state): { stat: string, style: number } {
|
||||
function getLowestCombatStat(state) {
|
||||
const skills = state.skills;
|
||||
const atk = skills.find(s => s.name === 'Attack')?.baseLevel ?? 1;
|
||||
const str = skills.find(s => s.name === 'Strength')?.baseLevel ?? 1;
|
||||
const def = skills.find(s => s.name === 'Defence')?.baseLevel ?? 1;
|
||||
|
||||
if (def <= atk && def <= str) return { stat: 'Defence', style: 3 };
|
||||
if (str <= atk) return { stat: 'Strength', style: 1 };
|
||||
return { stat: 'Attack', style: 0 };
|
||||
if (def <= atk && def <= str) return 'Defence';
|
||||
if (str <= atk) return 'Strength';
|
||||
return 'Attack';
|
||||
}
|
||||
|
||||
// Set combat style based on lowest stat
|
||||
const { stat, style } = getLowestCombatStat(ctx.sdk.getState());
|
||||
await ctx.sdk.sendSetCombatStyle(style);
|
||||
console.log(`Training ${stat} (lowest)`);
|
||||
const state = sdk.getState();
|
||||
if (!state) throw new Error('No world state');
|
||||
const stat = getLowestCombatStat(state);
|
||||
const style = state.combatStyle?.styles.find(option => option.trainedSkill === stat);
|
||||
if (!style) throw new Error(`No style trains ${stat} with the equipped weapon`);
|
||||
await sdk.sendSetCombatStyle(style.index);
|
||||
console.log(`Training ${stat} with ${style.name}`);
|
||||
```
|
||||
|
||||
This pattern enabled balanced 60+ in all melee stats.
|
||||
+28
-22
@@ -7,8 +7,8 @@ Successful patterns for handling game dialogs and interfaces.
|
||||
Level-up dialogs block all actions. Dismiss them immediately:
|
||||
|
||||
```typescript
|
||||
if (state.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
if (sdk.getState()?.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
continue; // Skip rest of loop iteration
|
||||
}
|
||||
```
|
||||
@@ -18,13 +18,13 @@ if (state.dialog.isOpen) {
|
||||
All BotActions methods automatically dismiss blocking UI before executing. Manual dismissal is only needed when using low-level sdk methods directly:
|
||||
|
||||
```typescript
|
||||
await ctx.bot.dismissBlockingUI();
|
||||
await bot.dismissBlockingUI();
|
||||
```
|
||||
|
||||
## Checking Dialog State
|
||||
|
||||
```typescript
|
||||
const dialog = ctx.sdk.getState()?.dialog;
|
||||
const dialog = sdk.getState()?.dialog;
|
||||
|
||||
// Is any dialog open?
|
||||
if (dialog.isOpen) { ... }
|
||||
@@ -45,7 +45,7 @@ For NPC conversations with choices:
|
||||
```typescript
|
||||
// Click through until specific option appears
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
const s = sdk.getState();
|
||||
if (!s?.dialog.isOpen) {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
continue;
|
||||
@@ -54,12 +54,12 @@ for (let i = 0; i < 20; i++) {
|
||||
// Look for target option
|
||||
const targetOpt = s.dialog.options.find((o) => /yes/i.test(o.text));
|
||||
if (targetOpt) {
|
||||
await ctx.sdk.sendClickDialog(targetOpt.index);
|
||||
await sdk.sendClickDialog(targetOpt.index);
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise click to continue
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await sdk.sendClickDialog(0);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
```
|
||||
@@ -67,6 +67,9 @@ for (let i = 0; i < 20; i++) {
|
||||
## Shop Interfaces
|
||||
|
||||
```typescript
|
||||
const state = sdk.getState();
|
||||
if (!state) throw new Error('No world state');
|
||||
|
||||
// Check if shop is open
|
||||
if (state.shop.isOpen) { ... }
|
||||
|
||||
@@ -86,20 +89,21 @@ Always use `0` as fallback for "Click here to continue" screens.
|
||||
Requires 10gp. Position west of gate: (3267, 3228).
|
||||
|
||||
```typescript
|
||||
const gate = ctx.sdk.getState()?.nearbyLocs.find((l) => /gate/i.test(l.name));
|
||||
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, 1);
|
||||
await sleep(1000);
|
||||
const gate = sdk.getState()?.nearbyLocs.find((l) => /gate/i.test(l.name));
|
||||
if (!gate) throw new Error('No gate nearby');
|
||||
await sdk.sendInteractLoc(gate.x, gate.z, gate.id, 1);
|
||||
await sdk.waitForTicks(2);
|
||||
|
||||
// Click through dialog: 0 = continue, or pick "Yes" when available
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const yesOpt = ctx.sdk
|
||||
const yesOpt = sdk
|
||||
.getState()
|
||||
?.dialog?.options.find((o) => /yes/i.test(o.text));
|
||||
await ctx.sdk.sendClickDialog(yesOpt?.index ?? 0);
|
||||
await sleep(300);
|
||||
await sdk.sendClickDialog(yesOpt?.index ?? 0);
|
||||
await sdk.waitForTicks(1);
|
||||
}
|
||||
|
||||
await ctx.bot.walkTo(3277, 3227); // Walk through to Al Kharid
|
||||
await bot.walkTo(3277, 3227); // Walk through to Al Kharid
|
||||
```
|
||||
|
||||
## Buying Kebabs from the Kebab Seller (Al Kharid)
|
||||
@@ -108,29 +112,31 @@ Sells kebabs via dialog (not a shop interface). Location: (3273, 3180), spawns a
|
||||
|
||||
```typescript
|
||||
// Walk to kebab seller
|
||||
await ctx.bot.walkTo(3273, 3180);
|
||||
await bot.walkTo(3273, 3180);
|
||||
|
||||
// Find the kebab seller
|
||||
const seller = ctx.sdk
|
||||
const seller = sdk
|
||||
.getState()
|
||||
?.nearbyNpcs.find((n) => /kebab/i.test(n.name));
|
||||
if (!seller) throw new Error('No kebab seller nearby');
|
||||
const talkOpt = seller.optionsWithIndex.find((o) => /talk/i.test(o.text));
|
||||
await ctx.sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
|
||||
if (!talkOpt) throw new Error('Kebab seller has no Talk-to option');
|
||||
await sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
// Handle dialog to buy kebab (1gp each)
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const s = ctx.sdk.getState();
|
||||
const s = 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);
|
||||
await sdk.sendClickDialog(buyOpt.index);
|
||||
break;
|
||||
}
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await sdk.sendClickDialog(0);
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
```
|
||||
@@ -143,9 +149,9 @@ If your script makes no progress, check for stuck dialogs:
|
||||
|
||||
```typescript
|
||||
// In your main loop
|
||||
if (state.dialog.isOpen) {
|
||||
if (sdk.getState()?.dialog.isOpen) {
|
||||
console.log("Dialog open, dismissing...");
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
+19
-9
@@ -7,7 +7,7 @@ Successful patterns for fishing automation.
|
||||
Fishing spots are **NPCs**, not locations:
|
||||
|
||||
```typescript
|
||||
const spot = state.nearbyNpcs.find((npc) => /fishing\s*spot/i.test(npc.name));
|
||||
const spot = sdk.getState()?.nearbyNpcs.find((npc) => /fishing\s*spot/i.test(npc.name));
|
||||
```
|
||||
|
||||
## Spot Types Matter
|
||||
@@ -24,17 +24,20 @@ Filter for the right spot type:
|
||||
|
||||
```typescript
|
||||
// Level 1 fishing - need "Bait" option (indicates small net spot)
|
||||
const fishingSpots = sdk.getState()?.nearbyNpcs.filter(
|
||||
npc => /fishing\s*spot/i.test(npc.name),
|
||||
) ?? [];
|
||||
const smallNetSpots = fishingSpots.filter((npc) =>
|
||||
npc.options.some((opt) => /^bait$/i.test(opt)),
|
||||
npc.optionsWithIndex.some((opt) => /^bait$/i.test(opt.text)),
|
||||
);
|
||||
```
|
||||
|
||||
## Fishing Action
|
||||
|
||||
```typescript
|
||||
const spot = state.nearbyNpcs.find((npc) => /fishing\s*spot/i.test(npc.name));
|
||||
const netOpt = spot.optionsWithIndex.find((o) => /^net$/i.test(o.text));
|
||||
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
const spot = sdk.getState()?.nearbyNpcs.find((npc) => /fishing\s*spot/i.test(npc.name));
|
||||
const netOpt = spot?.optionsWithIndex.find((o) => /^net$/i.test(o.text));
|
||||
if (spot && netOpt) await sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
```
|
||||
|
||||
## Continuous Clicking Works
|
||||
@@ -43,16 +46,22 @@ Don't over-engineer wait conditions. Just keep clicking:
|
||||
|
||||
```typescript
|
||||
while (true) {
|
||||
const state = sdk.getState();
|
||||
if (!state) {
|
||||
await sdk.waitForStateUpdate();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dismiss any dialogs (level-ups)
|
||||
if (state.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
await sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const spot = state.nearbyNpcs.find((npc) => /fishing\s*spot/i.test(npc.name));
|
||||
if (spot) {
|
||||
const netOpt = spot.optionsWithIndex.find((o) => /^net$/i.test(o.text));
|
||||
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
if (netOpt) await sdk.sendInteractNpc(spot.index, netOpt.opIndex);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
@@ -79,7 +88,8 @@ Fishing spots move. Check distance and walk back if needed:
|
||||
const START_AREA = { x: 3087, z: 3230 };
|
||||
const MAX_DRIFT = 15;
|
||||
|
||||
const player = state.player;
|
||||
const player = sdk.getState()?.player;
|
||||
if (!player) throw new Error('No player state');
|
||||
const drift = Math.sqrt(
|
||||
Math.pow(player.worldX - START_AREA.x, 2) +
|
||||
Math.pow(player.worldZ - START_AREA.z, 2),
|
||||
@@ -87,6 +97,6 @@ const drift = Math.sqrt(
|
||||
|
||||
if (drift > MAX_DRIFT) {
|
||||
console.log(`Drifted ${drift.toFixed(0)} tiles, walking back`);
|
||||
await ctx.bot.walkTo(START_AREA.x, START_AREA.z);
|
||||
await bot.walkTo(START_AREA.x, START_AREA.z);
|
||||
}
|
||||
```
|
||||
+17
-14
@@ -7,7 +7,8 @@ Successful patterns for mining automation.
|
||||
Rocks are **locations** (not NPCs). Filter for rocks with a "Mine" option:
|
||||
|
||||
```typescript
|
||||
const rock = state.nearbyLocs
|
||||
const state = sdk.getState();
|
||||
const rock = state?.nearbyLocs
|
||||
.filter(loc => /rocks?/i.test(loc.name))
|
||||
.filter(loc => loc.optionsWithIndex.some(o => /^mine$/i.test(o.text)))
|
||||
.sort((a, b) => a.distance - b.distance)[0];
|
||||
@@ -16,23 +17,23 @@ const rock = state.nearbyLocs
|
||||
## Mining Action
|
||||
|
||||
```typescript
|
||||
// Walk closer if needed (interaction range is ~3 tiles)
|
||||
if (rock.distance > 3) {
|
||||
await ctx.sdk.sendWalk(rock.x, rock.z, true);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
const mineOpt = rock.optionsWithIndex.find(o => /^mine$/i.test(o.text));
|
||||
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, mineOpt.opIndex);
|
||||
if (!rock) throw new Error('No mineable rock nearby');
|
||||
const result = await bot.interactLoc(rock, 'mine');
|
||||
if (!result.success) console.warn(result.reason, result.message);
|
||||
```
|
||||
|
||||
For lower-level control, `sendInteractLoc()` only confirms client dispatch. Take
|
||||
an XP/inventory baseline and wait for the intended change rather than sleeping
|
||||
for an arbitrary duration.
|
||||
|
||||
## Detecting Mining Activity
|
||||
|
||||
Animation ID 625 indicates active mining:
|
||||
|
||||
```typescript
|
||||
const isMining = state.player?.animId === 625;
|
||||
const isIdle = state.player?.animId === -1;
|
||||
const animation = sdk.getState()?.player?.animId ?? -1;
|
||||
const isMining = animation === 625;
|
||||
const isIdle = animation === -1;
|
||||
```
|
||||
|
||||
|
||||
@@ -53,8 +54,8 @@ const isIdle = state.player?.animId === -1;
|
||||
## Counting Ore
|
||||
|
||||
```typescript
|
||||
function countOre(ctx): number {
|
||||
const state = ctx.sdk.getState();
|
||||
function countOre() {
|
||||
const state = sdk.getState();
|
||||
if (!state) return 0;
|
||||
return state.inventory
|
||||
.filter(i => /ore$/i.test(i.name))
|
||||
@@ -65,10 +66,12 @@ function countOre(ctx): number {
|
||||
## Drop When Full
|
||||
|
||||
```typescript
|
||||
const state = sdk.getState();
|
||||
if (!state) throw new Error('No world state');
|
||||
if (state.inventory.length >= 28) {
|
||||
const ores = state.inventory.filter(i => /ore$/i.test(i.name));
|
||||
for (const ore of ores) {
|
||||
await ctx.sdk.sendDropItem(ore.slot);
|
||||
await sdk.sendDropItem(ore.slot);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -37,19 +37,19 @@ Cow hides are worth ~100 GP each normally, but the Lumbridge general store pays
|
||||
|
||||
```typescript
|
||||
// Find shopkeeper
|
||||
const shopkeeper = ctx.sdk.getState()?.nearbyNpcs.find(n => /shopkeeper/i.test(n.name));
|
||||
const shopkeeper = sdk.getState()?.nearbyNpcs.find(n => /shopkeeper/i.test(n.name));
|
||||
if (!shopkeeper) return;
|
||||
|
||||
// Find Trade option
|
||||
const tradeOpt = shopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
|
||||
if (tradeOpt) {
|
||||
await ctx.sdk.sendInteractNpc(shopkeeper.index, tradeOpt.opIndex);
|
||||
await sdk.sendInteractNpc(shopkeeper.index, tradeOpt.opIndex);
|
||||
}
|
||||
|
||||
// Wait for shop interface
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
if (ctx.sdk.getState()?.shop?.isOpen) {
|
||||
if (sdk.getState()?.shop?.isOpen) {
|
||||
console.log('Shop opened!');
|
||||
break;
|
||||
}
|
||||
@@ -60,16 +60,16 @@ for (let i = 0; i < 15; i++) {
|
||||
|
||||
```typescript
|
||||
// Shop must be open first
|
||||
if (!ctx.sdk.getState()?.shop?.isOpen) {
|
||||
if (!sdk.getState()?.shop?.isOpen) {
|
||||
console.log('Shop not open!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find item to sell in inventory
|
||||
const item = ctx.sdk.getState()?.inventory.find(i => /^cow hide$/i.test(i.name));
|
||||
const item = sdk.getState()?.inventory.find(i => /^cow hide$/i.test(i.name));
|
||||
if (item) {
|
||||
// Sell item (slot, quantity)
|
||||
await ctx.sdk.sendShopSell(item.slot, item.count);
|
||||
await sdk.sendShopSell(item.slot, item.count);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
```
|
||||
@@ -78,9 +78,9 @@ if (item) {
|
||||
|
||||
```typescript
|
||||
// Find item in shop stock
|
||||
const shopItem = ctx.sdk.getState()?.shop?.shopItems?.find(i => /sword/i.test(i.name));
|
||||
const shopItem = sdk.getState()?.shop?.shopItems?.find(i => /sword/i.test(i.name));
|
||||
if (shopItem && shopItem.count > 0) {
|
||||
await ctx.sdk.sendShopBuy(shopItem.slot, 1);
|
||||
await sdk.sendShopBuy(shopItem.slot, 1);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
```
|
||||
|
||||
+20
-29
@@ -6,18 +6,26 @@ Bronze bars require **1 copper ore + 1 tin ore** smelted at a furnace.
|
||||
|
||||
### How to Smelt
|
||||
|
||||
Furnaces have **no click options** — use `sendUseItemOnLoc` with ore:
|
||||
Some furnace variants expose a `Smelt` option and others require using ore on
|
||||
the furnace. Inspect `optionsWithIndex` first; using ore on the furnace is the
|
||||
portable low-level fallback:
|
||||
|
||||
```typescript
|
||||
const state = sdk.getState();
|
||||
if (!state) throw new Error('No world state');
|
||||
const copper = state.inventory.find(i => /copper ore/i.test(i.name));
|
||||
const furnace = state.nearbyLocs.find(l => /furnace/i.test(l.name));
|
||||
if (!copper || !furnace) throw new Error('Copper ore and a nearby furnace are required');
|
||||
await sdk.sendUseItemOnLoc(copper.slot, furnace.x, furnace.z, furnace.id);
|
||||
await new Promise(r => setTimeout(r, 2500));
|
||||
await sdk.waitForCondition(
|
||||
next => next.inventory.some(item => /bronze bar/i.test(item.name)),
|
||||
10_000,
|
||||
);
|
||||
```
|
||||
|
||||
- Use **copper** on furnace — it auto-consumes 1 tin from inventory
|
||||
- Triggers level-up dialogs — handled automatically by BotActions methods
|
||||
- ~2.5s per bar is reliable timing
|
||||
- Low-level calls do not dismiss level-up dialogs; use `bot.dismissBlockingUI()`
|
||||
when a modal interrupts a batch
|
||||
- Each bronze bar gives **155 xp** (at level 1-14 range, leveled from 1→14 with 10 bars = 1550 xp total, so ~155 xp per smelt)
|
||||
|
||||
### Furnace Locations
|
||||
@@ -36,20 +44,14 @@ Also anvils at (3188, 3424) and (3188, 3426) — same building, same id 2783.
|
||||
|
||||
### How to Smith at Anvil
|
||||
|
||||
Anvils have **no click options** — use `sendUseItemOnLoc` with a bar:
|
||||
Prefer the high-level helper, which opens the interface and observes Smithing XP:
|
||||
|
||||
```typescript
|
||||
const bar = state.inventory.find(i => /bronze bar/i.test(i.name));
|
||||
const anvil = state.nearbyLocs.find(l => /anvil/i.test(l.name));
|
||||
await sdk.sendUseItemOnLoc(bar.slot, anvil.x, anvil.z, anvil.id);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// Interface 994 opens with smithing options
|
||||
// Components 1119-1122: individual items [Make, Make 5, Make 10]
|
||||
// Component 1123: sets [Make set, Make 5 sets, Make 10 sets]
|
||||
// Component 1119 = dagger (1 bar each)
|
||||
await sdk.sendClickComponentWithOption(1119, 1); // Make 1 dagger
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
const result = await bot.smithAtAnvil('dagger', {
|
||||
barPattern: /bronze bar/i,
|
||||
timeout: 10_000,
|
||||
});
|
||||
if (!result.success) console.warn(result.reason, result.message);
|
||||
```
|
||||
|
||||
**Requires hammer in inventory** (buy from Lumbridge General Store for 1gp).
|
||||
@@ -60,15 +62,6 @@ await new Promise(r => setTimeout(r, 3000));
|
||||
- 10 bars smelted: 1→14 Smithing
|
||||
- 10 daggers smithed: 14→26 Smithing
|
||||
|
||||
### Smithing Interface Components (Interface 994)
|
||||
| Component | Item | Bars needed |
|
||||
|-----------|------|-------------|
|
||||
| 1119 | Dagger | 1 |
|
||||
| 1120 | ? (needs testing) | ? |
|
||||
| 1121 | ? (needs testing) | ? |
|
||||
| 1122 | ? (needs testing) | ? |
|
||||
| 1123 | Set | ? |
|
||||
|
||||
### Workflow
|
||||
1. Mine equal copper (rock ids 2090/2091) and tin (rock ids 2093/2094) at SE Varrock mine (3285, 3365)
|
||||
2. Walk to Lumbridge furnace (3225, 3256) → smelt into bronze bars
|
||||
@@ -85,7 +78,5 @@ await new Promise(r => setTimeout(r, 3000));
|
||||
### Route: Mine → Furnace → Anvil
|
||||
This is a long loop. Consider using Varrock West Bank (nearby anvils) to store bars between trips.
|
||||
|
||||
## TODO
|
||||
- Test iron smelting (rock ids 2093/2095) — need Mining 15+
|
||||
- Discover what components 1120-1122 make
|
||||
- Find a closer furnace to Varrock anvils (Al Kharid?)
|
||||
Use `sdk/API.md` for the current `smithAtAnvil` product and option signature
|
||||
rather than hard-coded interface component IDs.
|
||||
+29
-21
@@ -16,7 +16,7 @@ Men at Lumbridge castle are excellent for early thieving. Proven: 1 → 43 in ~1
|
||||
|
||||
```typescript
|
||||
// Find a man to pickpocket
|
||||
const man = ctx.sdk.getState()?.nearbyNpcs.find(n => /^man$/i.test(n.name));
|
||||
const man = sdk.getState()?.nearbyNpcs.find(n => /^man$/i.test(n.name));
|
||||
if (!man) {
|
||||
console.log('No man found nearby');
|
||||
return;
|
||||
@@ -30,7 +30,7 @@ if (!pickpocketOpt) {
|
||||
}
|
||||
|
||||
// Execute pickpocket
|
||||
await ctx.sdk.sendInteractNpc(man.index, pickpocketOpt.opIndex);
|
||||
await sdk.sendInteractNpc(man.index, pickpocketOpt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500)); // Wait for result
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ When caught, the character is stunned for ~5 seconds:
|
||||
|
||||
```typescript
|
||||
// Check for stun (player can't act)
|
||||
const messages = ctx.sdk.getState()?.gameMessages ?? [];
|
||||
const messages = sdk.getState()?.gameMessages ?? [];
|
||||
const wasStunned = messages.some(m => /stunned|caught/i.test(m.text));
|
||||
|
||||
if (wasStunned) {
|
||||
@@ -63,22 +63,22 @@ if (wasStunned) {
|
||||
### Full Thieving Loop
|
||||
|
||||
```typescript
|
||||
async function pickpocketLoop(ctx, duration: number) {
|
||||
async function pickpocketLoop(duration) {
|
||||
const startTime = Date.now();
|
||||
let successCount = 0;
|
||||
|
||||
while (Date.now() - startTime < duration) {
|
||||
// Dismiss any dialogs first
|
||||
if (ctx.sdk.getState()?.dialog.isOpen) {
|
||||
await ctx.sdk.sendClickDialog(0);
|
||||
if (sdk.getState()?.dialog.isOpen) {
|
||||
await sdk.sendClickDialog(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find target
|
||||
const man = ctx.sdk.getState()?.nearbyNpcs.find(n => /^man$/i.test(n.name));
|
||||
const man = sdk.getState()?.nearbyNpcs.find(n => /^man$/i.test(n.name));
|
||||
if (!man) {
|
||||
// Walk to Lumbridge castle
|
||||
await ctx.bot.walkTo(3222, 3218);
|
||||
await bot.walkTo(3222, 3218);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
continue;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ async function pickpocketLoop(ctx, duration: number) {
|
||||
// Pickpocket
|
||||
const opt = man.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
|
||||
if (opt) {
|
||||
await ctx.sdk.sendInteractNpc(man.index, opt.opIndex);
|
||||
await sdk.sendInteractNpc(man.index, opt.opIndex);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
successCount++;
|
||||
}
|
||||
@@ -104,12 +104,12 @@ Bank when you hit 200-500 GP to avoid losing progress on disconnect:
|
||||
const GP_BANK_THRESHOLD = 500;
|
||||
|
||||
// Check GP in inventory
|
||||
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
const coins = sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
|
||||
const gp = coins?.count ?? 0;
|
||||
|
||||
if (gp >= GP_BANK_THRESHOLD) {
|
||||
console.log(`Have ${gp} GP - banking!`);
|
||||
await bankTrip(ctx); // Walk to Draynor, deposit
|
||||
await bankTrip(); // Walk to Draynor, deposit
|
||||
}
|
||||
```
|
||||
|
||||
@@ -133,32 +133,40 @@ const MIN_KEBABS = 3;
|
||||
const EAT_HP_THRESHOLD = 7;
|
||||
|
||||
// Check if we need food
|
||||
const hp = getHP(ctx);
|
||||
const kebabCount = getKebabCount(ctx);
|
||||
const state = sdk.getState();
|
||||
if (!state?.player) throw new Error('No player state');
|
||||
const hp = state.player.hp;
|
||||
const kebabCount = state.inventory
|
||||
.filter(item => /kebab/i.test(item.name))
|
||||
.reduce((total, item) => total + item.count, 0);
|
||||
const coins = state.inventory.find(item => /coins/i.test(item.name))?.count ?? 0;
|
||||
|
||||
if (hp.current <= EAT_HP_THRESHOLD) {
|
||||
if (hp <= EAT_HP_THRESHOLD) {
|
||||
// Eat food if available
|
||||
const food = ctx.sdk.getState()?.inventory.find(i => /kebab/i.test(i.name));
|
||||
const food = sdk.getState()?.inventory.find(i => /kebab/i.test(i.name));
|
||||
if (food) {
|
||||
const eatOpt = food.optionsWithIndex.find(o => /eat/i.test(o.text));
|
||||
await ctx.sdk.sendUseItem(food.slot, eatOpt.opIndex);
|
||||
if (eatOpt) await sdk.sendUseItem(food.slot, eatOpt.opIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Restock kebabs if low
|
||||
if (kebabCount < MIN_KEBABS && getCoins(ctx) >= 3) {
|
||||
await ctx.bot.walkTo(3273, 3180); // Kebab seller
|
||||
if (kebabCount < MIN_KEBABS && coins >= 3) {
|
||||
await bot.walkTo(3273, 3180); // Kebab seller
|
||||
// ... buy kebab dialog (see dialogs.md)
|
||||
}
|
||||
|
||||
// Walk to the warriors if not nearby
|
||||
const distToTargets = /* calculate distance to (3293, 3170) */;
|
||||
const distToTargets = Math.hypot(
|
||||
state.player.worldX - 3293,
|
||||
state.player.worldZ - 3170,
|
||||
);
|
||||
if (distToTargets > 15) {
|
||||
await ctx.bot.walkTo(3293, 3170);
|
||||
await bot.walkTo(3293, 3170);
|
||||
}
|
||||
|
||||
// Pickpocket - match the actual NPC name, /^man$/i finds nothing here
|
||||
const target = ctx.sdk.getState()?.nearbyNpcs.find(n => /^al-kharid warrior$/i.test(n.name));
|
||||
const target = sdk.getState()?.nearbyNpcs.find(n => /^al-kharid warrior$/i.test(n.name));
|
||||
// ... standard pickpocket pattern
|
||||
```
|
||||
|
||||
|
||||
+28
-23
@@ -4,15 +4,17 @@ Successful patterns for movement and pathfinding.
|
||||
|
||||
## Basic Walking
|
||||
|
||||
For short distances (<30 tiles), `bot.walkTo()` works directly:
|
||||
`bot.walkTo()` performs pathfinding for both short and long routes:
|
||||
|
||||
```typescript
|
||||
await ctx.bot.walkTo(3222, 3218); // Walk to Lumbridge
|
||||
const result = await bot.walkTo(3222, 3218); // Walk to Lumbridge
|
||||
if (!result.success) console.warn(result.reason, result.message);
|
||||
```
|
||||
|
||||
## Long Distance Walking
|
||||
|
||||
For distances >30 tiles, use waypoints with 20-25 tile segments:
|
||||
`walkTo()` supports long routes. Use waypoints when you intentionally need a
|
||||
safer route, want progress checkpoints, or need to avoid a dangerous area:
|
||||
|
||||
```typescript
|
||||
const WAYPOINTS_TO_BANK = [
|
||||
@@ -23,14 +25,14 @@ const WAYPOINTS_TO_BANK = [
|
||||
{ x: 3185, z: 3436 }, // Final destination
|
||||
];
|
||||
|
||||
async function walkWaypoints(ctx, waypoints) {
|
||||
async function walkWaypoints(waypoints) {
|
||||
for (const wp of waypoints) {
|
||||
// Try up to 3 times per waypoint
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await ctx.bot.walkTo(wp.x, wp.z);
|
||||
await bot.walkTo(wp.x, wp.z);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
const player = ctx.sdk.getState()?.player;
|
||||
const player = sdk.getState()?.player;
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(player.worldX - wp.x, 2) +
|
||||
Math.pow(player.worldZ - wp.z, 2)
|
||||
@@ -47,9 +49,9 @@ async function walkWaypoints(ctx, waypoints) {
|
||||
Always check if you actually arrived:
|
||||
|
||||
```typescript
|
||||
const result = await ctx.bot.walkTo(targetX, targetZ);
|
||||
const result = await bot.walkTo(targetX, targetZ);
|
||||
|
||||
const player = ctx.sdk.getState()?.player;
|
||||
const player = sdk.getState()?.player;
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(player.worldX - targetX, 2) +
|
||||
Math.pow(player.worldZ - targetZ, 2)
|
||||
@@ -82,17 +84,20 @@ const BANK_TO_MINE = [
|
||||
];
|
||||
```
|
||||
|
||||
## Opening Obstacles (CRITICAL!)
|
||||
## Opening Obstacles
|
||||
|
||||
**MUST open doors/gates BEFORE attempting to walk through.** The pathfinder cannot handle closed gates and will get stuck.
|
||||
`bot.walkTo()` attempts to open doors and gates on its route. If a route fails,
|
||||
inspect nearby objects and open the specific obstacle before retrying:
|
||||
|
||||
```typescript
|
||||
// WRONG - pathfinder gets stuck at closed gate
|
||||
await ctx.bot.walkTo(3250, 3260); // Will fail if gate is closed
|
||||
|
||||
// CORRECT - open gate first, then walk
|
||||
await ctx.bot.openDoor(/gate/i);
|
||||
await ctx.bot.walkTo(3250, 3260); // Now works!
|
||||
let result = await bot.walkTo(3250, 3260);
|
||||
if (!result.success) {
|
||||
const gate = sdk.findNearbyLoc(/^gate$/i);
|
||||
if (gate?.optionsWithIndex.some(option => /^open$/i.test(option.text))) {
|
||||
await bot.openDoor(gate);
|
||||
result = await bot.walkTo(3250, 3260);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -111,12 +116,12 @@ Stay **north of z=3240** when walking west from cow field to Draynor. Use `walkT
|
||||
|
||||
```typescript
|
||||
// Safe: curve north around Dark Wizards
|
||||
await ctx.bot.walkTo(3230, 3270); // Go west, stay north
|
||||
await ctx.bot.walkTo(3150, 3250); // Continue west
|
||||
await ctx.bot.walkTo(3092, 3243); // Draynor Bank
|
||||
await bot.walkTo(3230, 3270); // Go west, stay north
|
||||
await bot.walkTo(3150, 3250); // Continue west
|
||||
await bot.walkTo(3092, 3243); // Draynor Bank
|
||||
|
||||
// DANGEROUS: straight line cuts through wizard area!
|
||||
await ctx.bot.walkTo(3092, 3243); // May path through (3220, 3220)
|
||||
await bot.walkTo(3092, 3243); // May path through (3220, 3220)
|
||||
```
|
||||
|
||||
## Key Coordinates
|
||||
@@ -125,7 +130,7 @@ await ctx.bot.walkTo(3092, 3243); // May path through (3220, 3220)
|
||||
|----------|-------------|
|
||||
| Lumbridge spawn | (3222, 3218) |
|
||||
| Lumbridge cows (field center) | (3253, 3290) |
|
||||
| Cow field gate | (3253, 3270) |
|
||||
| Cow field south gate | (3253, 3266) |
|
||||
| Draynor fishing | (3087, 3230) |
|
||||
| Varrock West bank | (3185, 3436) |
|
||||
| SE Varrock mine | (3285, 3365) |
|
||||
@@ -152,8 +157,8 @@ await ctx.bot.walkTo(3092, 3243); // May path through (3220, 3220)
|
||||
```typescript
|
||||
// Simple check: x >= 3270 means inside Al Kharid
|
||||
// BUT note the bank is at x=3269, so use x >= 3267 for safety
|
||||
function isInAlKharid(ctx): boolean {
|
||||
const player = ctx.sdk.getState()?.player;
|
||||
function isInAlKharid() {
|
||||
const player = sdk.getState()?.player;
|
||||
if (!player) return false;
|
||||
return player.worldX >= 3267 && player.worldZ < 3220;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Successful patterns for woodcutting automation.
|
||||
Trees are **locations** with "Chop down" option:
|
||||
|
||||
```typescript
|
||||
const tree = state.nearbyLocs
|
||||
const tree = sdk.getState()?.nearbyLocs
|
||||
.filter(loc => /^tree$/i.test(loc.name))
|
||||
.filter(loc => loc.optionsWithIndex.some(o => /chop/i.test(o.text)))
|
||||
.sort((a, b) => a.distance - b.distance)[0];
|
||||
@@ -17,11 +17,11 @@ const tree = state.nearbyLocs
|
||||
|
||||
```typescript
|
||||
// Using porcelain method
|
||||
const result = await ctx.bot.chopTree();
|
||||
const result = await bot.chopTree();
|
||||
|
||||
// Or raw SDK
|
||||
const chopOpt = tree.optionsWithIndex.find(o => /chop/i.test(o.text));
|
||||
await ctx.sdk.sendInteractLoc(tree.x, tree.z, tree.id, chopOpt.opIndex);
|
||||
await sdk.sendInteractLoc(tree.x, tree.z, tree.id, chopOpt.opIndex);
|
||||
```
|
||||
|
||||
## Tree Locations
|
||||
@@ -40,24 +40,27 @@ Stay within a reasonable area:
|
||||
const TREE_AREA = { x: 3195, z: 3220 };
|
||||
const MAX_DRIFT = 15;
|
||||
|
||||
const player = state.player;
|
||||
const player = sdk.getState()?.player;
|
||||
if (!player) throw new Error('No player state');
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(player.worldX - TREE_AREA.x, 2) +
|
||||
Math.pow(player.worldZ - TREE_AREA.z, 2)
|
||||
);
|
||||
|
||||
if (dist > MAX_DRIFT) {
|
||||
await ctx.bot.walkTo(TREE_AREA.x, TREE_AREA.z);
|
||||
await bot.walkTo(TREE_AREA.x, TREE_AREA.z);
|
||||
}
|
||||
```
|
||||
|
||||
## Drop Logs When Full
|
||||
|
||||
```typescript
|
||||
const state = sdk.getState();
|
||||
if (!state) throw new Error('No world state');
|
||||
if (state.inventory.length >= 28) {
|
||||
const logs = state.inventory.filter(i => /logs$/i.test(i.name));
|
||||
for (const log of logs) {
|
||||
await ctx.sdk.sendDropItem(log.slot);
|
||||
await sdk.sendDropItem(log.slot);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
@@ -68,11 +71,11 @@ if (state.inventory.length >= 28) {
|
||||
Burn logs for additional XP (requires tinderbox):
|
||||
|
||||
```typescript
|
||||
const logs = ctx.sdk.findInventoryItem(/logs/i);
|
||||
const tinderbox = ctx.sdk.findInventoryItem(/tinderbox/i);
|
||||
const logs = sdk.findInventoryItem(/logs/i);
|
||||
const tinderbox = sdk.findInventoryItem(/tinderbox/i);
|
||||
|
||||
if (logs && tinderbox) {
|
||||
const result = await ctx.bot.burnLogs();
|
||||
const result = await bot.burnLogs();
|
||||
if (result.success) {
|
||||
console.log(`Burned logs, gained ${result.xpGained} FM XP`);
|
||||
}
|
||||
|
||||
+8
-1
@@ -2,7 +2,14 @@
|
||||
"name": "server",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"fetch-collision-data": "bun sdk/fetch-collision-data.ts"
|
||||
"check": "bun run typecheck && bun run typecheck:webclient && bun run test:focused && bun run docs:api:check",
|
||||
"docs:api": "bun sdk/generate-api-docs.ts",
|
||||
"docs:api:check": "bun sdk/generate-api-docs.ts --check",
|
||||
"fetch-collision-data": "bun sdk/fetch-collision-data.ts",
|
||||
"test:docs": "bun test sdk/test/api-docs.test.ts sdk/test/docs-snippets.test.ts",
|
||||
"test:focused": "bun sdk/test/chat-history.test.ts && bun sdk/test/click-dialog-by-text.test.ts && bun run test:docs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:webclient": "tsc --noEmit -p server/webclient/tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
||||
+368
-233
@@ -1,267 +1,273 @@
|
||||
# SDK API Reference
|
||||
|
||||
> Auto-generated from source. Do not edit directly.
|
||||
> Run `bun sdk/generate-api-docs.ts` to regenerate.
|
||||
> Auto-generated from TypeScript source. Do not edit directly.
|
||||
> Run `bun run docs:api` to regenerate or `bun run docs:api:check` to verify it.
|
||||
|
||||
## BotActions (High-Level)
|
||||
## Execution model
|
||||
|
||||
These methods wait for the **effect to complete**, not just server acknowledgment.
|
||||
- `bot.*` methods are high-level helpers. They attempt to observe method-specific evidence such as movement, inventory, XP, dialog, or state changes. When a method returns a result, inspect its `success` and any `reason` or `message`: the strength of completion evidence varies by method.
|
||||
- `sdk.send*` methods are low-level browser-client dispatches. A successful `ActionResult` means the enhanced browser client accepted and dispatched the command; it does **not** prove that the game server processed it or that the intended game effect occurred.
|
||||
- Methods whose signature returns `Promise<...>` are asynchronous and must be awaited. In particular, `scanNearbyLocs()` and `scanGroundItems()` return promises.
|
||||
- `getInventory().length` is the number of occupied slots. `InventoryItem.count` is the quantity in that one slot, and `findInventoryItem()` returns the first matching slot rather than an aggregate across matching slots.
|
||||
|
||||
## BotActions (high-level)
|
||||
|
||||
### UI & Dialog
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `skipTutorial(options)` | Skip tutorial by navigating dialogs and talking to tutorial NPCs. |
|
||||
| `dismissBlockingUI()` | Dismiss any blocking UI like level-up dialogs. |
|
||||
| `waitForDialogClose(timeout)` | Wait for dialog to close. |
|
||||
|
||||
### Movement
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `walkTo(x, z, tolerance)` | Walk to coordinates using pathfinding, auto-opening doors. |
|
||||
|
||||
### Combat & Equipment
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `equipItem(target)` | Equip an item from inventory. |
|
||||
| `unequipItem(target)` | Unequip an item to inventory. |
|
||||
| `getEquipment()` | Get all currently equipped items. |
|
||||
| `findEquippedItem(pattern)` | Find an equipped item by name pattern. |
|
||||
| `eatFood(target)` | Eat food to restore hitpoints. |
|
||||
| `attackNpc(target, timeout)` | Attack an NPC, walking to it if needed. |
|
||||
| `castSpellOnNpc(target, spellComponent, timeout)` | Cast a combat spell on an NPC. |
|
||||
| `craftLeather(product?)` | Craft leather into armour using needle and thread. |
|
||||
|
||||
### Woodcutting & Firemaking
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `chopTree(target?)` | Chop a tree and wait for logs to appear in inventory. |
|
||||
| `burnLogs(logsTarget?)` | Burn logs using a tinderbox, wait for firemaking XP. |
|
||||
|
||||
### Items & Inventory
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `pickupItem(target)` | Pick up an item from the ground. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async skipTutorial(options: { randomizeAppearance?: boolean } = {}): Promise<ActionResult>` | Skip tutorial by navigating dialogs and talking to tutorial NPCs. This is a porcelain method - domain logic that was moved from bot client. |
|
||||
| `async dismissBlockingUI(): Promise<void>` | Dismiss any blocking UI like level-up dialogs. |
|
||||
| `async waitForDialogClose(timeout: number = 30000): Promise<void>` | Wait for dialog to close. |
|
||||
| `async navigateDialog(choices: (number \| string \| RegExp)[]): Promise<void>` | _No description provided._ |
|
||||
|
||||
### Doors
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `openDoor(target?)` | Open a door or gate, walking to it if needed. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async openDoor(target?: NearbyLoc \| string \| RegExp): Promise<OpenDoorResult>` | Open a door or gate, walking to it if needed. |
|
||||
|
||||
### NPC Interaction
|
||||
### Other
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `talkTo(target)` | Talk to an NPC and wait for dialog to open. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async useItemOnLoc(item: InventoryItem \| string \| RegExp, loc: NearbyLoc \| string \| RegExp, options: { timeout?: number } = {}): Promise<UseItemOnLocResult>` | Use an inventory item on a nearby location (e.g., fish on range, ore on furnace). Walks to the location first (handling doors), then uses the item. |
|
||||
| `async useItemOnNpc(item: InventoryItem \| string \| RegExp, npc: NearbyNpc \| string \| RegExp, options: { timeout?: number } = {}): Promise<UseItemOnNpcResult>` | Use an inventory item on a nearby NPC (e.g., bones on altar keeper, item on NPC). Walks to the NPC first (handling doors), then uses the item. |
|
||||
| `async closeInterface(timeout: number = 5000): Promise<ActionResult>` | Close any open modal interface (bank, book, quest scroll, etc.). |
|
||||
|
||||
### Woodcutting & Firemaking
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async chopTree(target?: NearbyLoc \| string \| RegExp): Promise<ChopTreeResult>` | Chop a tree and wait for logs to appear in inventory. |
|
||||
| `async burnLogs(logsTarget?: InventoryItem \| string \| RegExp): Promise<BurnLogsResult>` | Burn logs using a tinderbox, wait for firemaking XP. |
|
||||
|
||||
### Items & Inventory
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async pickupItem(target: GroundItem \| string \| RegExp): Promise<PickupResult>` | Pick up an item from the ground. |
|
||||
|
||||
### NPC & Object Interaction
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async talkTo(target: NearbyNpc \| string \| RegExp): Promise<TalkResult>` | Talk to an NPC and wait for dialog to open. Walks to the NPC first (handling doors). |
|
||||
| `async interactLoc(target: NearbyLoc \| string \| RegExp, option: number \| string \| RegExp = 1): Promise<InteractLocResult>` | Interact with a nearby location object (rock, fishing spot, furnace, etc.). Walks to the target first (handling doors), sends the interaction, then waits for an effect (animation, dialog, interface) or detects failure when the player has been idle for 2 ticks with nothing happening. |
|
||||
| `async interactNpc(target: NearbyNpc \| string \| RegExp, option: number \| string \| RegExp = 1): Promise<InteractNpcResult>` | Interact with a nearby NPC using a specified option (e.g. "Trade", "Pickpocket", "Fish"). Walks to the NPC first (handling doors), sends the interaction, then waits for an effect (animation, dialog, interface) or detects failure when the player has been idle for 2 ticks with nothing happening. |
|
||||
| `async pickpocketNpc(target: NearbyNpc \| string \| RegExp): Promise<PickpocketResult>` | Pickpocket an NPC. Handles door retrying if path is blocked. |
|
||||
|
||||
### Movement
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async walkTo(x: number, z: number, tolerance: number = 3): Promise<ActionResult>` | Walk to coordinates using pathfinding, auto-opening doors. |
|
||||
|
||||
### Shopping
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `closeShop(timeout)` | Close the shop interface. |
|
||||
| `openShop(target)` | Open a shop by trading with an NPC. |
|
||||
| `buyFromShop(target, amount)` | Buy an item from an open shop . |
|
||||
| `sellToShop(target, amount)` | Sell an item to an open shop. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async closeShop(timeout: number = 5000): Promise<ActionResult>` | Close the shop interface. |
|
||||
| `async openShop(target: NearbyNpc \| string \| RegExp = /shop\s*keeper/i): Promise<ActionResult>` | Open a shop by trading with an NPC. |
|
||||
| `async buyFromShop(target: ShopItem \| string \| RegExp, amount: number = 1): Promise<ShopResult>` | Buy an item from an open shop . |
|
||||
| `async sellToShop(target: InventoryItem \| ShopItem \| string \| RegExp, amount: SellAmount = 1): Promise<ShopSellResult>` | Sell an item to an open shop. |
|
||||
|
||||
### Banking
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `openBank(timeout)` | Open a bank booth or talk to a banker. |
|
||||
| `closeBank(timeout)` | Close the bank interface. |
|
||||
| `depositItem(target, amount)` | Deposit an item into the bank. |
|
||||
| `withdrawItem(target, amount)` | Withdraw an item from the bank by slot number. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async openBank(timeout: number = 10000): Promise<OpenBankResult>` | Open a bank booth or talk to a banker. |
|
||||
| `async closeBank(timeout: number = 5000): Promise<ActionResult>` | Close the bank interface. |
|
||||
| `async depositItem(target: InventoryItem \| string \| RegExp, amount: number = -1): Promise<BankDepositResult>` | Deposit an item into the bank. Use -1 for all. |
|
||||
| `async withdrawItem(target: BankItem \| string \| RegExp \| number, amount: number = 1): Promise<BankWithdrawResult>` | Withdraw an item from the bank by slot number. |
|
||||
|
||||
### Combat & Equipment
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async equipItem(target: InventoryItem \| string \| RegExp): Promise<EquipResult>` | Equip an item from inventory. |
|
||||
| `async unequipItem(target: InventoryItem \| string \| RegExp): Promise<UnequipResult>` | Unequip an item to inventory. |
|
||||
| `getEquipment(): InventoryItem[]` | Get all currently equipped items. |
|
||||
| `findEquippedItem(pattern: string \| RegExp): InventoryItem \| null` | Find an equipped item by name pattern. |
|
||||
| `async eatFood(target: InventoryItem \| string \| RegExp): Promise<EatResult>` | Eat food to restore hitpoints. |
|
||||
| `async attackNpc(target: NearbyNpc \| string \| RegExp, timeout: number = 5000): Promise<AttackResult>` | Attack an NPC, walking to it if needed. |
|
||||
| `async castSpellOnNpc(target: NearbyNpc \| string \| RegExp, spellComponent: number, timeout: number = 3000): Promise<CastSpellResult>` | Cast a combat spell on an NPC. |
|
||||
|
||||
### Condition Waiting
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async waitForSkillLevel(skillName: string, targetLevel: number, timeout: number = 60000): Promise<SkillState>` | Wait until a skill reaches a target level. |
|
||||
| `async waitForInventoryItem(pattern: string \| RegExp, timeout: number = 30000): Promise<InventoryItem>` | Wait until an item appears in inventory. |
|
||||
| `async waitForIdle(timeout: number = 10000): Promise<void>` | Wait for player to stop moving. |
|
||||
|
||||
### Crafting & Smithing
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `fletchLogs(product?)` | Fletch logs into bows or arrow shafts using a knife. |
|
||||
| `smithAtAnvil(product, options)` | Smith a bar into an item at an anvil. |
|
||||
| `craftJewelry(options: {
|
||||
barPattern?: RegExp;
|
||||
product?: string;
|
||||
gem?: string;
|
||||
timeout?: number;
|
||||
} = {})` | Craft jewelry at a furnace using a gold/silver bar and optional gem. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async fletchLogs(product?: string): Promise<FletchResult>` | Fletch logs into bows or arrow shafts using a knife. |
|
||||
| `async craftLeather(product?: string): Promise<CraftLeatherResult>` | Craft leather into armour using needle and thread. |
|
||||
| `async smithAtAnvil(product: string \| number = 'dagger', options: { barPattern?: RegExp; timeout?: number } = {}): Promise<SmithResult>` | Smith a bar into an item at an anvil. |
|
||||
| `async craftJewelry(options: { barPattern?: RegExp; product?: string; gem?: string; timeout?: number; } = {}): Promise<CraftJewelryResult>` | Craft jewelry at a furnace using a gold/silver bar and optional gem. Requires: bar + mould in inventory (ring mould, necklace mould, or amulet mould). Optionally a gem for gem-set jewelry. |
|
||||
| `async enchantItem(target: InventoryItem \| string \| RegExp, level: 1 \| 2 \| 3 \| 4 \| 5, options: { timeout?: number } = {}): Promise<EnchantResult>` | Cast an enchantment spell on a jewelry item. |
|
||||
| `async stringAmulet(target: InventoryItem \| string \| RegExp = /amulet/i, options: { timeout?: number } = {}): Promise<StringAmuletResult>` | String an amulet using a ball of wool. |
|
||||
|
||||
### Condition Waiting
|
||||
### Prayer
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `waitForSkillLevel(skillName, targetLevel, timeout)` | Wait until a skill reaches a target level. |
|
||||
| `waitForInventoryItem(pattern, timeout)` | Wait until an item appears in inventory. |
|
||||
| `waitForIdle(timeout)` | Wait for player to stop moving. |
|
||||
|
||||
### Other
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `useItemOnLoc(item, loc, options)` | Use an inventory item on a nearby location (e. |
|
||||
| `useItemOnNpc(item, npc, options)` | Use an inventory item on a nearby NPC (e. |
|
||||
| `closeInterface(timeout)` | Close any open modal interface (bank, book, quest scroll, etc. |
|
||||
| `interactLoc(target, option)` | Interact with a nearby location object (rock, fishing spot, furnace, etc. |
|
||||
| `interactNpc(target, option)` | Interact with a nearby NPC using a specified option (e. |
|
||||
| `pickpocketNpc(target)` | Pickpocket an NPC. |
|
||||
| `activatePrayer(prayer)` | Activate a prayer by name or index. |
|
||||
| `deactivatePrayer(prayer)` | Deactivate a prayer by name or index. |
|
||||
| `deactivateAllPrayers()` | Deactivate all currently active prayers. |
|
||||
| `enchantItem(target, level, options)` | Cast an enchantment spell on a jewelry item. |
|
||||
| `stringAmulet(target, options)` | String an amulet using a ball of wool. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async activatePrayer(prayer: PrayerName \| number): Promise<PrayerResult>` | Activate a prayer by name or index. Checks preconditions (level, prayer points, not already active) before toggling. |
|
||||
| `async deactivatePrayer(prayer: PrayerName \| number): Promise<PrayerResult>` | Deactivate a prayer by name or index. Checks if the prayer is actually active before toggling. |
|
||||
| `async deactivateAllPrayers(): Promise<PrayerResult>` | Deactivate all currently active prayers. Toggles each active prayer off one by one. |
|
||||
|
||||
---
|
||||
|
||||
## BotSDK (Low-Level)
|
||||
## BotSDK (low-level)
|
||||
|
||||
These methods resolve when server **acknowledges** them (not when effects complete).
|
||||
### Connection & Subscriptions
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async connect(): Promise<void>` | Connect to the gateway WebSocket. |
|
||||
| `async disconnect(): Promise<void>` | Disconnect from the gateway. |
|
||||
| `onConnectionStateChange(listener: (state: ConnectionState, attempt?: number) => void): () => void` | _No description provided._ |
|
||||
| `onStateUpdate(listener: (state: BotWorldState) => void): () => void` | _No description provided._ |
|
||||
|
||||
### State Access
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `getConnectionState()` | Get current connection state (connecting, connected, reconnecting, disconnected). |
|
||||
| `getReconnectAttempt()` | Get current reconnection attempt number. |
|
||||
| `getConnectionMode()` | Get connection mode (control or observe). |
|
||||
| `getState()` | Get current game state snapshot. |
|
||||
| `getStateReceivedAt()` | Get timestamp when state was last received (ms since epoch) |
|
||||
| `getStateAge()` | Get age of current state in milliseconds |
|
||||
| `getChat(opts)` | Read recent chat messages. |
|
||||
| `getNewChat(opts)` | Read only chat messages that have arrived since the last call (cursor-based, newest last). |
|
||||
| `getChatFrom(name, opts)` | Read recent chat from a specific sender (case-insensitive, substring match on name), newest last,... |
|
||||
| `getSkill(name)` | Get a skill by name (case-insensitive). |
|
||||
| `getSkillXp(name)` | Get XP for a skill by name. |
|
||||
| `getSkills()` | Get all skills. |
|
||||
| `getInventoryItem(slot)` | Get inventory item by slot number. |
|
||||
| `findInventoryItem(pattern)` | Find inventory item by name pattern. |
|
||||
| `getInventory()` | Get all inventory items. |
|
||||
| `getEquipmentItem(slot)` | Get equipment item by slot number. |
|
||||
| `findEquipmentItem(pattern)` | Find equipment item by name pattern. |
|
||||
| `getEquipment()` | Get all equipped items. |
|
||||
| `getBankItem(slot)` | Get bank item by slot number (bank must be open). |
|
||||
| `findBankItem(pattern)` | Find bank item by name pattern (bank must be open). |
|
||||
| `getBankItems()` | Get all bank items (bank must be open). |
|
||||
| `getNearbyNpc(index)` | Get NPC by index. |
|
||||
| `findNearbyNpc(pattern)` | Find NPC by name pattern. |
|
||||
| `getNearbyNpcs()` | Get all nearby NPCs. |
|
||||
| `getNearbyLoc(x, z, id)` | Get location (object) by coordinates and ID. |
|
||||
| `findNearbyLoc(pattern)` | Find location by name pattern. |
|
||||
| `getNearbyLocs()` | Get all nearby locations (trees, rocks, etc). |
|
||||
| `findGroundItem(pattern)` | Find ground item by name pattern. |
|
||||
| `getGroundItems()` | Get all ground items. |
|
||||
| `getDialog()` | Get current dialog state. |
|
||||
| `getPrayerState()` | Get current prayer state from world state. |
|
||||
| `getActivePrayers()` | Get list of all currently active prayer names. |
|
||||
|
||||
### On-Demand Scanning
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `scanNearbyLocs(radius?)` | Scan for nearby locations with custom radius. |
|
||||
| `scanGroundItems(radius?)` | Scan for ground items on-demand. |
|
||||
| `scanFindNearbyLoc(pattern, radius?)` | Find a nearby location by name pattern (on-demand scan). |
|
||||
| `scanFindGroundItem(pattern, radius?)` | Find a ground item by name pattern (on-demand scan). |
|
||||
|
||||
### Raw Actions
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `sendWalk(x, z, running)` | Send walk command to coordinates. |
|
||||
| `sendInteractLoc(x, z, locId, option)` | Interact with a location (tree, rock, door, etc). |
|
||||
| `sendInteractNpc(npcIndex, option)` | Interact with an NPC by index and option. |
|
||||
| `sendInteractPlayer(playerIndex, option)` | Interact with a player by index and option. |
|
||||
| `sendTalkToNpc(npcIndex)` | Talk to an NPC by index. |
|
||||
| `sendPickup(x, z, itemId)` | Pick up a ground item. |
|
||||
| `sendUseItem(slot, option)` | Use an inventory item (eat, equip, etc). |
|
||||
| `sendUseEquipmentItem(slot, option)` | Use an equipped item (remove, operate, etc). |
|
||||
| `sendDropItem(slot)` | Drop an inventory item. |
|
||||
| `sendUseItemOnItem(sourceSlot, targetSlot)` | Use one inventory item on another. |
|
||||
| `sendUseItemOnLoc(itemSlot, x, z, locId)` | Use an inventory item on a location. |
|
||||
| `sendUseItemOnNpc(itemSlot, npcIndex)` | Use an inventory item on an NPC. |
|
||||
| `sendClickDialog(option)` | Click a dialog option by its server-assigned index. |
|
||||
| `sendClickComponent(componentId)` | Click a component using IF_BUTTON packet - for simple buttons, spellcasting, etc. |
|
||||
| `sendClickComponentWithOption(componentId, optionIndex, slot)` | Click a component using INV_BUTTON packet - for components with inventory operations (smithing, c... |
|
||||
| `sendClickInterfaceOption(optionIndex)` | Click an interface option by index. |
|
||||
| `sendAcceptCharacterDesign()` | Accept character design in tutorial. |
|
||||
| `sendRandomizeCharacterDesign()` | Randomize character appearance in tutorial. |
|
||||
| `sendShopBuy(slot, amount)` | Buy from shop by slot and amount. |
|
||||
| `sendShopSell(slot, amount)` | Sell to shop by slot and amount. |
|
||||
| `sendCloseShop()` | Close shop interface. |
|
||||
| `sendCloseModal()` | Close any modal interface. |
|
||||
| `sendCountDialog(value)` | Submit a numeric value to an open p_countdialog (Enter Amount) prompt. |
|
||||
| `sendSetCombatStyle(style)` | Set combat style (0-3). |
|
||||
| `sendTogglePrayer(prayer)` | Toggle a prayer on or off by name or index (0-14). |
|
||||
| `sendSpellOnNpc(npcIndex, spellComponent)` | Cast spell on NPC using spell component ID. |
|
||||
| `sendSpellOnItem(slot, spellComponent)` | Cast spell on inventory item. |
|
||||
| `sendSpellOnGroundItem(x, z, itemId, spellComponent)` | Cast spell on ground item (e. |
|
||||
| `sendSetTab(tabIndex)` | Switch to a UI tab by index. |
|
||||
| `sendSay(message)` | Send a single chat message. |
|
||||
| `sendWait(ticks)` | Wait for specified number of game ticks. |
|
||||
| `sendBankDeposit(slot, amount)` | Deposit item to bank by slot. |
|
||||
| `sendBankWithdraw(slot, amount)` | Withdraw item from bank by slot. |
|
||||
| `sendScreenshot(timeout)` | Request a screenshot from the bot client. |
|
||||
| `sendFindPath(destX, destZ, maxWaypoints)` | Find path to destination (async alias for findPath). |
|
||||
|
||||
### Pathfinding
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `findPath(destX, destZ, maxWaypoints)` | Find path to destination using local collision data. |
|
||||
|
||||
### Condition Waiting
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `waitForBotConnection(timeout?)` | Wait for bot to connect to gateway after browser launch. |
|
||||
| `waitForConnection(timeout)` | Wait for WebSocket connection to be established. |
|
||||
| `waitForChat(opts: {
|
||||
from?: string;
|
||||
matching?: RegExp | string;
|
||||
types?: readonly number[];
|
||||
includeSelf?: boolean;
|
||||
timeout?: number;
|
||||
} = {})` | Wait for the next chat message matching the given filters (messages arriving after this call; you... |
|
||||
| `waitForReady(timeout)` | Wait for game state to be fully loaded and ready. |
|
||||
| `waitForStateChange(timeout)` | Wait for next state update from server. |
|
||||
| `waitForTicks(ticks)` | Wait for a specific number of server ticks (~300ms each). |
|
||||
| `waitForStateUpdate()` | Wait for the next state update from the server. |
|
||||
|
||||
### Connection
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `connect()` | Connect to the gateway WebSocket. |
|
||||
| `disconnect()` | Disconnect from the gateway. |
|
||||
| `isConnected()` | Check if WebSocket is connected. |
|
||||
| `isBotConnected()` | Check if bot is currently connected to gateway. |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `isConnected(): boolean` | Check if WebSocket is connected. |
|
||||
| `isAuthenticated(): boolean` | Check if the gateway has accepted our credentials. True means the transport and auth are both fine - if state is still missing after this, the problem is that no game client is logged in, not the connection. |
|
||||
| `getConnectionState(): ConnectionState` | Get current connection state (connecting, connected, reconnecting, disconnected). |
|
||||
| `getReconnectAttempt(): number` | Get current reconnection attempt number. |
|
||||
| `getConnectionMode(): SDKConnectionMode` | Get connection mode (control or observe). |
|
||||
| `async isBotConnected(): Promise<boolean>` | Check if bot is currently connected to gateway. |
|
||||
| `getState(): BotWorldState \| null` | Get current game state snapshot. |
|
||||
| `getStateReceivedAt(): number` | Get timestamp when state was last received (ms since epoch) |
|
||||
| `getStateAge(): number` | Get age of current state in milliseconds |
|
||||
| `getChat(opts: { limit?: number; types?: readonly number[]; includeSelf?: boolean } = {}): GameMessage[]` | Read recent chat messages. Returns player chat (public + PMs) by default, newest last. Reads from the SDK's accumulated history — up to 500 messages retained since connect — so old lines survive both system spam (level-ups, combat) and the client's own 100-deep ring eviction. |
|
||||
| `getNewChat(opts: { types?: readonly number[]; includeSelf?: boolean } = {}): GameMessage[]` | Read only chat messages that have arrived since the last call (cursor-based, newest last). Repeat polls never re-show the same message — no need to hand-roll a baseline. The first call returns everything seen since connect. Excludes your own messages by default. |
|
||||
| `getChatFrom(name: string, opts: { limit?: number } = {}): GameMessage[]` | Read recent chat from a specific sender (case-insensitive, substring match on name), newest last, from the accumulated history. Handy for "what did my partner say?" without regex-matching the sender field yourself. |
|
||||
| `getSkill(name: string): SkillState \| null` | Get a skill by name (case-insensitive). |
|
||||
| `getSkillXp(name: string): number \| null` | Get XP for a skill by name. |
|
||||
| `getSkills(): SkillState[]` | Get all skills. |
|
||||
| `getInventoryItem(slot: number): InventoryItem \| null` | Get inventory item by slot number. |
|
||||
| `findInventoryItem(pattern: string \| RegExp): InventoryItem \| null` | Find inventory item by name pattern. |
|
||||
| `getInventory(): InventoryItem[]` | Get all inventory items. |
|
||||
| `getEquipmentItem(slot: number): InventoryItem \| null` | Get equipment item by slot number. |
|
||||
| `findEquipmentItem(pattern: string \| RegExp): InventoryItem \| null` | Find equipment item by name pattern. |
|
||||
| `getEquipment(): InventoryItem[]` | Get all equipped items. |
|
||||
| `getBankItem(slot: number): BankItem \| null` | Get bank item by slot number (bank must be open). |
|
||||
| `findBankItem(pattern: string \| RegExp): BankItem \| null` | Find bank item by name pattern (bank must be open). |
|
||||
| `getBankItems(): BankItem[]` | Get all bank items (bank must be open). |
|
||||
| `isBankOpen(): boolean` | Check if bank interface is open. |
|
||||
| `getNearbyNpc(index: number): NearbyNpc \| null` | Get NPC by index. |
|
||||
| `findNearbyNpc(pattern: string \| RegExp): NearbyNpc \| null` | Find NPC by name pattern. |
|
||||
| `getNearbyNpcs(): NearbyNpc[]` | Get all nearby NPCs. |
|
||||
| `getNearbyLoc(x: number, z: number, id: number): NearbyLoc \| null` | Get location (object) by coordinates and ID. |
|
||||
| `findNearbyLoc(pattern: string \| RegExp): NearbyLoc \| null` | Find location by name pattern. |
|
||||
| `getNearbyLocs(): NearbyLoc[]` | Get all nearby locations (trees, rocks, etc). |
|
||||
| `findGroundItem(pattern: string \| RegExp): GroundItem \| null` | Find ground item by name pattern. |
|
||||
| `getGroundItems(): GroundItem[]` | Get all ground items. |
|
||||
| `getDialog(): DialogState \| null` | Get current dialog state. |
|
||||
| `getPrayerState(): PrayerState \| null` | Get current prayer state from world state. |
|
||||
| `isPrayerActive(prayer: PrayerName \| number): boolean` | Check if a specific prayer is currently active. |
|
||||
| `getActivePrayers(): PrayerName[]` | Get list of all currently active prayer names. |
|
||||
|
||||
### Other
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `isAuthenticated()` | Check if the gateway has accepted our credentials. |
|
||||
| `checkBotStatus()` | Check bot status via gateway HTTP endpoint. |
|
||||
| `launchBrowser()` | Launch native browser to client URL. |
|
||||
| `isBankOpen()` | Check if bank interface is open. |
|
||||
| `clickDialogByText(pattern)` | Click a dialog option whose visible text matches `pattern`. |
|
||||
| `isPrayerActive(prayer)` | Check if a specific prayer is currently active. |
|
||||
| `say(text, opts)` | Send a message of any length, auto-split into chunks on word boundaries and sent in order (so a m... |
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async checkBotStatus(): Promise<BotStatus>` | Check bot status via gateway HTTP endpoint. Returns info about whether bot is connected and who else is controlling/observing. |
|
||||
| `async launchBrowser(): Promise<void>` | Launch native browser to client URL. Uses the `open` package for cross-platform support (macOS, Windows, Linux, WSL). Falls back to printing the URL if no browser can be opened. |
|
||||
|
||||
### Condition Waiting
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async waitForBotConnection(timeout?: number): Promise<void>` | Wait for bot to connect to gateway after browser launch. |
|
||||
| `async waitForConnection(timeout: number = 60000): Promise<void>` | Wait for WebSocket connection to be established. |
|
||||
| `async waitForChat(opts: { from?: string; matching?: RegExp \| string; types?: readonly number[]; includeSelf?: boolean; timeout?: number; } = {}): Promise<GameMessage \| null>` | Wait for the next chat message matching the given filters (messages arriving after this call; your own messages are excluded by default). The easy way to coordinate two bots: `sdk.say('ready'); const reply = await sdk.waitForChat({ from: 'partner', timeout: 60000 });` |
|
||||
| `async waitForReady(timeout: number = 15000): Promise<BotWorldState>` | Wait for game state to be fully loaded and ready. Ensures player position is valid (not 0,0), bot is in-game, and state is recent. |
|
||||
| `async waitForCondition(predicate: (state: BotWorldState) => boolean, timeout: number = 30000): Promise<BotWorldState>` | _No description provided._ |
|
||||
| `async waitForStateChange(timeout: number = 30000): Promise<BotWorldState>` | Wait for next state update from server. |
|
||||
| `async waitForTicks(ticks: number = 1): Promise<BotWorldState>` | Wait for a specific number of server ticks (~300ms each). |
|
||||
| `async waitForStateUpdate(): Promise<BotWorldState>` | Wait for the next state update from the server. This is the most common waiting pattern - ensures fresh data after an action. State updates arrive once per server tick (~300ms) when PLAYER_INFO is received. |
|
||||
|
||||
### On-Demand Scanning
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]>` | Scan for nearby locations with custom radius. |
|
||||
| `async scanGroundItems(radius?: number): Promise<GroundItem[]>` | Scan for ground items on-demand. This is more efficient than constantly pushing this data in state updates. |
|
||||
| `async scanFindNearbyLoc(pattern: string \| RegExp, radius?: number): Promise<NearbyLoc \| null>` | Find a nearby location by name pattern (on-demand scan). |
|
||||
| `async scanFindGroundItem(pattern: string \| RegExp, radius?: number): Promise<GroundItem \| null>` | Find a ground item by name pattern (on-demand scan). |
|
||||
|
||||
### Raw Actions
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `async sendWalk(x: number, z: number, running: boolean = true): Promise<ActionResult>` | Send walk command to coordinates. |
|
||||
| `async sendInteractLoc(x: number, z: number, locId: number, option: number = 1): Promise<ActionResult>` | Interact with a location (tree, rock, door, etc). |
|
||||
| `async sendInteractNpc(npcIndex: number, option: number = 1): Promise<ActionResult>` | Interact with an NPC by index and option. |
|
||||
| `async sendInteractPlayer(playerIndex: number, option: number = 2): Promise<ActionResult>` | Interact with a player by index and option. Option 2 = Attack (wilderness), 3 = Follow, 4 = Trade. |
|
||||
| `async sendTalkToNpc(npcIndex: number): Promise<ActionResult>` | Talk to an NPC by index. |
|
||||
| `async sendPickup(x: number, z: number, itemId: number): Promise<ActionResult>` | Pick up a ground item. |
|
||||
| `async sendUseItem(slot: number, option: number = 1): Promise<ActionResult>` | Use an inventory item (eat, equip, etc). |
|
||||
| `async sendUseEquipmentItem(slot: number, option: number = 1): Promise<ActionResult>` | Use an equipped item (remove, operate, etc). |
|
||||
| `async sendDropItem(slot: number): Promise<ActionResult>` | Drop an inventory item. |
|
||||
| `async sendUseItemOnItem(sourceSlot: number, targetSlot: number): Promise<ActionResult>` | Use one inventory item on another. |
|
||||
| `async sendUseItemOnLoc(itemSlot: number, x: number, z: number, locId: number): Promise<ActionResult>` | Use an inventory item on a location. |
|
||||
| `async sendUseItemOnNpc(itemSlot: number, npcIndex: number): Promise<ActionResult>` | Use an inventory item on an NPC. |
|
||||
| `async sendClickDialog(option: number = 0): Promise<ActionResult>` | Click a dialog option by its server-assigned index. IMPORTANT: `option` is the **server-assigned index** stored on each `DialogOption.index` field — NOT the array position in `dialog.options`. Server-assigned indices are 1-based: `dialog.options[0].index === 1`. Pass `0` only as the implicit "continue" click for dialogs with no selectable options (the common pattern: pass through narration pages). To click an option by its visible text, prefer `clickDialogByText()`, which avoids the index-vs-position footgun entirely. |
|
||||
| `async clickDialogByText(pattern: string \| RegExp): Promise<ActionResult>` | Click a dialog option whose visible text matches `pattern`. Convenience wrapper that resolves the server-assigned index for you, sidestepping the 1-based vs 0-based array-position confusion of `sendClickDialog()`. Matches against `DialogOption.text` (case-insensitive by default for string patterns). |
|
||||
| `async sendClickComponent(componentId: number): Promise<ActionResult>` | Click a component using IF_BUTTON packet - for simple buttons, spellcasting, etc. |
|
||||
| `async sendClickComponentWithOption(componentId: number, optionIndex: number = 1, slot: number = 0): Promise<ActionResult>` | Click a component using INV_BUTTON packet - for components with inventory operations (smithing, crafting, etc.) |
|
||||
| `async sendClickInterfaceOption(optionIndex: number): Promise<ActionResult>` | Click an interface option by index. Convenience wrapper that looks up componentId from state. |
|
||||
| `async sendAcceptCharacterDesign(): Promise<ActionResult>` | Accept character design in tutorial. |
|
||||
| `async sendRandomizeCharacterDesign(): Promise<ActionResult>` | Randomize character appearance in tutorial. |
|
||||
| `async sendShopBuy(slot: number, amount: number = 1): Promise<ActionResult>` | Buy from shop by slot and amount. |
|
||||
| `async sendShopSell(slot: number, amount: number = 1): Promise<ActionResult>` | Sell to shop by slot and amount. |
|
||||
| `async sendCloseShop(): Promise<ActionResult>` | Close shop interface. |
|
||||
| `async sendCloseModal(): Promise<ActionResult>` | Close any modal interface. |
|
||||
| `async sendCountDialog(value: number): Promise<ActionResult>` | Submit a numeric value to an open p_countdialog (Enter Amount) prompt. |
|
||||
| `async sendSetCombatStyle(style: number): Promise<ActionResult>` | Set combat style (0-3). |
|
||||
| `async sendTogglePrayer(prayer: PrayerName \| number): Promise<ActionResult>` | Toggle a prayer on or off by name or index (0-14). |
|
||||
| `async sendSpellOnNpc(npcIndex: number, spellComponent: number): Promise<ActionResult>` | Cast spell on NPC using spell component ID. |
|
||||
| `async sendSpellOnItem(slot: number, spellComponent: number): Promise<ActionResult>` | Cast spell on inventory item. |
|
||||
| `async sendSpellOnGroundItem(x: number, z: number, itemId: number, spellComponent: number): Promise<ActionResult>` | Cast spell on ground item (e.g., Telekinetic Grab). |
|
||||
| `async sendSetTab(tabIndex: number): Promise<ActionResult>` | Switch to a UI tab by index. |
|
||||
| `async sendSay(message: string): Promise<ActionResult>` | Send a single chat message. The server caps public chat at {@link maxMessageLength} chars (80 by default) and runs a word filter; `result.data` reports `{ sent, truncated, filtered, finalText }` so you know if your message was clipped or censored. For longer text that shouldn't be silently truncated, use {@link say}. |
|
||||
| `async say(text: string, opts: { maxLen?: number; delayTicks?: number } = {}): Promise<ActionResult[]>` | Send a message of any length, auto-split into chunks on word boundaries and sent in order (so a multi-sentence plan isn't lost to the chat-length cap). Waits a tick between chunks so they don't collide. Returns one ActionResult per chunk. |
|
||||
| `async sendWait(ticks: number = 1): Promise<ActionResult>` | Wait for specified number of game ticks. |
|
||||
| `async sendBankDeposit(slot: number, amount: number = 1): Promise<ActionResult>` | Deposit item to bank by slot. |
|
||||
| `async sendBankWithdraw(slot: number, amount: number = 1): Promise<ActionResult>` | Withdraw item from bank by slot. |
|
||||
| `async sendScreenshot(timeout: number = 10000): Promise<string>` | Request a screenshot from the bot client. Returns the screenshot as a data URL (data:image/png;base64,...). |
|
||||
| `async sendFindPath(destX: number, destZ: number, maxWaypoints: number = 500): Promise<{ success: boolean; waypoints: Array<{ x: number; z: number; level: number }>; reachedDestination?: boolean; error?: string }>` | Find path to destination (async alias for findPath). |
|
||||
|
||||
### Pathfinding
|
||||
|
||||
| Signature | Description |
|
||||
|---|---|
|
||||
| `findPath(destX: number, destZ: number, maxWaypoints: number = 500): { success: boolean; waypoints: Array<{ x: number; z: number; level: number }>; reachedDestination?: boolean; error?: string }` | Find path to destination using local collision data. |
|
||||
|
||||
---
|
||||
|
||||
## Result Types
|
||||
## Result and state types
|
||||
|
||||
### PlayerCombatState
|
||||
|
||||
Combat state tracking for player
|
||||
|
||||
```typescript
|
||||
interface PlayerCombatState {
|
||||
inCombat: boolean; // Currently engaged in combat (has a target)
|
||||
targetIndex: number; // Index of NPC/player we're targeting (-1 if none)
|
||||
lastDamageTick: number; // Tick when we last took damage (-1 if never)
|
||||
/** Currently engaged in combat (has a target) */
|
||||
inCombat: boolean;
|
||||
/** Index of NPC/player we're targeting (-1 if none) */
|
||||
targetIndex: number;
|
||||
/** Tick when we last took damage (-1 if never) */
|
||||
lastDamageTick: number;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -271,18 +277,24 @@ interface PlayerCombatState {
|
||||
interface PlayerState {
|
||||
name: string;
|
||||
combatLevel: number;
|
||||
hp: number; // Current hitpoints level (boosted/drained)
|
||||
maxHp: number; // Base hitpoints level (max HP)
|
||||
/** Current hitpoints level (boosted/drained) */
|
||||
hp: number;
|
||||
/** Base hitpoints level (max HP) */
|
||||
maxHp: number;
|
||||
x: number;
|
||||
z: number;
|
||||
worldX: number;
|
||||
worldZ: number;
|
||||
level: number; // Map plane/floor: 0=ground, 1=first floor (upstairs), 2=second floor, 3=third floor
|
||||
/** Map plane/floor: 0=ground, 1=first floor (upstairs), 2=second floor, 3=third floor */
|
||||
level: number;
|
||||
runEnergy: number;
|
||||
runWeight: number;
|
||||
animId: number; // Current animation ID (-1 = idle/none)
|
||||
spotanimId: number; // Current spot animation ID (-1 = none)
|
||||
combat: PlayerCombatState; // Combat state tracking
|
||||
/** Current animation ID (-1 = idle/none) */
|
||||
animId: number;
|
||||
/** Current spot animation ID (-1 = none) */
|
||||
spotanimId: number;
|
||||
/** Combat state tracking */
|
||||
combat: PlayerCombatState;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -297,14 +309,137 @@ interface SkillState {
|
||||
}
|
||||
```
|
||||
|
||||
### DialogState
|
||||
|
||||
```typescript
|
||||
interface DialogState {
|
||||
isOpen: boolean;
|
||||
options: DialogOption[];
|
||||
isWaiting: boolean;
|
||||
text?: string;
|
||||
allComponents?: DialogComponent[];
|
||||
}
|
||||
```
|
||||
|
||||
### InterfaceState
|
||||
|
||||
```typescript
|
||||
interface InterfaceState {
|
||||
isOpen: boolean;
|
||||
interfaceId: number;
|
||||
options: Array<{ index: number; text: string; componentId: number }>;
|
||||
}
|
||||
```
|
||||
|
||||
### ShopState
|
||||
|
||||
```typescript
|
||||
interface ShopState {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
shopItems: ShopItem[];
|
||||
playerItems: ShopItem[];
|
||||
shopConfig?: ShopConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### BankState
|
||||
|
||||
```typescript
|
||||
interface BankState {
|
||||
isOpen: boolean;
|
||||
items: BankItem[];
|
||||
}
|
||||
```
|
||||
|
||||
### CombatStyleState
|
||||
|
||||
```typescript
|
||||
interface CombatStyleState {
|
||||
currentStyle: number;
|
||||
weaponName: string;
|
||||
styles: CombatStyleOption[];
|
||||
}
|
||||
```
|
||||
|
||||
### PrayerState
|
||||
|
||||
```typescript
|
||||
interface PrayerState {
|
||||
/** Active state of each prayer (indexed 0-14, matching PRAYER_NAMES order) */
|
||||
activePrayers: boolean[];
|
||||
/** Current prayer points (current skill level - drains while prayers active) */
|
||||
prayerPoints: number;
|
||||
/** Base prayer level */
|
||||
prayerLevel: number;
|
||||
}
|
||||
```
|
||||
|
||||
### PrayerResult
|
||||
|
||||
```typescript
|
||||
interface PrayerResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
reason?: 'invalid_prayer' | 'no_prayer_points' | 'level_too_low' | 'already_active' | 'already_inactive' | 'timeout';
|
||||
}
|
||||
```
|
||||
|
||||
### BotWorldState
|
||||
|
||||
```typescript
|
||||
interface BotWorldState {
|
||||
tick: number;
|
||||
inGame: boolean;
|
||||
player: PlayerState | null;
|
||||
skills: SkillState[];
|
||||
inventory: InventoryItem[];
|
||||
equipment: InventoryItem[];
|
||||
nearbyNpcs: NearbyNpc[];
|
||||
nearbyPlayers: NearbyPlayer[];
|
||||
nearbyLocs: NearbyLoc[];
|
||||
groundItems: GroundItem[];
|
||||
gameMessages: GameMessage[];
|
||||
recentDialogs: DialogEntry[];
|
||||
dialog: DialogState;
|
||||
interface: InterfaceState;
|
||||
shop: ShopState;
|
||||
bank: BankState;
|
||||
modalOpen: boolean;
|
||||
modalInterface: number;
|
||||
combatStyle?: CombatStyleState;
|
||||
combatEvents: CombatEvent[];
|
||||
prayers: PrayerState;
|
||||
}
|
||||
```
|
||||
|
||||
### ActionResult
|
||||
|
||||
```typescript
|
||||
interface ActionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
/** Optional data payload (used by scan actions to return results) */
|
||||
data?: any;
|
||||
/** Machine-readable failure category (e.g. 'cant_reach', 'no_match', 'timeout') */
|
||||
reason?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### SayResult
|
||||
|
||||
Outcome of sending a chat message. RS silently caps public chat at 80 chars and runs a word filter, so `sendSay` surfaces both via the `data` field (shape below) and `say()` returns these per chunk.
|
||||
|
||||
```typescript
|
||||
interface SayResult {
|
||||
sent: boolean; // Whether the message was sent (false only if not in game).
|
||||
truncated: boolean; // True if the message exceeded 80 chars and was clipped.
|
||||
filtered: boolean; // True if the word filter altered the text (likely censorship).
|
||||
finalText: string; // The text as actually broadcast (post-truncation, post-filter).
|
||||
/** Whether the message was sent (false only if not in game). */
|
||||
sent: boolean;
|
||||
/** True if the message exceeded 80 chars and was clipped. */
|
||||
truncated: boolean;
|
||||
/** True if the word filter altered the text (likely censorship). */
|
||||
filtered: boolean;
|
||||
/** The text as actually broadcast (post-truncation, post-filter). */
|
||||
finalText: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+227
-357
@@ -1,409 +1,279 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Generate API documentation from SDK source files.
|
||||
* Extracts method signatures and JSDoc comments to create sdk/API.md
|
||||
* Generate sdk/API.md from the TypeScript syntax tree.
|
||||
*
|
||||
* Run: bun sdk/generate-api-docs.ts
|
||||
* Usage:
|
||||
* bun sdk/generate-api-docs.ts
|
||||
* bun sdk/generate-api-docs.ts --check
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
interface MethodDoc {
|
||||
export interface MethodDoc {
|
||||
name: string;
|
||||
signature: string;
|
||||
params: { name: string; type: string; description?: string }[];
|
||||
returnType: string;
|
||||
description: string;
|
||||
isAsync: boolean;
|
||||
}
|
||||
|
||||
interface PropertyDoc {
|
||||
signature: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface TypeDoc {
|
||||
name: string;
|
||||
description: string;
|
||||
properties: { name: string; type: string; description?: string }[];
|
||||
properties: PropertyDoc[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JSDoc comment above a position in the source
|
||||
*/
|
||||
function extractJSDoc(source: string, methodStart: number): string {
|
||||
// Look backwards from method start for JSDoc - but only if it's immediately before
|
||||
const before = source.slice(Math.max(0, methodStart - 2000), methodStart);
|
||||
|
||||
// Find ALL JSDoc comments in the window and take the LAST one
|
||||
// This handles cases where multiple methods are close together
|
||||
const jsDocPattern = /\/\*\*([^]*?)\*\//g;
|
||||
let lastMatch: RegExpExecArray | null = null;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = jsDocPattern.exec(before)) !== null) {
|
||||
lastMatch = match;
|
||||
}
|
||||
|
||||
if (!lastMatch) return '';
|
||||
|
||||
// Verify this JSDoc is actually adjacent to the method (only whitespace between)
|
||||
const afterJsDoc = before.slice(lastMatch.index + lastMatch[0].length);
|
||||
if (!/^\s*$/.test(afterJsDoc)) {
|
||||
return ''; // There's non-whitespace between JSDoc and method
|
||||
}
|
||||
|
||||
const content = lastMatch[1] || '';
|
||||
|
||||
// Clean up: split by lines, remove asterisks, filter @tags, join
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s*\*\s?/, '').trim())
|
||||
.filter(line => line && !line.startsWith('@')); // Skip empty and @param, @returns etc
|
||||
|
||||
// Take only the first sentence/line as description
|
||||
const description = lines.join(' ').trim();
|
||||
|
||||
// Limit to first sentence (end at period followed by space or end)
|
||||
const firstSentence = description.match(/^[^.]+\.?/)?.[0] || description;
|
||||
|
||||
// Truncate if still too long
|
||||
if (firstSentence.length > 100) {
|
||||
return firstSentence.slice(0, 97) + '...';
|
||||
}
|
||||
|
||||
return firstSentence;
|
||||
interface ApiModel {
|
||||
botActionsMethods: MethodDoc[];
|
||||
sdkMethods: MethodDoc[];
|
||||
resultTypes: TypeDoc[];
|
||||
}
|
||||
|
||||
// Keywords and built-ins to skip
|
||||
const SKIP_NAMES = new Set([
|
||||
'async', 'await', 'if', 'else', 'while', 'for', 'return', 'throw', 'try', 'catch',
|
||||
'new', 'function', 'class', 'interface', 'type', 'const', 'let', 'var',
|
||||
'RegExp', 'map', 'filter', 'find', 'reduce', 'forEach', 'some', 'every',
|
||||
'log', 'warn', 'error', 'console', 'JSON', 'Object', 'Array', 'String', 'Number',
|
||||
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'Promise',
|
||||
'constructor', 'super', 'this', 'null', 'undefined', 'true', 'false'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Parse method signatures from a class
|
||||
*/
|
||||
function parseClassMethods(source: string, className: string): MethodDoc[] {
|
||||
const methods: MethodDoc[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
// Match class method declarations with proper indentation (4 spaces or 1 tab at start)
|
||||
// Pattern: async? methodName(params): ReturnType {
|
||||
const methodRegex = /^[ \t]+(async\s+)?(\w+)\s*\(([^)]*)\)\s*:\s*([^{]+?)\s*\{/gm;
|
||||
|
||||
let match;
|
||||
while ((match = methodRegex.exec(source)) !== null) {
|
||||
const [fullMatch, asyncKeyword, name, params, returnType] = match;
|
||||
|
||||
// Skip if required groups didn't match (params may be empty for zero-arg methods)
|
||||
if (!name || params === undefined || !returnType) continue;
|
||||
|
||||
// Skip private methods, constructor, keywords, and duplicates
|
||||
if (name.startsWith('_')) continue;
|
||||
if (SKIP_NAMES.has(name)) continue;
|
||||
if (seenNames.has(name)) continue;
|
||||
|
||||
// Skip if name looks like it's from inside a function body (lowercase common patterns)
|
||||
if (/^(result|state|item|npc|loc|error|response|data|msg|opt)$/.test(name)) continue;
|
||||
|
||||
seenNames.add(name);
|
||||
|
||||
const description = extractJSDoc(source, match.index);
|
||||
const parsedParams = parseParams(params);
|
||||
const cleanReturn = returnType.trim();
|
||||
|
||||
methods.push({
|
||||
name,
|
||||
signature: `${name}(${params.trim()}): ${cleanReturn}`,
|
||||
params: parsedParams,
|
||||
returnType: cleanReturn,
|
||||
description,
|
||||
isAsync: !!asyncKeyword
|
||||
});
|
||||
}
|
||||
|
||||
return methods;
|
||||
function sourceFile(path: string, source: string): ts.SourceFile {
|
||||
return ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse parameter string into structured params
|
||||
*/
|
||||
function parseParams(paramStr: string): { name: string; type: string; description?: string }[] {
|
||||
if (!paramStr.trim()) return [];
|
||||
function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
|
||||
return ts.canHaveModifiers(node) && Boolean(ts.getModifiers(node)?.some(modifier => modifier.kind === kind));
|
||||
}
|
||||
|
||||
const params: { name: string; type: string }[] = [];
|
||||
function isPublic(node: ts.Node): boolean {
|
||||
return !hasModifier(node, ts.SyntaxKind.PrivateKeyword) &&
|
||||
!hasModifier(node, ts.SyntaxKind.ProtectedKeyword);
|
||||
}
|
||||
|
||||
// Split by comma, but be careful of generic types like Map<string, number>
|
||||
let depth = 0;
|
||||
let current = '';
|
||||
function normalizeCode(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
for (const char of paramStr) {
|
||||
if (char === '<' || char === '(' || char === '{') depth++;
|
||||
else if (char === '>' || char === ')' || char === '}') depth--;
|
||||
else if (char === ',' && depth === 0) {
|
||||
if (current.trim()) params.push(parseParam(current.trim()));
|
||||
current = '';
|
||||
continue;
|
||||
function descriptionFor(node: ts.Node): string {
|
||||
const docs = ts.getJSDocCommentsAndTags(node).filter(ts.isJSDoc);
|
||||
const comment = docs.at(-1)?.comment;
|
||||
return normalizeCode(ts.getTextOfJSDocComment(comment) ?? '');
|
||||
}
|
||||
|
||||
function methodName(node: ts.MethodDeclaration, file: ts.SourceFile): string {
|
||||
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
return normalizeCode(node.name.getText(file));
|
||||
}
|
||||
|
||||
function methodSignature(node: ts.MethodDeclaration, file: ts.SourceFile): string {
|
||||
const asyncPrefix = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
||||
const typeParameters = node.typeParameters?.length
|
||||
? `<${node.typeParameters.map(parameter => normalizeCode(parameter.getText(file))).join(', ')}>`
|
||||
: '';
|
||||
const parameters = node.parameters
|
||||
.map(parameter => normalizeCode(parameter.getText(file)))
|
||||
.join(', ');
|
||||
const returnType = node.type ? normalizeCode(node.type.getText(file)) : 'unknown';
|
||||
return `${asyncPrefix}${methodName(node, file)}${typeParameters}(${parameters}): ${returnType}`;
|
||||
}
|
||||
|
||||
export function extractPublicClassMethods(
|
||||
path: string,
|
||||
source: string,
|
||||
className: string
|
||||
): MethodDoc[] {
|
||||
const file = sourceFile(path, source);
|
||||
const classNode = file.statements.find(
|
||||
(statement): statement is ts.ClassDeclaration =>
|
||||
ts.isClassDeclaration(statement) && statement.name?.text === className
|
||||
);
|
||||
if (!classNode) {
|
||||
throw new Error(`Class ${className} was not found in ${path}`);
|
||||
}
|
||||
|
||||
return classNode.members
|
||||
.filter((member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) && isPublic(member)
|
||||
)
|
||||
.map(method => ({
|
||||
name: methodName(method, file),
|
||||
signature: methodSignature(method, file),
|
||||
description: descriptionFor(method),
|
||||
}));
|
||||
}
|
||||
|
||||
function extractResultTypes(path: string, source: string): TypeDoc[] {
|
||||
const file = sourceFile(path, source);
|
||||
|
||||
return file.statements
|
||||
.filter((statement): statement is ts.InterfaceDeclaration =>
|
||||
ts.isInterfaceDeclaration(statement) &&
|
||||
hasModifier(statement, ts.SyntaxKind.ExportKeyword) &&
|
||||
(statement.name.text.endsWith('Result') || statement.name.text.endsWith('State'))
|
||||
)
|
||||
.map(statement => ({
|
||||
name: statement.name.text,
|
||||
description: descriptionFor(statement),
|
||||
properties: statement.members
|
||||
.filter((member): member is ts.PropertySignature =>
|
||||
ts.isPropertySignature(member) && Boolean(member.name)
|
||||
)
|
||||
.map(property => ({
|
||||
signature: normalizeCode(property.getText(file)).replace(/;$/, ''),
|
||||
description: descriptionFor(property),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function markdownCell(text: string): string {
|
||||
return text
|
||||
.replace(/\|/g, '\\|')
|
||||
.replace(/\r?\n/g, ' ');
|
||||
}
|
||||
|
||||
function methodCategory(name: string, surface: 'actions' | 'sdk'): string {
|
||||
const lower = name.toLowerCase();
|
||||
|
||||
if (surface === 'actions') {
|
||||
if (lower.includes('dialog') || lower.includes('blocking') || lower.includes('tutorial')) return 'UI & Dialog';
|
||||
if (lower.includes('walk')) return 'Movement';
|
||||
if (lower.includes('fletch') || lower.includes('craft') || lower.includes('smith') || lower.includes('enchant') || lower.includes('string')) {
|
||||
return 'Crafting & Smithing';
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
if (current.trim()) params.push(parseParam(current.trim()));
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function parseParam(param: string): { name: string; type: string } {
|
||||
// Handle patterns like: name: type, name?: type, name = default
|
||||
const match = param.match(/^(\w+)(\??)\s*(?::\s*(.+?))?(?:\s*=\s*.+)?$/);
|
||||
if (!match) return { name: param, type: 'unknown' };
|
||||
|
||||
const [, name, optional, type] = match;
|
||||
return {
|
||||
name: name + (optional || ''),
|
||||
type: type?.trim() || 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse interface/type definitions
|
||||
*/
|
||||
function parseTypes(source: string): TypeDoc[] {
|
||||
const types: TypeDoc[] = [];
|
||||
|
||||
// Match interface definitions
|
||||
const interfaceRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?export\s+interface\s+(\w+)\s*\{([^}]+)\}/g;
|
||||
|
||||
let match;
|
||||
while ((match = interfaceRegex.exec(source)) !== null) {
|
||||
const [fullMatch, name, body] = match;
|
||||
if (!name || !body) continue;
|
||||
|
||||
const description = extractJSDoc(source, match.index);
|
||||
|
||||
const properties = parseInterfaceBody(body);
|
||||
|
||||
types.push({ name, description, properties });
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
function parseInterfaceBody(body: string): { name: string; type: string; description?: string }[] {
|
||||
const props: { name: string; type: string; description?: string }[] = [];
|
||||
|
||||
// Match property: type patterns, with optional JSDoc
|
||||
const lines = body.split('\n');
|
||||
let currentComment = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Capture single-line comments
|
||||
const commentMatch = trimmed.match(/^\/\*\*\s*(.+?)\s*\*\/$/);
|
||||
if (commentMatch && commentMatch[1]) {
|
||||
currentComment = commentMatch[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match property definition
|
||||
const propMatch = trimmed.match(/^(\w+)(\??)\s*:\s*(.+?);?$/);
|
||||
if (propMatch) {
|
||||
const [, name, optional, type] = propMatch;
|
||||
if (!name || !type) continue;
|
||||
props.push({
|
||||
name: name + (optional || ''),
|
||||
type: type.replace(/;$/, ''),
|
||||
description: currentComment || undefined
|
||||
});
|
||||
currentComment = '';
|
||||
if (lower.includes('attack') || lower.includes('equip') || lower.includes('eat') || lower.includes('cast')) {
|
||||
return 'Combat & Equipment';
|
||||
}
|
||||
if (lower.includes('chop') || lower.includes('burn')) return 'Woodcutting & Firemaking';
|
||||
if (lower.includes('pickup')) return 'Items & Inventory';
|
||||
if (lower.includes('door')) return 'Doors';
|
||||
if (lower.includes('talk') || lower.includes('interact') || lower.includes('pickpocket')) return 'NPC & Object Interaction';
|
||||
if (lower.includes('shop') || lower.includes('buy') || lower.includes('sell')) return 'Shopping';
|
||||
if (lower.includes('bank') || lower.includes('deposit') || lower.includes('withdraw')) return 'Banking';
|
||||
if (lower.includes('wait')) return 'Condition Waiting';
|
||||
if (lower.includes('prayer')) return 'Prayer';
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
return props;
|
||||
if (lower.startsWith('get') || (lower.startsWith('find') && !lower.includes('path')) || lower.startsWith('is')) {
|
||||
return 'State Access';
|
||||
}
|
||||
if (lower.startsWith('scan')) return 'On-Demand Scanning';
|
||||
if (lower.startsWith('send') || lower === 'say' || lower === 'clickdialogbytext') return 'Raw Actions';
|
||||
if (lower.includes('path')) return 'Pathfinding';
|
||||
if (lower.includes('wait')) return 'Condition Waiting';
|
||||
if (lower.includes('connect') || lower.includes('connection') || lower.startsWith('onstate')) return 'Connection & Subscriptions';
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown from parsed docs
|
||||
*/
|
||||
function generateMarkdown(
|
||||
botActionsMethods: MethodDoc[],
|
||||
sdkMethods: MethodDoc[],
|
||||
resultTypes: TypeDoc[]
|
||||
): string {
|
||||
function renderMethodGroups(
|
||||
lines: string[],
|
||||
methods: MethodDoc[],
|
||||
surface: 'actions' | 'sdk'
|
||||
): void {
|
||||
const groups = new Map<string, MethodDoc[]>();
|
||||
for (const method of methods) {
|
||||
const category = methodCategory(method.name, surface);
|
||||
const group = groups.get(category) ?? [];
|
||||
group.push(method);
|
||||
groups.set(category, group);
|
||||
}
|
||||
|
||||
for (const [category, group] of groups) {
|
||||
lines.push(`### ${category}`, '', '| Signature | Description |', '|---|---|');
|
||||
for (const method of group) {
|
||||
const description = method.description || '_No description provided._';
|
||||
lines.push(`| \`${markdownCell(method.signature)}\` | ${markdownCell(description)} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
export function renderApiMarkdown(model: ApiModel): string {
|
||||
const lines: string[] = [
|
||||
'# SDK API Reference',
|
||||
'',
|
||||
'> Auto-generated from source. Do not edit directly.',
|
||||
'> Run `bun sdk/generate-api-docs.ts` to regenerate.',
|
||||
'> Auto-generated from TypeScript source. Do not edit directly.',
|
||||
'> Run `bun run docs:api` to regenerate or `bun run docs:api:check` to verify it.',
|
||||
'',
|
||||
'## BotActions (High-Level)',
|
||||
'## Execution model',
|
||||
'',
|
||||
'- `bot.*` methods are high-level helpers. They attempt to observe method-specific evidence such as movement, inventory, XP, dialog, or state changes. When a method returns a result, inspect its `success` and any `reason` or `message`: the strength of completion evidence varies by method.',
|
||||
'- `sdk.send*` methods are low-level browser-client dispatches. A successful `ActionResult` means the enhanced browser client accepted and dispatched the command; it does **not** prove that the game server processed it or that the intended game effect occurred.',
|
||||
'- Methods whose signature returns `Promise<...>` are asynchronous and must be awaited. In particular, `scanNearbyLocs()` and `scanGroundItems()` return promises.',
|
||||
'- `getInventory().length` is the number of occupied slots. `InventoryItem.count` is the quantity in that one slot, and `findInventoryItem()` returns the first matching slot rather than an aggregate across matching slots.',
|
||||
'',
|
||||
'## BotActions (high-level)',
|
||||
'',
|
||||
'These methods wait for the **effect to complete**, not just server acknowledgment.',
|
||||
''
|
||||
];
|
||||
|
||||
// Group BotActions methods by category
|
||||
const categories: Record<string, MethodDoc[]> = {
|
||||
'UI & Dialog': [],
|
||||
'Movement': [],
|
||||
'Combat & Equipment': [],
|
||||
'Woodcutting & Firemaking': [],
|
||||
'Items & Inventory': [],
|
||||
'Doors': [],
|
||||
'NPC Interaction': [],
|
||||
'Shopping': [],
|
||||
'Banking': [],
|
||||
'Crafting & Smithing': [],
|
||||
'Condition Waiting': [],
|
||||
'Other': []
|
||||
};
|
||||
renderMethodGroups(lines, model.botActionsMethods, 'actions');
|
||||
|
||||
for (const method of botActionsMethods) {
|
||||
const name = method.name.toLowerCase();
|
||||
if (name.includes('dialog') || name.includes('blocking') || name.includes('tutorial')) {
|
||||
categories['UI & Dialog']!.push(method);
|
||||
} else if (name.includes('walk')) {
|
||||
categories['Movement']!.push(method);
|
||||
} else if (name.includes('attack') || name.includes('equip') || name.includes('eat') || name.includes('cast')) {
|
||||
categories['Combat & Equipment']!.push(method);
|
||||
} else if (name.includes('chop') || name.includes('burn')) {
|
||||
categories['Woodcutting & Firemaking']!.push(method);
|
||||
} else if (name.includes('pickup')) {
|
||||
categories['Items & Inventory']!.push(method);
|
||||
} else if (name.includes('door')) {
|
||||
categories['Doors']!.push(method);
|
||||
} else if (name.includes('talk')) {
|
||||
categories['NPC Interaction']!.push(method);
|
||||
} else if (name.includes('shop') || name.includes('buy') || name.includes('sell')) {
|
||||
categories['Shopping']!.push(method);
|
||||
} else if (name.includes('bank') || name.includes('deposit') || name.includes('withdraw')) {
|
||||
categories['Banking']!.push(method);
|
||||
} else if (name.includes('fletch') || name.includes('craft') || name.includes('smith')) {
|
||||
categories['Crafting & Smithing']!.push(method);
|
||||
} else if (name.includes('wait')) {
|
||||
categories['Condition Waiting']!.push(method);
|
||||
} else {
|
||||
categories['Other']!.push(method);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [category, methods] of Object.entries(categories)) {
|
||||
if (methods.length === 0) continue;
|
||||
|
||||
lines.push(`### ${category}`, '');
|
||||
lines.push('| Method | Description |');
|
||||
lines.push('|--------|-------------|');
|
||||
|
||||
for (const method of methods) {
|
||||
const sig = formatSignature(method);
|
||||
const desc = method.description || '_No description_';
|
||||
lines.push(`| \`${sig}\` | ${desc} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// SDK methods
|
||||
lines.push('---', '', '## BotSDK (Low-Level)', '');
|
||||
lines.push('These methods resolve when server **acknowledges** them (not when effects complete).', '');
|
||||
|
||||
const sdkCategories: Record<string, MethodDoc[]> = {
|
||||
'State Access': [],
|
||||
'On-Demand Scanning': [],
|
||||
'Raw Actions': [],
|
||||
'Pathfinding': [],
|
||||
'Condition Waiting': [],
|
||||
'Connection': [],
|
||||
'Other': []
|
||||
};
|
||||
|
||||
for (const method of sdkMethods) {
|
||||
const name = method.name.toLowerCase();
|
||||
if (name.startsWith('get') || name.startsWith('find') && !name.includes('path')) {
|
||||
sdkCategories['State Access']!.push(method);
|
||||
} else if (name.startsWith('scan')) {
|
||||
sdkCategories['On-Demand Scanning']!.push(method);
|
||||
} else if (name.startsWith('send')) {
|
||||
sdkCategories['Raw Actions']!.push(method);
|
||||
} else if (name.includes('path')) {
|
||||
sdkCategories['Pathfinding']!.push(method);
|
||||
} else if (name.includes('wait')) {
|
||||
sdkCategories['Condition Waiting']!.push(method);
|
||||
} else if (name.includes('connect') || name === 'isConnected' || name.includes('connection')) {
|
||||
sdkCategories['Connection']!.push(method);
|
||||
} else {
|
||||
sdkCategories['Other']!.push(method);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [category, methods] of Object.entries(sdkCategories)) {
|
||||
if (methods.length === 0) continue;
|
||||
|
||||
lines.push(`### ${category}`, '');
|
||||
lines.push('| Method | Description |');
|
||||
lines.push('|--------|-------------|');
|
||||
|
||||
for (const method of methods) {
|
||||
const sig = formatSignature(method);
|
||||
const desc = method.description || '_No description_';
|
||||
lines.push(`| \`${sig}\` | ${desc} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Result types
|
||||
lines.push('---', '', '## Result Types', '');
|
||||
|
||||
for (const type of resultTypes) {
|
||||
if (!type.name.endsWith('Result') && !type.name.endsWith('State')) continue;
|
||||
lines.push('---', '', '## BotSDK (low-level)', '');
|
||||
renderMethodGroups(lines, model.sdkMethods, 'sdk');
|
||||
|
||||
lines.push('---', '', '## Result and state types', '');
|
||||
for (const type of model.resultTypes) {
|
||||
lines.push(`### ${type.name}`, '');
|
||||
if (type.description) lines.push(type.description, '');
|
||||
lines.push('```typescript');
|
||||
lines.push(`interface ${type.name} {`);
|
||||
for (const prop of type.properties) {
|
||||
const comment = prop.description ? ` // ${prop.description}` : '';
|
||||
lines.push(` ${prop.name}: ${prop.type};${comment}`);
|
||||
lines.push('```typescript', `interface ${type.name} {`);
|
||||
for (const property of type.properties) {
|
||||
if (property.description) {
|
||||
lines.push(` /** ${property.description.replace(/\*\//g, '*\\/')} */`);
|
||||
}
|
||||
lines.push(` ${property.signature};`);
|
||||
}
|
||||
lines.push('}');
|
||||
lines.push('```', '');
|
||||
lines.push('}', '```', '');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
return `${lines.join('\n').trimEnd()}\n`;
|
||||
}
|
||||
|
||||
function formatSignature(method: MethodDoc): string {
|
||||
// Compact format: methodName(param1, param2?)
|
||||
const params = method.params
|
||||
.map(p => p.name)
|
||||
.join(', ');
|
||||
return `${method.name}(${params})`;
|
||||
export async function buildApiModel(sdkDir = import.meta.dir): Promise<ApiModel> {
|
||||
const actionsPath = join(sdkDir, 'actions.ts');
|
||||
const indexPath = join(sdkDir, 'index.ts');
|
||||
const typesPath = join(sdkDir, 'types.ts');
|
||||
const [actionsSource, indexSource, typesSource] = await Promise.all([
|
||||
readFile(actionsPath, 'utf8'),
|
||||
readFile(indexPath, 'utf8'),
|
||||
readFile(typesPath, 'utf8'),
|
||||
]);
|
||||
|
||||
return {
|
||||
botActionsMethods: extractPublicClassMethods(actionsPath, actionsSource, 'BotActions'),
|
||||
sdkMethods: extractPublicClassMethods(indexPath, indexSource, 'BotSDK'),
|
||||
resultTypes: extractResultTypes(typesPath, typesSource),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
export async function generateApiMarkdown(sdkDir = import.meta.dir): Promise<string> {
|
||||
return renderApiMarkdown(await buildApiModel(sdkDir));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const check = process.argv.includes('--check');
|
||||
const sdkDir = import.meta.dir;
|
||||
|
||||
// Read source files
|
||||
const actionsSource = await readFile(join(sdkDir, 'actions.ts'), 'utf-8');
|
||||
const indexSource = await readFile(join(sdkDir, 'index.ts'), 'utf-8');
|
||||
const typesSource = await readFile(join(sdkDir, 'types.ts'), 'utf-8');
|
||||
|
||||
// Parse
|
||||
const botActionsMethods = parseClassMethods(actionsSource, 'BotActions');
|
||||
const sdkMethods = parseClassMethods(indexSource, 'BotSDK');
|
||||
const types = parseTypes(typesSource);
|
||||
|
||||
// Generate markdown
|
||||
const markdown = generateMarkdown(botActionsMethods, sdkMethods, types);
|
||||
|
||||
// Write output
|
||||
const outputPath = join(sdkDir, 'API.md');
|
||||
await writeFile(outputPath, markdown);
|
||||
const model = await buildApiModel(sdkDir);
|
||||
const markdown = renderApiMarkdown(model);
|
||||
|
||||
console.log(`Generated ${outputPath}`);
|
||||
console.log(` - ${botActionsMethods.length} BotActions methods`);
|
||||
console.log(` - ${sdkMethods.length} BotSDK methods`);
|
||||
console.log(` - ${types.filter(t => t.name.endsWith('Result') || t.name.endsWith('State')).length} result types`);
|
||||
if (check) {
|
||||
const current = await readFile(outputPath, 'utf8').catch(() => '');
|
||||
if (current !== markdown) {
|
||||
throw new Error('sdk/API.md is stale. Run `bun run docs:api` and commit the result.');
|
||||
}
|
||||
console.log(`Verified ${outputPath}`);
|
||||
} else {
|
||||
await writeFile(outputPath, markdown);
|
||||
console.log(`Generated ${outputPath}`);
|
||||
}
|
||||
|
||||
console.log(` - ${model.botActionsMethods.length} BotActions methods`);
|
||||
console.log(` - ${model.sdkMethods.length} BotSDK methods`);
|
||||
console.log(` - ${model.resultTypes.length} result/state types`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
if (import.meta.main) {
|
||||
main().catch(error => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
buildApiModel,
|
||||
generateApiMarkdown,
|
||||
} from '../generate-api-docs';
|
||||
|
||||
const sdkDir = join(import.meta.dir, '..');
|
||||
|
||||
describe('generated API reference', () => {
|
||||
test('matches the checked-in document', async () => {
|
||||
const [generated, checkedIn] = await Promise.all([
|
||||
generateApiMarkdown(sdkDir),
|
||||
readFile(join(sdkDir, 'API.md'), 'utf8'),
|
||||
]);
|
||||
expect(checkedIn).toBe(generated);
|
||||
});
|
||||
|
||||
test('contains every public class method and complete signatures', async () => {
|
||||
const model = await buildApiModel(sdkDir);
|
||||
const generated = await generateApiMarkdown(sdkDir);
|
||||
const methods = [...model.botActionsMethods, ...model.sdkMethods];
|
||||
|
||||
expect(methods.length).toBeGreaterThan(100);
|
||||
for (const method of methods) {
|
||||
expect(generated).toContain(method.name);
|
||||
expect(method.signature).toContain(':');
|
||||
}
|
||||
|
||||
for (const historicallyOmittedMethod of [
|
||||
'navigateDialog',
|
||||
'onConnectionStateChange',
|
||||
'onStateUpdate',
|
||||
'waitForCondition',
|
||||
]) {
|
||||
expect(methods.some(method => method.name === historicallyOmittedMethod)).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves async markers, return types, defaults, and dotted descriptions', async () => {
|
||||
const model = await buildApiModel(sdkDir);
|
||||
const methods = [...model.botActionsMethods, ...model.sdkMethods];
|
||||
|
||||
expect(methods.find(method => method.name === 'scanNearbyLocs')?.signature)
|
||||
.toBe('async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]>');
|
||||
expect(methods.find(method => method.name === 'sendWalk')?.signature)
|
||||
.toContain('running: boolean = true');
|
||||
expect(methods.find(method => method.name === 'useItemOnLoc')?.description)
|
||||
.toContain('e.g.');
|
||||
});
|
||||
});
|
||||
@@ -66,7 +66,7 @@ runTest({
|
||||
|
||||
// Check bank contents
|
||||
const bankState = sdk.getState();
|
||||
const bankCoins = bankState.bank.items.find(i => /coins/i.test(i.name));
|
||||
const bankCoins = bankState?.bank.items.find(i => /coins/i.test(i.name));
|
||||
console.log(`Bank coins: ${bankCoins ? `${bankCoins.count} (slot ${bankCoins.slot})` : 'NOT FOUND'}`);
|
||||
|
||||
// Step 3: Withdraw exactly 25 coins
|
||||
@@ -94,7 +94,7 @@ runTest({
|
||||
console.log(`Inventory coins: ${invCount} (expected: 25)`);
|
||||
|
||||
const finalBankState = sdk.getState();
|
||||
const remainingBankCoins = finalBankState.bank.items.find(i => /coins/i.test(i.name));
|
||||
const remainingBankCoins = finalBankState?.bank.items.find(i => /coins/i.test(i.name));
|
||||
const bankCount = remainingBankCoins?.count ?? 0;
|
||||
console.log(`Bank coins remaining: ${bankCount} (expected: 75)`);
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
const root = join(import.meta.dir, '..', '..');
|
||||
|
||||
describe('agent documentation snippets', () => {
|
||||
test('learnings use execute_code globals consistently', async () => {
|
||||
const learningDir = join(root, 'learnings');
|
||||
const files = (await readdir(learningDir)).filter(file => file.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
const markdown = await readFile(join(learningDir, file), 'utf8');
|
||||
expect(markdown).not.toContain('ctx.bot');
|
||||
expect(markdown).not.toContain('ctx.sdk');
|
||||
expect(markdown).not.toMatch(/\bstate\.combat\b/);
|
||||
expect(markdown).not.toMatch(/\bfunction\s+\w+\([^)]*:\s*\w+/);
|
||||
}
|
||||
});
|
||||
|
||||
test('TypeScript fences are syntactically valid', async () => {
|
||||
const learningDir = join(root, 'learnings');
|
||||
const files = (await readdir(learningDir)).filter(file => file.endsWith('.md'));
|
||||
|
||||
for (const file of files) {
|
||||
const markdown = await readFile(join(learningDir, file), 'utf8');
|
||||
const snippets = [...markdown.matchAll(/```(?:typescript|ts)\s*\n([\s\S]*?)```/g)];
|
||||
for (const [index, match] of snippets.entries()) {
|
||||
const snippet = (match[1] ?? '').replace(/\{\s*\.\.\.\s*\}/g, '{}');
|
||||
if (/^\s*import\s/m.test(snippet)) continue;
|
||||
const result = ts.transpileModule(`async function snippet() {\n${snippet}\n}`, {
|
||||
fileName: `${file}#${index + 1}.ts`,
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
},
|
||||
});
|
||||
const errors = (result.diagnostics ?? []).filter(
|
||||
diagnostic => diagnostic.category === ts.DiagnosticCategory.Error
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
const messages = errors.map(error =>
|
||||
ts.flattenDiagnosticMessageText(error.messageText, '\n')
|
||||
).join('; ');
|
||||
throw new Error(`${file} snippet ${index + 1}: ${messages}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -953,6 +953,10 @@ function generateItemMarkdown(page: ItemPage): string {
|
||||
|
||||
function updateReadme(pages: { name: string; file: string; category: ItemCategory }[]) {
|
||||
const readmePath = join(WIKI_DIR, 'README.md');
|
||||
if (!existsSync(readmePath)) {
|
||||
console.log(' No wiki/README.md; skipping index update');
|
||||
return;
|
||||
}
|
||||
let content = readFileSync(readmePath, 'utf-8');
|
||||
|
||||
// Remove existing Items section if present
|
||||
|
||||
+22
-9
@@ -1064,22 +1064,31 @@ function generateNpcMarkdown(page: NpcPage, objNames: Map<string, string>): stri
|
||||
if (page.spawns.length > 0) {
|
||||
lines.push(page.spawns.length > 1 ? '## Locations' : '## Location');
|
||||
lines.push('');
|
||||
lines.push('| Area | Map |');
|
||||
lines.push('|------|-----|');
|
||||
lines.push('| Area | Map | Coordinate samples |');
|
||||
lines.push('|------|-----|--------------------|');
|
||||
|
||||
// Group spawns by map square, then by area
|
||||
const byArea = new Map<string, Set<string>>();
|
||||
// Group spawns by area while retaining useful exact coordinates. Limit
|
||||
// samples so very common NPCs do not produce enormous generated pages.
|
||||
const byArea = new Map<string, { squares: Set<string>; coordinates: Set<string> }>();
|
||||
for (const spawn of page.spawns) {
|
||||
const area = getAreaFromMapSquare(spawn.mapSquare);
|
||||
if (!byArea.has(area)) byArea.set(area, new Set());
|
||||
byArea.get(area)!.add(spawn.mapSquare);
|
||||
if (!byArea.has(area)) {
|
||||
byArea.set(area, { squares: new Set(), coordinates: new Set() });
|
||||
}
|
||||
const group = byArea.get(area)!;
|
||||
group.squares.add(spawn.mapSquare);
|
||||
const floor = spawn.level > 0 ? ` L${spawn.level}` : '';
|
||||
group.coordinates.add(`(${spawn.globalX}, ${spawn.globalZ})${floor}`);
|
||||
}
|
||||
|
||||
// Sort by area name
|
||||
const sortedAreas = [...byArea.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
for (const [area, squares] of sortedAreas) {
|
||||
const squareList = [...squares].sort().join(', ');
|
||||
lines.push(`| ${area} | ${squareList} |`);
|
||||
for (const [area, group] of sortedAreas) {
|
||||
const squareList = [...group.squares].sort().join(', ');
|
||||
const allCoordinates = [...group.coordinates].sort();
|
||||
const coordinateList = allCoordinates.slice(0, 12).join(', ');
|
||||
const overflow = allCoordinates.length > 12 ? `, +${allCoordinates.length - 12} more` : '';
|
||||
lines.push(`| ${area} | ${squareList} | ${coordinateList}${overflow} |`);
|
||||
}
|
||||
lines.push('');
|
||||
} else if (primary.hasPatrol || primary.defaultmode === 'patrol') {
|
||||
@@ -1220,6 +1229,10 @@ function generateNotes(page: NpcPage): string[] {
|
||||
|
||||
function updateReadme(pages: { name: string; file: string }[]) {
|
||||
const readmePath = join(WIKI_DIR, 'README.md');
|
||||
if (!existsSync(readmePath)) {
|
||||
console.log(' No wiki/README.md; skipping index update');
|
||||
return;
|
||||
}
|
||||
let content = readFileSync(readmePath, 'utf-8');
|
||||
|
||||
// Remove existing NPCs section if present
|
||||
|
||||
@@ -475,6 +475,10 @@ function generateShopMarkdown(page: ShopPage, objData: Map<string, ObjInfo>): st
|
||||
|
||||
function updateReadme(pages: { name: string; file: string; location: string }[]) {
|
||||
const readmePath = join(WIKI_DIR, 'README.md');
|
||||
if (!existsSync(readmePath)) {
|
||||
console.log(' No wiki/README.md; skipping index update');
|
||||
return;
|
||||
}
|
||||
let content = readFileSync(readmePath, 'utf-8');
|
||||
|
||||
// Remove existing Shops section if present
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 1/3rds full jug
|
||||
|
||||
*This jug is one thirds full. It has a 3 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# 1/5ths full bucket
|
||||
|
||||
*This bucket is twenty percent full. It has a 5 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2/3rds full jug
|
||||
|
||||
*This jug is two thirds full. It has a 3 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2/5ths full bucket
|
||||
|
||||
*This bucket is forty percent full. It has a 5 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# 3/5ths full bucket
|
||||
|
||||
*This bucket is sixty percent full. It has a 5 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# 4/5ths full bucket
|
||||
|
||||
*This bucket is eighty percent full. It has a 5 painted on it's side.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Adam plateskirt (g)
|
||||
|
||||
*Adamant plateskirt with gold trim.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | No |
|
||||
| **Weight** | 23lb |
|
||||
| **Value** | 6400 gp |
|
||||
| **Equipable** | Yes — Legs |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -21 |
|
||||
| Range Attack | -7 |
|
||||
| Stab Defence | +33 |
|
||||
| Slash Defence | +31 |
|
||||
| Crush Defence | +29 |
|
||||
| Magic Defence | -4 |
|
||||
| Range Defence | +31 |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Adam plateskirt (t)
|
||||
|
||||
*Adamant plateskirt with trim.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | No |
|
||||
| **Weight** | 23lb |
|
||||
| **Value** | 6400 gp |
|
||||
| **Equipable** | Yes — Legs |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -21 |
|
||||
| Range Attack | -7 |
|
||||
| Stab Defence | +33 |
|
||||
| Slash Defence | +31 |
|
||||
| Crush Defence | +29 |
|
||||
| Magic Defence | -4 |
|
||||
| Range Defence | +31 |
|
||||
@@ -0,0 +1,29 @@
|
||||
# Adamant halberd
|
||||
|
||||
*An adamant halberd.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Weapon |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 8lb |
|
||||
| **Value** | 6400 gp |
|
||||
| **Equipable** | Yes — Weapon |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +28 |
|
||||
| Slash Attack | +41 |
|
||||
| Magic Attack | -4 |
|
||||
| Stab Defence | -1 |
|
||||
| Slash Defence | +3 |
|
||||
| Crush Defence | +4 |
|
||||
| Strength Bonus | +42 |
|
||||
| Attack Speed | 7 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Regicide General Shop](../shops/regicide-general-shop.md) for 6400 gp
|
||||
- Sold by: [Quatermaster's Stores](../shops/quatermasters-stores.md) for 8320 gp
|
||||
@@ -21,5 +21,5 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Authentic Throwing Weapons](../shops/authentic-throwing-weapons.md) for 208 gp
|
||||
- Sold by: [Authentic throwing weapons](../shops/authentic-throwing-weapons.md) for 208 gp
|
||||
- Dropped by: [Black dragon](../npcs/black-dragon.md) (20/128)
|
||||
@@ -24,5 +24,5 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Louie's Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 6400 gp
|
||||
- Sold by: [Louies' Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 6400 gp
|
||||
- Dropped by: [Greater demon](../npcs/greater-demon.md) (1/128)
|
||||
@@ -0,0 +1,28 @@
|
||||
# Adamant spear(kp)
|
||||
|
||||
*A Karambwan poisoned adamantite tipped spear.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Weapon |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 6lb |
|
||||
| **Value** | 2080 gp |
|
||||
| **Equipable** | Yes — Weapon |
|
||||
| **Tradeable** | No |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +24 |
|
||||
| Slash Attack | +24 |
|
||||
| Crush Attack | +24 |
|
||||
| Stab Defence | +1 |
|
||||
| Slash Defence | +1 |
|
||||
| Strength Bonus | +28 |
|
||||
| Attack Speed | 6 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Tamayu's Spear Stall](../shops/tamayus-spear-stall.md) for 2080 gp
|
||||
@@ -22,4 +22,4 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Authentic Throwing Weapons](../shops/authentic-throwing-weapons.md) for 228 gp
|
||||
- Sold by: [Authentic throwing weapons](../shops/authentic-throwing-weapons.md) for 228 gp
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Skulgrimen's Battle Gear](../shops/skulgrimens-battle-gear.md) for 5356 gp
|
||||
- Dropped by: [Berry](../npcs/berry.md) (4/128)
|
||||
- Dropped by: [Twig](../npcs/twig.md) (4/128)
|
||||
- Dropped by: [Mountain Troll](../npcs/mountain-troll.md) (4/128)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Alco-chunks
|
||||
|
||||
*Pineapple chunks dipped in strong liquor.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 80g |
|
||||
| **Value** | 1 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,28 @@
|
||||
# Archer helm
|
||||
|
||||
*This helmet is worn by archers.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 6lb |
|
||||
| **Value** | 60000 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Slash Attack | +5 |
|
||||
| Magic Attack | -5 |
|
||||
| Range Attack | +6 |
|
||||
| Stab Defence | +6 |
|
||||
| Slash Defence | +8 |
|
||||
| Crush Defence | +10 |
|
||||
| Magic Defence | +6 |
|
||||
| Range Defence | +6 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Skulgrimen's Battle Gear](../shops/skulgrimens-battle-gear.md) for 78000 gp
|
||||
@@ -0,0 +1,14 @@
|
||||
# Asyn remains
|
||||
|
||||
*The remains of a deadly shade.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 0 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Asyn Shadow](../npcs/asyn-shadow.md) (Always)
|
||||
- Dropped by: [Asyn Shade](../npcs/asyn-shade.md) (Always)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Barbed bolts
|
||||
|
||||
*Great if you have a crossbow!*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Ammunition |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 18g |
|
||||
| **Value** | 200 gp |
|
||||
| **Equipable** | Yes — Ammo |
|
||||
| **Stackable** | Yes |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Range Bonus | +46 |
|
||||
| Level Requirement | 1 |
|
||||
@@ -0,0 +1,11 @@
|
||||
# Barrel bomb
|
||||
|
||||
*A barrel full of fire oil.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 32kg |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# Barrel of coal-tar
|
||||
|
||||
*A barrel full of coal-tar.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 32kg |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,11 @@
|
||||
# Barrel of naphtha
|
||||
|
||||
*A barrel full of naphtha.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 32kg |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -9,3 +9,10 @@
|
||||
| **Weight** | 22lb |
|
||||
| **Value** | 1 gp |
|
||||
| **Tradeable** | No |
|
||||
|
||||
## Spawn Locations
|
||||
|
||||
| Area | Floor | Quantity |
|
||||
|------|-------|----------|
|
||||
| m34_49 | Ground | 1 |
|
||||
| m34_50 | Ground | 1 |
|
||||
@@ -15,14 +15,18 @@
|
||||
|------|-------|----------|
|
||||
| Karamja (west) | Ground | 1 |
|
||||
| m39_52 | Ground | 1 |
|
||||
| m41_57 | Ground | 1 |
|
||||
| m51_47 | Floor 1 | 1 |
|
||||
| Wilderness | Ground | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Karamja Wines, Spirits, and Beers](../shops/karamja-wines-spirits-and-beers.md) for 2 gp
|
||||
- Sold by: [Rellekka Longhall Bar](../shops/rellekka-longhall-bar.md) for 2 gp
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (1/128)
|
||||
- Dropped by: [Goblin guard](../npcs/goblin-guard.md) (2/128)
|
||||
- Dropped by: [Giant](../npcs/giant.md) (6/128)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (1/128)
|
||||
- Dropped by: [Goblin](../npcs/goblin.md) (2/128)
|
||||
- Dropped by: [Gunthor the brave](../npcs/gunthor-the-brave.md) (1/128)
|
||||
- Dropped by: [Barbarian woman](../npcs/barbarian-woman.md) (1/128)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Beret
|
||||
|
||||
*Pouvez-vous parler francais?*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 40g |
|
||||
| **Value** | 80 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
@@ -0,0 +1,27 @@
|
||||
# Berserker helm
|
||||
|
||||
*This helmet is worn by berserkers.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 6lb |
|
||||
| **Value** | 60000 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -5 |
|
||||
| Range Attack | -5 |
|
||||
| Stab Defence | +31 |
|
||||
| Slash Defence | +29 |
|
||||
| Crush Defence | +33 |
|
||||
| Range Defence | +30 |
|
||||
| Strength Bonus | +3 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Skulgrimen's Battle Gear](../shops/skulgrimens-battle-gear.md) for 78000 gp
|
||||
@@ -0,0 +1,11 @@
|
||||
# Big book of bangs
|
||||
|
||||
*A book by Mel Achy.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 8oz |
|
||||
| **Value** | 1 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -17,4 +17,5 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Fremennik Fish Monger](../shops/fremennik-fish-monger.md) for 26 gp
|
||||
- Sold by: [Harrys Fishing Shop](../shops/harrys-fishing-shop.md) for 20 gp
|
||||
@@ -0,0 +1,29 @@
|
||||
# Black halberd
|
||||
|
||||
*A black halberd.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Weapon |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 7lb |
|
||||
| **Value** | 1920 gp |
|
||||
| **Equipable** | Yes — Weapon |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +19 |
|
||||
| Slash Attack | +25 |
|
||||
| Magic Attack | -4 |
|
||||
| Stab Defence | -1 |
|
||||
| Slash Defence | +2 |
|
||||
| Crush Defence | +3 |
|
||||
| Strength Bonus | +20 |
|
||||
| Attack Speed | 7 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Regicide General Shop](../shops/regicide-general-shop.md) for 1920 gp
|
||||
- Sold by: [Quatermaster's Stores](../shops/quatermasters-stores.md) for 2496 gp
|
||||
@@ -0,0 +1,10 @@
|
||||
# Black key black
|
||||
|
||||
*A black key with a black painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 94 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Black key brown
|
||||
|
||||
*A black key with a brown painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 92 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Black key crimson
|
||||
|
||||
*A black key with a crimson painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 93 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Black key purple
|
||||
|
||||
*A black key with a purple painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 95 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Black key red
|
||||
|
||||
*A black key with a blood-red painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 91 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -25,4 +25,4 @@
|
||||
## Sources
|
||||
|
||||
- Sold by: [Valaine's Shop of Champions](../shops/valaines-shop-of-champions.md) for 2496 gp
|
||||
- Sold by: [Louie's Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 1920 gp
|
||||
- Sold by: [Louies' Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 1920 gp
|
||||
@@ -0,0 +1,23 @@
|
||||
# Black plateskirt (g)
|
||||
|
||||
*Black plateskirt with gold trim.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | No |
|
||||
| **Weight** | 20lb |
|
||||
| **Value** | 1920 gp |
|
||||
| **Equipable** | Yes — Legs |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -21 |
|
||||
| Range Attack | -7 |
|
||||
| Stab Defence | +21 |
|
||||
| Slash Defence | +20 |
|
||||
| Crush Defence | +19 |
|
||||
| Magic Defence | -4 |
|
||||
| Range Defence | +20 |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Black plateskirt (t)
|
||||
|
||||
*Black plateskirt with trim.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | No |
|
||||
| **Weight** | 20lb |
|
||||
| **Value** | 1920 gp |
|
||||
| **Equipable** | Yes — Legs |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -21 |
|
||||
| Range Attack | -7 |
|
||||
| Stab Defence | +21 |
|
||||
| Slash Defence | +20 |
|
||||
| Crush Defence | +19 |
|
||||
| Magic Defence | -4 |
|
||||
| Range Defence | +20 |
|
||||
@@ -23,9 +23,12 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Skulgrimen's Battle Gear](../shops/skulgrimens-battle-gear.md) for 832 gp
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (1/128)
|
||||
- Dropped by: [Berry](../npcs/berry.md) (4/128)
|
||||
- Dropped by: [Pee Hat](../npcs/pee-hat.md) (4/128)
|
||||
- Dropped by: [Twig](../npcs/twig.md) (4/128)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (1/128)
|
||||
- Dropped by: [Rock](../npcs/rock.md) (4/128)
|
||||
- Dropped by: [Stick](../npcs/stick.md) (4/128)
|
||||
- Dropped by: [Troll General](../npcs/troll-general.md) (4/128)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Blamish bark shell
|
||||
|
||||
*A large bark coloured blamish snail shell, looks protective.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 10kg |
|
||||
| **Value** | 150 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Bark Blamish Snail](../npcs/bark-blamish-snail.md) (Always)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Blamish blue shell
|
||||
|
||||
*A large blue coloured blamish snail shell, looks protective.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 10kg |
|
||||
| **Value** | 150 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Bruise Blamish Snail](../npcs/bruise-blamish-snail.md) (Always)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Blamish myre shell
|
||||
|
||||
*A large 'Myre' coloured blamish snail shell, looks protective.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 10kg |
|
||||
| **Value** | 150 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Myre Blamish Snail](../npcs/myre-blamish-snail.md) (Always)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Blamish ochre shell
|
||||
|
||||
*A large muddy yellow coloured blamish snail shell, looks protective.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 10kg |
|
||||
| **Value** | 150 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Ochre Blamish Snail](../npcs/ochre-blamish-snail.md) (Always)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Blamish red shell
|
||||
|
||||
*A large red and black blamish snail shell, looks protective.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 10kg |
|
||||
| **Value** | 150 gp |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Blood Blamish Snail](../npcs/blood-blamish-snail.md) (Always)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Blood'n'tar snelm
|
||||
|
||||
*A red and black Snail shell helmet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 2kg |
|
||||
| **Value** | 300 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -3 |
|
||||
| Range Attack | -1 |
|
||||
| Stab Defence | +7 |
|
||||
| Slash Defence | +8 |
|
||||
| Crush Defence | +6 |
|
||||
| Magic Defence | -1 |
|
||||
| Range Defence | +7 |
|
||||
@@ -0,0 +1,11 @@
|
||||
# Blue thread
|
||||
|
||||
*Some blue thread.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 2g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Bolts(p)
|
||||
|
||||
*Vicious poisoned bolts.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Ammunition |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 18g |
|
||||
| **Value** | 3 gp |
|
||||
| **Equipable** | Yes — Ammo |
|
||||
| **Stackable** | Yes |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Range Bonus | +10 |
|
||||
| Level Requirement | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Tribesman](../npcs/tribesman.md) (2/138)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Bolts
|
||||
|
||||
*Good if you have a crossbow!*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Ammunition |
|
||||
| **Members** | No |
|
||||
| **Weight** | 18g |
|
||||
| **Value** | 3 gp |
|
||||
| **Equipable** | Yes — Ammo |
|
||||
| **Stackable** | Yes |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Range Bonus | +10 |
|
||||
| Level Requirement | 1 |
|
||||
|
||||
## Spawn Locations
|
||||
|
||||
| Area | Floor | Quantity |
|
||||
|------|-------|----------|
|
||||
| Falador (east) | Floor 1 | 1 |
|
||||
| m48_57 | Ground | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Armoury](../shops/armoury.md) for 4 gp
|
||||
- Sold by: [Lowe's Archery Emporium](../shops/lowes-archery-emporium.md) for 3 gp
|
||||
- Sold by: [Hicktons Archery Emporium](../shops/hicktons-archery-emporium.md) for 3 gp
|
||||
- Sold by: [Gulluck and Sons](../shops/gulluck-and-sons.md) for 3 gp
|
||||
- Dropped by: [Goblin guard](../npcs/goblin-guard.md) (3/128)
|
||||
- Dropped by: [Imp](../npcs/imp.md) (8/128)
|
||||
- Dropped by: [Al-Kharid warrior](../npcs/al-kharid-warrior.md) (22/128 (members))
|
||||
- Dropped by: [Woman](../npcs/woman.md) (22/128 (members))
|
||||
- Dropped by: [Goblin](../npcs/goblin.md) (3/128)
|
||||
- Dropped by: [Dwarf](../npcs/dwarf.md) (7/128 (members))
|
||||
- Dropped by: [Rogue](../npcs/rogue.md) (22/128)
|
||||
- Dropped by: [Thief](../npcs/thief.md) (22/128 (members))
|
||||
- Dropped by: [Man](../npcs/man.md) (22/128 (members))
|
||||
+30
-3
@@ -25,6 +25,7 @@
|
||||
| m40_46 | Ground | 1 |
|
||||
| m40_47 | Ground | 1 |
|
||||
| m41_153 | Ground | 1 |
|
||||
| m41_58 | Ground | 1 |
|
||||
| m44_149 | Ground | 1 |
|
||||
| m44_152 | Ground | 1 |
|
||||
| m45_153 | Ground | 1 |
|
||||
@@ -52,8 +53,10 @@
|
||||
|
||||
- Dropped by: [Kalphite Soldier](../npcs/kalphite-soldier.md) (Always)
|
||||
- Dropped by: [Unferth](../npcs/unferth.md) (Always)
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (Always)
|
||||
- Dropped by: [Tower guard](../npcs/tower-guard.md) (Always)
|
||||
- Dropped by: [Ice warrior](../npcs/ice-warrior.md) (Always)
|
||||
- Dropped by: [Pontak](../npcs/pontak.md) (Always)
|
||||
- Dropped by: [Farmer](../npcs/farmer.md) (Always)
|
||||
- Dropped by: [Dark warrior](../npcs/dark-warrior.md) (Always)
|
||||
- Dropped by: [Ugthanki](../npcs/ugthanki.md) (Always)
|
||||
@@ -70,22 +73,23 @@
|
||||
- Dropped by: [Guardian of Armadyl](../npcs/guardian-of-armadyl.md) (Always)
|
||||
- Dropped by: [Shantay Guard](../npcs/shantay-guard.md) (Always)
|
||||
- Dropped by: [Gnome woman](../npcs/gnome-woman.md) (Always)
|
||||
- Dropped by: [Monkey](../npcs/monkey.md) (Always)
|
||||
- Dropped by: [Chaos druid warrior](../npcs/chaos-druid-warrior.md) (Always)
|
||||
- Dropped by: [Barbarian guard](../npcs/barbarian-guard.md) (Always)
|
||||
- Dropped by: [Wizard](../npcs/wizard.md) (Always)
|
||||
- Dropped by: [Wyson the gardener](../npcs/wyson-the-gardener.md) (Always)
|
||||
- Dropped by: [Ocga](../npcs/ocga.md) (Always)
|
||||
- Dropped by: [Colonel Radick](../npcs/colonel-radick.md) (Always)
|
||||
- Dropped by: [Archer](../npcs/archer.md) (Always)
|
||||
- Dropped by: [Iban disciple](../npcs/iban-disciple.md) (Always)
|
||||
- Dropped by: [Monk of Zamorak](../npcs/monk-of-zamorak.md) (Always)
|
||||
- Dropped by: [Bedabin Nomad Guard](../npcs/bedabin-nomad-guard.md) (Always)
|
||||
- Dropped by: [Inga](../npcs/inga.md) (Always)
|
||||
- Dropped by: [Eadburg](../npcs/eadburg.md) (Always)
|
||||
- Dropped by: [Chaos druid](../npcs/chaos-druid.md) (Always)
|
||||
- Dropped by: [Earth warrior](../npcs/earth-warrior.md) (Always)
|
||||
- Dropped by: [Bandit](../npcs/bandit.md) (Always)
|
||||
- Dropped by: [Dagannoth](../npcs/dagannoth.md) (Always)
|
||||
- Dropped by: [Skeleton Mage](../npcs/skeleton-mage.md) (Always)
|
||||
- Dropped by: [Unicorn Foal](../npcs/unicorn-foal.md) (Always)
|
||||
- Dropped by: [Freygerd](../npcs/freygerd.md) (Always)
|
||||
- Dropped by: [Khazard commander](../npcs/khazard-commander.md) (Always)
|
||||
- Dropped by: [Salarin the twisted](../npcs/salarin-the-twisted.md) (Always)
|
||||
- Dropped by: [Giant rat](../npcs/giant-rat.md) (Always)
|
||||
@@ -94,6 +98,7 @@
|
||||
- Dropped by: [Lord Daquarius](../npcs/lord-daquarius.md) (Always)
|
||||
- Dropped by: [Chicken](../npcs/chicken.md) (Always)
|
||||
- Dropped by: [Gnome guard](../npcs/gnome-guard.md) (Always)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (Always)
|
||||
- Dropped by: [Air elemental](../npcs/air-elemental.md) (Always)
|
||||
- Dropped by: [Al-Kharid warrior](../npcs/al-kharid-warrior.md) (Always)
|
||||
- Dropped by: [Woman](../npcs/woman.md) (Always)
|
||||
@@ -103,6 +108,7 @@
|
||||
- Dropped by: [Kalphite Queen](../npcs/kalphite-queen.md) (Always)
|
||||
- Dropped by: [Chompy bird](../npcs/chompy-bird.md) (Always)
|
||||
- Dropped by: [Chaos dwarf](../npcs/chaos-dwarf.md) (Always)
|
||||
- Dropped by: [Ghoul](../npcs/ghoul.md) (Always)
|
||||
- Dropped by: [Invrigar the Necromancer](../npcs/invrigar-the-necromancer.md) (Always)
|
||||
- Dropped by: [Khazard Ogre](../npcs/khazard-ogre.md) (Always)
|
||||
- Dropped by: [Elvarg](../npcs/elvarg.md) (Always)
|
||||
@@ -110,18 +116,26 @@
|
||||
- Dropped by: [Goblin](../npcs/goblin.md) (Always)
|
||||
- Dropped by: [Pirate Guard](../npcs/pirate-guard.md) (Always)
|
||||
- Dropped by: [Head Thief](../npcs/head-thief.md) (Always)
|
||||
- Dropped by: [Grizzly bear](../npcs/grizzly-bear.md) (Always)
|
||||
- Dropped by: [Terrorbird](../npcs/terrorbird.md) (Always)
|
||||
- Dropped by: [Agnar](../npcs/agnar.md) (Always)
|
||||
- Dropped by: [Gunthor the brave](../npcs/gunthor-the-brave.md) (Always)
|
||||
- Dropped by: [Bear Cub](../npcs/bear-cub.md) (Always)
|
||||
- Dropped by: [Fortress Guard](../npcs/fortress-guard.md) (Always)
|
||||
- Dropped by: [Leech](../npcs/leech.md) (Always)
|
||||
- Dropped by: [Breoca](../npcs/breoca.md) (Always)
|
||||
- Dropped by: [Rabbit](../npcs/rabbit.md) (Always)
|
||||
- Dropped by: [Rowdy slave](../npcs/rowdy-slave.md) (Always)
|
||||
- Dropped by: [Watchman](../npcs/watchman.md) (Always)
|
||||
- Dropped by: [Servant](../npcs/servant.md) (Always)
|
||||
- Dropped by: [Jennella](../npcs/jennella.md) (Always)
|
||||
- Dropped by: [Dire Wolf](../npcs/dire-wolf.md) (Always)
|
||||
- Dropped by: [Pirate](../npcs/pirate.md) (Always)
|
||||
- Dropped by: [Zombie](../npcs/zombie.md) (Always)
|
||||
- Dropped by: [Fire Warrior of Lesarkus](../npcs/fire-warrior-of-lesarkus.md) (Always)
|
||||
- Dropped by: [Entrana Fire Bird](../npcs/entrana-fire-bird.md) (Always)
|
||||
- Dropped by: [Black Knight Titan](../npcs/black-knight-titan.md) (Always)
|
||||
- Dropped by: [The Draugen](../npcs/the-draugen.md) (Always)
|
||||
- Dropped by: [Yeti](../npcs/yeti.md) (Always)
|
||||
- Dropped by: [Soldier](../npcs/soldier.md) (Always)
|
||||
- Dropped by: [Gull](../npcs/gull.md) (Always)
|
||||
@@ -140,6 +154,7 @@
|
||||
- Dropped by: [Sir Mordred](../npcs/sir-mordred.md) (Always)
|
||||
- Dropped by: [Black unicorn](../npcs/black-unicorn.md) (Always)
|
||||
- Dropped by: [Renegade Knight](../npcs/renegade-knight.md) (Always)
|
||||
- Dropped by: [Tyras guard](../npcs/tyras-guard.md) (Always)
|
||||
- Dropped by: [Wormbrain](../npcs/wormbrain.md) (Always)
|
||||
- Dropped by: [Viyeldi](../npcs/viyeldi.md) (Always)
|
||||
- Dropped by: [Highwayman](../npcs/highwayman.md) (Always)
|
||||
@@ -148,6 +163,7 @@
|
||||
- Dropped by: [Melzar the mad](../npcs/melzar-the-mad.md) (Always)
|
||||
- Dropped by: [Summoned Zombie](../npcs/summoned-zombie.md) (Always)
|
||||
- Dropped by: [Straven](../npcs/straven.md) (Always)
|
||||
- Dropped by: [Lensa](../npcs/lensa.md) (Always)
|
||||
- Dropped by: [Mounted terrorbird gnome](../npcs/mounted-terrorbird-gnome.md) (Always)
|
||||
- Dropped by: [Knight of Ardougne](../npcs/knight-of-ardougne.md) (Always)
|
||||
- Dropped by: [Black Heather](../npcs/black-heather.md) (Always)
|
||||
@@ -168,18 +184,25 @@
|
||||
- Dropped by: [Penguin](../npcs/penguin.md) (Always)
|
||||
- Dropped by: [Thug](../npcs/thug.md) (Always)
|
||||
- Dropped by: [Ogre shaman](../npcs/ogre-shaman.md) (Always)
|
||||
- Dropped by: [Afflicted](../npcs/afflicted.md) (Always)
|
||||
- Dropped by: [Witch](../npcs/witch.md) (Always)
|
||||
- Dropped by: [Dagannoth mother](../npcs/dagannoth-mother.md) (Always)
|
||||
- Dropped by: [Black unicorn Foal](../npcs/black-unicorn-foal.md) (Always)
|
||||
- Dropped by: [Lanzig](../npcs/lanzig.md) (Always)
|
||||
- Dropped by: [Khazard warlord](../npcs/khazard-warlord.md) (Always)
|
||||
- Dropped by: [Alomone](../npcs/alomone.md) (Always)
|
||||
- Dropped by: [Kalphite Guardian](../npcs/kalphite-guardian.md) (Always)
|
||||
- Dropped by: [Blessed Giant rat](../npcs/blessed-giant-rat.md) (Always)
|
||||
- Dropped by: [Tower Archer](../npcs/tower-archer.md) (Always)
|
||||
- Dropped by: [Jogre](../npcs/jogre.md) (3/128)
|
||||
- Dropped by: [Sassilik](../npcs/sassilik.md) (Always)
|
||||
- Dropped by: [Ceolburg](../npcs/ceolburg.md) (Always)
|
||||
- Dropped by: [Barbarian](../npcs/barbarian.md) (Always)
|
||||
- Dropped by: [White Knight](../npcs/white-knight.md) (Always)
|
||||
- Dropped by: [Hazeel Cultist](../npcs/hazeel-cultist.md) (Always)
|
||||
- Dropped by: [Forester](../npcs/forester.md) (Always)
|
||||
- Dropped by: [Grizzly bear cub](../npcs/grizzly-bear-cub.md) (Always)
|
||||
- Dropped by: [Borrokar](../npcs/borrokar.md) (Always)
|
||||
- Dropped by: [Sir Carl](../npcs/sir-carl.md) (Always)
|
||||
- Dropped by: [Snake](../npcs/snake.md) (Always)
|
||||
- Dropped by: [Guard Bandit](../npcs/guard-bandit.md) (Always)
|
||||
@@ -187,6 +210,7 @@
|
||||
- Dropped by: [Necromancer](../npcs/necromancer.md) (Always)
|
||||
- Dropped by: [Mercenary](../npcs/mercenary.md) (Always)
|
||||
- Dropped by: [Mugger](../npcs/mugger.md) (Always)
|
||||
- Dropped by: [vampire_count](../npcs/vampire-count.md) (Always)
|
||||
- Dropped by: [Jailer](../npcs/jailer.md) (Always)
|
||||
- Dropped by: [Weaponsmaster](../npcs/weaponsmaster.md) (Always)
|
||||
- Dropped by: [Donny the lad](../npcs/donny-the-lad.md) (Always)
|
||||
@@ -194,12 +218,14 @@
|
||||
- Dropped by: [Shipyard worker](../npcs/shipyard-worker.md) (Always)
|
||||
- Dropped by: [Foreman](../npcs/foreman.md) (Always)
|
||||
- Dropped by: [Jail guard](../npcs/jail-guard.md) (Always)
|
||||
- Dropped by: [Elf warrior](../npcs/elf-warrior.md) (Always)
|
||||
- Dropped by: [Shadow warrior](../npcs/shadow-warrior.md) (Always)
|
||||
- Dropped by: [Paladin](../npcs/paladin.md) (Always)
|
||||
- Dropped by: [Man](../npcs/man.md) (Always)
|
||||
- Dropped by: [Black Knight](../npcs/black-knight.md) (Always)
|
||||
- Dropped by: [Unicorn](../npcs/unicorn.md) (Always)
|
||||
- Dropped by: [Cow](../npcs/cow.md) (Always)
|
||||
- Dropped by: [Saradomin Wizard](../npcs/saradomin-wizard.md) (Always)
|
||||
- Dropped by: [Zamorak Wizard](../npcs/zamorak-wizard.md) (Always)
|
||||
- Dropped by: [Skeleton](../npcs/skeleton.md) (Always)
|
||||
- Dropped by: [Skavid](../npcs/skavid.md) (Always)
|
||||
@@ -207,3 +233,4 @@
|
||||
- Dropped by: [Hild](../npcs/hild.md) (Always)
|
||||
- Dropped by: [Undead One](../npcs/undead-one.md) (Always)
|
||||
- Dropped by: [Penda](../npcs/penda.md) (Always)
|
||||
- Dropped by: [Freidir](../npcs/freidir.md) (Always)
|
||||
@@ -0,0 +1,28 @@
|
||||
# Book of balance
|
||||
|
||||
*The holy book of Guthix.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 1kg |
|
||||
| **Value** | 200 gp |
|
||||
| **Equipable** | Yes — Shield |
|
||||
| **Tradeable** | No |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +4 |
|
||||
| Slash Attack | +4 |
|
||||
| Crush Attack | +4 |
|
||||
| Magic Attack | +4 |
|
||||
| Range Attack | +4 |
|
||||
| Stab Defence | +4 |
|
||||
| Slash Defence | +4 |
|
||||
| Crush Defence | +4 |
|
||||
| Magic Defence | +4 |
|
||||
| Range Defence | +4 |
|
||||
| Prayer Bonus | +5 |
|
||||
@@ -0,0 +1,11 @@
|
||||
# Branch
|
||||
|
||||
*I can use this to make a lyre.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 4lb |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -11,4 +11,5 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [The Lighthouse Store](../shops/the-lighthouse-store.md) for 5 gp
|
||||
- Sold by: [Funch's Fine Groceries](../shops/funchs-fine-groceries.md) for 5 gp
|
||||
@@ -17,9 +17,12 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Regicide General Shop](../shops/regicide-general-shop.md) for 12 gp
|
||||
- Sold by: [Quatermaster's Stores](../shops/quatermasters-stores.md) for 15 gp
|
||||
- Sold by: [Jiminua's Jungle Store](../shops/jiminuas-jungle-store.md) for 18 gp
|
||||
- Sold by: [Ardougne Baker's Stall](../shops/ardougne-bakers-stall.md) for 12 gp
|
||||
- Sold by: [West Ardougne General Store](../shops/west-ardougne-general-store.md) for 14 gp
|
||||
- Sold by: [Sigmund the Merchant](../shops/sigmund-the-merchant.md) for 15 gp
|
||||
- Sold by: [Foodshop](../shops/foodshop.md) for 12 gp
|
||||
- Sold by: [Food Store](../shops/food-store.md) for 12 gp
|
||||
- Sold by: [Obli's General Store](../shops/oblis-general-store.md) for 18 gp
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Broken bark snelm
|
||||
|
||||
*An orange and bark coloured snail shell helmet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 2kg |
|
||||
| **Value** | 300 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -3 |
|
||||
| Range Attack | -1 |
|
||||
| Stab Defence | +7 |
|
||||
| Slash Defence | +8 |
|
||||
| Crush Defence | +6 |
|
||||
| Magic Defence | -1 |
|
||||
| Range Defence | +7 |
|
||||
@@ -39,26 +39,19 @@
|
||||
- Sold by: [Hicktons Archery Emporium](../shops/hicktons-archery-emporium.md) for 1 gp
|
||||
- Sold by: [Bolkoy's Village Shop](../shops/bolkoys-village-shop.md) for 1 gp
|
||||
- Sold by: [Gulluck and Sons](../shops/gulluck-and-sons.md) for 1 gp
|
||||
- Dropped by: [Unferth](../npcs/unferth.md) (3/128)
|
||||
- Dropped by: [Tower guard](../npcs/tower-guard.md) (5/128)
|
||||
- Dropped by: [Farmer](../npcs/farmer.md) (3/128)
|
||||
- Dropped by: [Dark warrior](../npcs/dark-warrior.md) (4/128)
|
||||
- Dropped by: [Guard](../npcs/guard.md) (3/128)
|
||||
- Dropped by: [Ocga](../npcs/ocga.md) (3/128)
|
||||
- Dropped by: [Colonel Radick](../npcs/colonel-radick.md) (5/128)
|
||||
- Dropped by: [Al-Kharid warrior](../npcs/al-kharid-warrior.md) (3/128)
|
||||
- Dropped by: [Woman](../npcs/woman.md) (3/128)
|
||||
- Dropped by: [Gunthor the brave](../npcs/gunthor-the-brave.md) (4/128)
|
||||
- Dropped by: [Breoca](../npcs/breoca.md) (3/128)
|
||||
- Dropped by: [Pirate](../npcs/pirate.md) (3/128)
|
||||
- Dropped by: [Soldier](../npcs/soldier.md) (5/128)
|
||||
- Dropped by: [Barbarian woman](../npcs/barbarian-woman.md) (3/128)
|
||||
- Dropped by: [Rogue](../npcs/rogue.md) (3/128)
|
||||
- Dropped by: [Hygd](../npcs/hygd.md) (3/128)
|
||||
- Dropped by: [Thief](../npcs/thief.md) (3/128)
|
||||
- Dropped by: [Ceolburg](../npcs/ceolburg.md) (3/128)
|
||||
- Dropped by: [Barbarian](../npcs/barbarian.md) (3/128)
|
||||
- Dropped by: [Man](../npcs/man.md) (3/128)
|
||||
- Dropped by: [Skeleton](../npcs/skeleton.md) (7/128)
|
||||
- Dropped by: [Hild](../npcs/hild.md) (3/128)
|
||||
- Dropped by: [Penda](../npcs/penda.md) (3/128)
|
||||
@@ -27,6 +27,7 @@
|
||||
|------|-------|----------|
|
||||
| Camelot area | Floor 1 | 1 |
|
||||
| Karamja (west) | Ground | 1 |
|
||||
| m41_58 | Ground | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Bronze halberd
|
||||
|
||||
*A bronze halberd.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Weapon |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 7lb |
|
||||
| **Value** | 80 gp |
|
||||
| **Equipable** | Yes — Weapon |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +7 |
|
||||
| Slash Attack | +8 |
|
||||
| Magic Attack | -4 |
|
||||
| Stab Defence | -1 |
|
||||
| Slash Defence | +1 |
|
||||
| Crush Defence | +2 |
|
||||
| Strength Bonus | +8 |
|
||||
| Attack Speed | 7 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Regicide General Shop](../shops/regicide-general-shop.md) for 80 gp
|
||||
- Sold by: [Quatermaster's Stores](../shops/quatermasters-stores.md) for 104 gp
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Authentic Throwing Weapons](../shops/authentic-throwing-weapons.md) for 5 gp
|
||||
- Sold by: [Authentic throwing weapons](../shops/authentic-throwing-weapons.md) for 5 gp
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bronze key black
|
||||
|
||||
*A bronze key with a black painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 84 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bronze key brown
|
||||
|
||||
*A bronze key with a brown painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 82 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bronze key crimson
|
||||
|
||||
*A bronze key with a crimson painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 83 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bronze key purple
|
||||
|
||||
*A bronze key with a purple painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 85 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bronze key red
|
||||
|
||||
*A bronze key with a blood-red painted eyelet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Value** | 81 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -32,20 +32,13 @@
|
||||
## Sources
|
||||
|
||||
- Sold by: [Helmet Shop](../shops/helmet-shop.md) for 24 gp
|
||||
- Dropped by: [Unferth](../npcs/unferth.md) (2/128)
|
||||
- Dropped by: [Farmer](../npcs/farmer.md) (2/128)
|
||||
- Dropped by: [Dark warrior](../npcs/dark-warrior.md) (3/128)
|
||||
- Dropped by: [Ocga](../npcs/ocga.md) (2/128)
|
||||
- Dropped by: [Al-Kharid warrior](../npcs/al-kharid-warrior.md) (2/128)
|
||||
- Dropped by: [Woman](../npcs/woman.md) (2/128)
|
||||
- Dropped by: [Breoca](../npcs/breoca.md) (2/128)
|
||||
- Dropped by: [Zombie](../npcs/zombie.md) (4/128)
|
||||
- Dropped by: [Dwarf](../npcs/dwarf.md) (4/128)
|
||||
- Dropped by: [Rogue](../npcs/rogue.md) (2/128)
|
||||
- Dropped by: [Hygd](../npcs/hygd.md) (2/128)
|
||||
- Dropped by: [Thief](../npcs/thief.md) (2/128)
|
||||
- Dropped by: [Ceolburg](../npcs/ceolburg.md) (2/128)
|
||||
- Dropped by: [Mugger](../npcs/mugger.md) (2/128)
|
||||
- Dropped by: [Man](../npcs/man.md) (2/128)
|
||||
- Dropped by: [Hild](../npcs/hild.md) (2/128)
|
||||
- Dropped by: [Penda](../npcs/penda.md) (2/128)
|
||||
@@ -24,4 +24,4 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Louie's Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 80 gp
|
||||
- Sold by: [Louies' Armoured Legs Bazaar](../shops/louies-armoured-legs-bazaar.md) for 80 gp
|
||||
@@ -0,0 +1,28 @@
|
||||
# Bronze spear(kp)
|
||||
|
||||
*A Karambwan poisoned bronze tipped spear.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Weapon |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 5lb |
|
||||
| **Value** | 26 gp |
|
||||
| **Equipable** | Yes — Weapon |
|
||||
| **Tradeable** | No |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Stab Attack | +5 |
|
||||
| Slash Attack | +5 |
|
||||
| Crush Attack | +5 |
|
||||
| Stab Defence | +1 |
|
||||
| Slash Defence | +1 |
|
||||
| Strength Bonus | +6 |
|
||||
| Attack Speed | 6 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Tamayu's Spear Stall](../shops/tamayus-spear-stall.md) for 26 gp
|
||||
@@ -22,4 +22,4 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Authentic Throwing Weapons](../shops/authentic-throwing-weapons.md) for 3 gp
|
||||
- Sold by: [Authentic throwing weapons](../shops/authentic-throwing-weapons.md) for 3 gp
|
||||
@@ -20,3 +20,9 @@
|
||||
| Magic Attack | -4 |
|
||||
| Strength Bonus | +8 |
|
||||
| Attack Speed | 6 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Skulgrimen's Battle Gear](../shops/skulgrimens-battle-gear.md) for 61 gp
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (3/128)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (3/128)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Bruise blue snelm
|
||||
|
||||
*A moody blue snail shell helmet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Armour |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 2kg |
|
||||
| **Value** | 300 gp |
|
||||
| **Equipable** | Yes — Head |
|
||||
|
||||
## Combat Stats
|
||||
|
||||
| Stat | Value |
|
||||
|------|-------|
|
||||
| Magic Attack | -3 |
|
||||
| Range Attack | -1 |
|
||||
| Stab Defence | +7 |
|
||||
| Slash Defence | +8 |
|
||||
| Crush Defence | +6 |
|
||||
| Magic Defence | -1 |
|
||||
| Range Defence | +7 |
|
||||
@@ -9,6 +9,12 @@
|
||||
| **Weight** | 2200g |
|
||||
| **Value** | 6 gp |
|
||||
|
||||
## Spawn Locations
|
||||
|
||||
| Area | Floor | Quantity |
|
||||
|------|-------|----------|
|
||||
| m41_57 | Ground | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Funch's Fine Groceries](../shops/funchs-fine-groceries.md) for 6 gp
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Sigmund the Merchant](../shops/sigmund-the-merchant.md) for 7 gp
|
||||
- Sold by: [The Lighthouse Store](../shops/the-lighthouse-store.md) for 6 gp
|
||||
- Sold by: [Bedabin Village Bartering](../shops/bedabin-village-bartering.md) for 7 gp
|
||||
- Sold by: [Shantay Pass Shop](../shops/shantay-pass-shop.md) for 6 gp
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (1/128)
|
||||
- Dropped by: [Imp](../npcs/imp.md) (2/128)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (1/128)
|
||||
@@ -31,10 +31,13 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Razmiregeneralstore](../shops/razmiregeneralstore.md) for 2 gp
|
||||
- Sold by: [Karamja General Store](../shops/karamja-general-store.md) for 2 gp
|
||||
- Sold by: [Dal's General Ogre Supplies](../shops/dals-general-ogre-supplies.md) for 2 gp
|
||||
- Sold by: [Khazard General Store](../shops/khazard-general-store.md) for 2 gp
|
||||
- Sold by: [West Ardougne General Store](../shops/west-ardougne-general-store.md) for 2 gp
|
||||
- Sold by: [Sigmund the Merchant](../shops/sigmund-the-merchant.md) for 2 gp
|
||||
- Sold by: [The Lighthouse Store](../shops/the-lighthouse-store.md) for 2 gp
|
||||
- Sold by: [Varrock General Store](../shops/varrock-general-store.md) for 2 gp
|
||||
- Sold by: [General Store](../shops/general-store.md) for 4 gp
|
||||
- Sold by: [Burthorpe Supplies](../shops/burthorpe-supplies.md) for 2 gp
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Burnt eel
|
||||
|
||||
*It looks like it's seen one too many fires.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 2kg |
|
||||
| **Value** | 0 gp |
|
||||
@@ -8,3 +8,4 @@
|
||||
| **Members** | Yes |
|
||||
| **Weight** | 800g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -19,21 +19,15 @@
|
||||
|
||||
## Sources
|
||||
|
||||
- Sold by: [Sigmund the Merchant](../shops/sigmund-the-merchant.md) for 1 gp
|
||||
- Sold by: [Foodshop](../shops/foodshop.md) for 1 gp
|
||||
- Sold by: [Food Store](../shops/food-store.md) for 1 gp
|
||||
- Sold by: [Grand Tree Groceries](../shops/grand-tree-groceries.md) for 1 gp
|
||||
- Dropped by: [Unferth](../npcs/unferth.md) (1/128)
|
||||
- Dropped by: [Farmer](../npcs/farmer.md) (1/128)
|
||||
- Dropped by: [Ocga](../npcs/ocga.md) (1/128)
|
||||
- Dropped by: [Imp](../npcs/imp.md) (2/128)
|
||||
- Dropped by: [Al-Kharid warrior](../npcs/al-kharid-warrior.md) (1/128)
|
||||
- Dropped by: [Woman](../npcs/woman.md) (1/128)
|
||||
- Dropped by: [Breoca](../npcs/breoca.md) (1/128)
|
||||
- Dropped by: [Rogue](../npcs/rogue.md) (1/128)
|
||||
- Dropped by: [Hygd](../npcs/hygd.md) (1/128)
|
||||
- Dropped by: [Thief](../npcs/thief.md) (1/128)
|
||||
- Dropped by: [Ceolburg](../npcs/ceolburg.md) (1/128)
|
||||
- Dropped by: [Mugger](../npcs/mugger.md) (1/128)
|
||||
- Dropped by: [Man](../npcs/man.md) (1/128)
|
||||
- Dropped by: [Hild](../npcs/hild.md) (1/128)
|
||||
- Dropped by: [Penda](../npcs/penda.md) (1/128)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Cadava berries
|
||||
|
||||
*Poisonous berries.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Other |
|
||||
| **Members** | No |
|
||||
| **Weight** | 7g |
|
||||
| **Value** | 0 gp |
|
||||
|
||||
## Spawn Locations
|
||||
|
||||
| Area | Floor | Quantity |
|
||||
|------|-------|----------|
|
||||
| Varrock (east) | Ground | 1 |
|
||||
|
||||
## Sources
|
||||
|
||||
- Dropped by: [Imp](../npcs/imp.md) (4/128)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Cadava potion
|
||||
|
||||
*I'm meant to give this to Juliet.*
|
||||
|
||||
| Detail | |
|
||||
|---|---|
|
||||
| **Type** | Quest Item |
|
||||
| **Members** | No |
|
||||
| **Weight** | 25g |
|
||||
| **Value** | 0 gp |
|
||||
| **Tradeable** | No |
|
||||
@@ -12,3 +12,5 @@
|
||||
## Sources
|
||||
|
||||
- Sold by: [Ardougne Baker's Stall](../shops/ardougne-bakers-stall.md) for 50 gp
|
||||
- Dropped by: [Warrior](../npcs/warrior.md) (4/128)
|
||||
- Dropped by: [Market Guard](../npcs/market-guard.md) (4/128)
|
||||
@@ -12,5 +12,7 @@
|
||||
## Sources
|
||||
|
||||
- Sold by: [Jiminua's Jungle Store](../shops/jiminuas-jungle-store.md) for 4 gp
|
||||
- Sold by: [Sigmund the Merchant](../shops/sigmund-the-merchant.md) for 3 gp
|
||||
- Sold by: [The Lighthouse Store](../shops/the-lighthouse-store.md) for 3 gp
|
||||
- Sold by: [Candle Shop](../shops/candle-shop.md) for 3 gp
|
||||
- Sold by: [Obli's General Store](../shops/oblis-general-store.md) for 4 gp
|
||||
Loaded 100 of 1164 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user