mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
Removes the dedup logging system (LogEntry, captured log functions, flush timers) from the runner and the log/warn/error fields from ScriptContext. Scripts now use standard console.log/warn/error instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
640 lines
22 KiB
TypeScript
640 lines
22 KiB
TypeScript
/**
|
|
* Thieving GP Maximizer Script
|
|
*
|
|
* Goal: Maximize coins earned through thieving in 5 minutes.
|
|
*
|
|
* Strategy v11:
|
|
* - Level 1-9: Pickpocket Men at Lumbridge (3 GP each)
|
|
* - Level 10+: Walk to Farmers (9 GP each) - proven strategy from v7
|
|
* - Larger zone radius (40 tiles) to avoid constant zone re-entry
|
|
* - Walk TO Farmer spawn when no target found (not random)
|
|
*
|
|
* v7: 339 GP (Farmers with door handling) - BASELINE
|
|
* v8: 206 GP (REGRESSION - Men in Al-Kharid)
|
|
* v9: 36 GP (FAILURE - Warriors not found)
|
|
* v10: 195 GP (REGRESSION - constant zone re-walking)
|
|
* v11 Target: 350+ GP with Farmers only, optimized zone handling
|
|
*/
|
|
|
|
import { runScript, type ScriptContext } from '../../sdk/runner';
|
|
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
|
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
|
import type { NearbyNpc } from '../../sdk/types';
|
|
|
|
// Thieving stats tracking
|
|
interface ThievingStats {
|
|
pickpocketAttempts: number;
|
|
successfulPickpockets: number;
|
|
failedPickpockets: number;
|
|
coinsEarned: number;
|
|
startCoins: number;
|
|
startThievingXp: number;
|
|
foodEaten: number;
|
|
stunTimeMs: number;
|
|
}
|
|
|
|
// Stun duration in ticks (~4.5 seconds = 7-8 game ticks)
|
|
const STUN_TICKS = 8;
|
|
const STUN_MS = 4800;
|
|
|
|
// v10: Simple zone switching - Men then Farmers (proven reliable)
|
|
const ZONES = {
|
|
lumbridge: { x: 3222, z: 3218, targetPattern: /^(man|woman)$/i, levelReq: 1, coins: 3 },
|
|
// Farmer area near crop patches (proven in v7: 339 GP)
|
|
farmers: { x: 3167, z: 3283, targetPattern: /^farmer$/i, levelReq: 10, coins: 9 },
|
|
};
|
|
|
|
// Key locations
|
|
const LOCATIONS = {
|
|
lumbridgeGeneralStore: { x: 3211, z: 3247 },
|
|
alKharidTollGate: { x: 3268, z: 3228 },
|
|
alKharidInside: { x: 3277, z: 3227 },
|
|
kebabSeller: { x: 3273, z: 3180 }, // Karim the kebab seller (dialog, not shop)
|
|
};
|
|
|
|
// State tracking for one-time setup
|
|
interface SetupState {
|
|
soldShortbow: boolean;
|
|
paidToll: boolean;
|
|
boughtKebabs: boolean;
|
|
tollAttempted: boolean; // Track if we've tried to enter Al-Kharid
|
|
}
|
|
|
|
/**
|
|
* Walk to destination, opening any doors/gates encountered along the way
|
|
*/
|
|
async function walkToWithDoors(ctx: ScriptContext, destX: number, destZ: number, maxAttempts: number = 5): Promise<boolean> {
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
const state = ctx.sdk.getState();
|
|
if (!state?.player) return false;
|
|
|
|
// Check if we're close enough
|
|
const dx = Math.abs(state.player.worldX - destX);
|
|
const dz = Math.abs(state.player.worldZ - destZ);
|
|
if (dx <= 5 && dz <= 5) {
|
|
return true; // Arrived!
|
|
}
|
|
|
|
// Try walking
|
|
const walkResult = await ctx.bot.walkTo(destX, destZ);
|
|
|
|
// Check if we made progress
|
|
const stateAfter = ctx.sdk.getState();
|
|
if (stateAfter?.player) {
|
|
const dxAfter = Math.abs(stateAfter.player.worldX - destX);
|
|
const dzAfter = Math.abs(stateAfter.player.worldZ - destZ);
|
|
if (dxAfter <= 5 && dzAfter <= 5) {
|
|
return true; // Arrived!
|
|
}
|
|
}
|
|
|
|
// If walk failed or we're stuck, look for a door/gate to open
|
|
const doors = ctx.sdk.getNearbyLocs()
|
|
.filter(loc => /door|gate/i.test(loc.name))
|
|
.filter(loc => loc.options.some(o => /^open$/i.test(o)))
|
|
.sort((a, b) => a.distance - b.distance);
|
|
|
|
if (doors.length > 0) {
|
|
const door = doors[0]!;
|
|
console.log(`Opening ${door.name} at (${door.x}, ${door.z})...`);
|
|
const openResult = await ctx.bot.openDoor(door);
|
|
if (openResult.success) {
|
|
console.log(`Opened ${door.name}`);
|
|
continue; // Try walking again
|
|
} else {
|
|
console.warn(`Failed to open ${door.name}: ${openResult.message}`);
|
|
}
|
|
}
|
|
|
|
// If no doors found and walk failed, we might be stuck
|
|
if (!walkResult.success) {
|
|
console.warn(`Walk failed: ${walkResult.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if we're in Al-Kharid (x >= 3270)
|
|
*/
|
|
function isInAlKharid(ctx: ScriptContext): boolean {
|
|
const state = ctx.sdk.getState();
|
|
return (state?.player?.worldX ?? 0) >= 3270;
|
|
}
|
|
|
|
/**
|
|
* Sell shortbow at Lumbridge general store for 20gp (for toll + extra)
|
|
* From script_best_practices.md: shortbow sells for 20gp, better than bronze sword (10gp)
|
|
*/
|
|
async function sellShortbow(ctx: ScriptContext): Promise<boolean> {
|
|
const shortbow = ctx.sdk.findInventoryItem(/^shortbow$/i);
|
|
if (!shortbow) {
|
|
console.log('No shortbow to sell');
|
|
return false;
|
|
}
|
|
|
|
console.log('Selling shortbow for toll money (20gp)...');
|
|
|
|
// Walk to general store
|
|
await walkToWithDoors(ctx, LOCATIONS.lumbridgeGeneralStore.x, LOCATIONS.lumbridgeGeneralStore.z);
|
|
|
|
// Open shop
|
|
const shopResult = await ctx.bot.openShop(/shop keeper|shop assistant/i);
|
|
if (!shopResult.success) {
|
|
console.warn(`Failed to open shop: ${shopResult.message}`);
|
|
return false;
|
|
}
|
|
|
|
// Sell the shortbow
|
|
const sellResult = await ctx.bot.sellToShop(/shortbow/i);
|
|
if (!sellResult.success) {
|
|
console.warn(`Failed to sell shortbow: ${sellResult.message}`);
|
|
}
|
|
|
|
// Close shop
|
|
await ctx.bot.closeShop();
|
|
|
|
console.log('Sold shortbow for 20gp');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Pay toll and enter Al-Kharid (from script_best_practices.md)
|
|
*/
|
|
async function payTollAndEnterAlKharid(ctx: ScriptContext): Promise<boolean> {
|
|
const coins = countCoins(ctx);
|
|
if (coins < 10) {
|
|
console.warn(`Not enough coins for toll (have ${coins}, need 10)`);
|
|
return false;
|
|
}
|
|
|
|
console.log('Walking to Al-Kharid toll gate...');
|
|
await walkToWithDoors(ctx, LOCATIONS.alKharidTollGate.x, LOCATIONS.alKharidTollGate.z);
|
|
|
|
// Find and click the gate
|
|
const state = ctx.sdk.getState();
|
|
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name));
|
|
if (!gate) {
|
|
console.warn('Cannot find toll gate');
|
|
return false;
|
|
}
|
|
|
|
const openOpt = gate.optionsWithIndex.find(o => /open|pay/i.test(o.text));
|
|
if (!openOpt) {
|
|
console.warn('Cannot find open option on gate');
|
|
return false;
|
|
}
|
|
|
|
console.log('Opening toll gate...');
|
|
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
|
await new Promise(r => setTimeout(r, 800));
|
|
|
|
// Handle dialog - click through until "Yes" option appears
|
|
for (let i = 0; i < 20; i++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) {
|
|
await new Promise(r => setTimeout(r, 150));
|
|
continue;
|
|
}
|
|
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
|
if (yesOpt) {
|
|
console.log('Paying 10gp toll...');
|
|
await ctx.sdk.sendClickDialog(yesOpt.index);
|
|
break;
|
|
}
|
|
await ctx.sdk.sendClickDialog(0); // Click to continue
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
// Dismiss any remaining dialogs
|
|
for (let i = 0; i < 5; i++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) break;
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
// Wait and walk through aggressively
|
|
await new Promise(r => setTimeout(r, 600));
|
|
for (let i = 0; i < 5; i++) {
|
|
// Check if already in
|
|
if (isInAlKharid(ctx)) {
|
|
console.log('Entered Al-Kharid!');
|
|
return true;
|
|
}
|
|
|
|
console.log(`Walking through toll gate (attempt ${i + 1})...`);
|
|
await ctx.bot.walkTo(LOCATIONS.alKharidInside.x, LOCATIONS.alKharidInside.z);
|
|
await new Promise(r => setTimeout(r, 800));
|
|
}
|
|
|
|
return isInAlKharid(ctx);
|
|
}
|
|
|
|
/**
|
|
* Buy kebabs from Al-Kharid kebab seller (uses DIALOG, not shop!)
|
|
* From script_best_practices.md - Karim uses dialog system
|
|
*/
|
|
async function buyKebabs(ctx: ScriptContext, quantity: number = 5): Promise<boolean> {
|
|
console.log(`Buying ${quantity} kebabs via dialog...`);
|
|
|
|
// Walk to kebab seller (3273, 3180)
|
|
await walkToWithDoors(ctx, 3273, 3180);
|
|
|
|
const kebabsBefore = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
|
|
|
|
for (let i = 0; i < quantity; i++) {
|
|
// Check if we have enough coins (1gp per kebab)
|
|
const coins = countCoins(ctx);
|
|
if (coins < 1) {
|
|
console.warn('Not enough coins to buy kebab');
|
|
break;
|
|
}
|
|
|
|
// Find kebab seller (Karim)
|
|
const seller = ctx.sdk.findNearbyNpc(/kebab/i);
|
|
if (!seller) {
|
|
console.warn('Cannot find kebab seller');
|
|
return false;
|
|
}
|
|
|
|
// Talk to kebab seller
|
|
const talkOpt = seller.optionsWithIndex.find(o => /talk/i.test(o.text));
|
|
if (!talkOpt) {
|
|
console.warn('Cannot find talk option on kebab seller');
|
|
return false;
|
|
}
|
|
|
|
await ctx.sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// Handle dialog - click through and select "Yes please."
|
|
let bought = false;
|
|
for (let j = 0; j < 15; j++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) {
|
|
await new Promise(r => setTimeout(r, 200));
|
|
continue;
|
|
}
|
|
|
|
const buyOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
|
if (buyOpt) {
|
|
await ctx.sdk.sendClickDialog(buyOpt.index); // Buy kebab (1gp)
|
|
bought = true;
|
|
} else {
|
|
await ctx.sdk.sendClickDialog(0); // Click to continue
|
|
}
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
// Check if dialog closed after buying
|
|
if (bought) {
|
|
const sAfter = ctx.sdk.getState();
|
|
if (!sAfter?.dialog.isOpen) break;
|
|
}
|
|
}
|
|
|
|
// Dismiss any remaining dialogs
|
|
for (let j = 0; j < 3; j++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) break;
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
const kebabsAfter = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
|
|
const bought = kebabsAfter - kebabsBefore;
|
|
console.log(`Bought ${bought} kebabs (now have ${kebabsAfter})`);
|
|
return bought > 0;
|
|
}
|
|
|
|
/**
|
|
* Get the best zone for current thieving level
|
|
* v10: Simple - Men then Farmers (proven reliable from v7)
|
|
*/
|
|
function getBestZone(thievingLevel: number) {
|
|
// Level 10+: Farmers (9 GP) - proven in v7 with 339 GP
|
|
if (thievingLevel >= ZONES.farmers.levelReq) {
|
|
return ZONES.farmers;
|
|
}
|
|
// Level 1-9: Lumbridge Men (3 GP)
|
|
return ZONES.lumbridge;
|
|
}
|
|
|
|
/**
|
|
* Check if we're in a zone
|
|
* v11: Larger radius (40 tiles) to avoid constant zone re-entry
|
|
*/
|
|
function isInZone(ctx: ScriptContext, zone: typeof ZONES.lumbridge): boolean {
|
|
const state = ctx.sdk.getState();
|
|
if (!state?.player) return false;
|
|
const dx = Math.abs(state.player.worldX - zone.x);
|
|
const dz = Math.abs(state.player.worldZ - zone.z);
|
|
return dx <= 40 && dz <= 40; // Large radius - stay in zone once entered
|
|
}
|
|
|
|
/**
|
|
* Find nearest pickpocket target matching the pattern
|
|
*/
|
|
function findPickpocketTarget(ctx: ScriptContext, pattern: RegExp): NearbyNpc | null {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) return null;
|
|
|
|
const targets = state.nearbyNpcs
|
|
.filter(npc => pattern.test(npc.name))
|
|
.filter(npc => npc.options.some(o => /pickpocket/i.test(o)))
|
|
.sort((a, b) => a.distance - b.distance);
|
|
|
|
return targets[0] ?? null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if we should eat food based on HP
|
|
*/
|
|
function shouldEat(ctx: ScriptContext): boolean {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) return false;
|
|
|
|
const hp = state.skills.find(s => s.name === 'Hitpoints');
|
|
if (!hp) return false;
|
|
|
|
// v6: Be more conservative - only eat at HP <= 3 to preserve food
|
|
// Success rate improves with thieving level, so later attempts are safer
|
|
return hp.level <= 3;
|
|
}
|
|
|
|
/**
|
|
* Check if we have food available
|
|
*/
|
|
function hasFood(ctx: ScriptContext): boolean {
|
|
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
|
|
return food !== null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Count coins in inventory
|
|
*/
|
|
function countCoins(ctx: ScriptContext): number {
|
|
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
|
return coins?.count ?? 0;
|
|
}
|
|
|
|
/**
|
|
* Attempt to pickpocket an NPC
|
|
* Returns result and coins gained
|
|
*/
|
|
async function attemptPickpocket(
|
|
ctx: ScriptContext,
|
|
target: NearbyNpc,
|
|
stats: ThievingStats
|
|
): Promise<'success' | 'failed' | 'stunned' | 'error'> {
|
|
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
|
const coinsBefore = countCoins(ctx);
|
|
|
|
// Find the Pickpocket option
|
|
const pickpocketOpt = target.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
|
|
if (!pickpocketOpt) {
|
|
console.warn(`No pickpocket option on ${target.name}`);
|
|
return 'error';
|
|
}
|
|
|
|
// Send the pickpocket action
|
|
const result = await ctx.sdk.sendInteractNpc(target.index, pickpocketOpt.opIndex);
|
|
if (!result.success) {
|
|
console.warn(`Pickpocket action failed: ${result.message}`);
|
|
return 'error';
|
|
}
|
|
|
|
stats.pickpocketAttempts++;
|
|
|
|
// Wait for result - either coins increase or we get stunned
|
|
// Check for game messages about success/failure
|
|
try {
|
|
await ctx.sdk.waitForCondition(state => {
|
|
// Check for success message or coins increasing
|
|
for (const msg of state.gameMessages) {
|
|
if (msg.tick > startTick) {
|
|
const text = msg.text.toLowerCase();
|
|
// Success messages
|
|
if (text.includes('you pick') || text.includes("you steal")) {
|
|
return true;
|
|
}
|
|
// Failure/stun messages
|
|
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check if coins increased
|
|
const currentCoins = state.inventory.find(i => /^coins$/i.test(i.name))?.count ?? 0;
|
|
if (currentCoins > coinsBefore) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}, 5000);
|
|
|
|
// Check what happened
|
|
const currentState = ctx.sdk.getState();
|
|
if (!currentState) return 'error';
|
|
|
|
const coinsAfter = countCoins(ctx);
|
|
if (coinsAfter > coinsBefore) {
|
|
// Success!
|
|
const gained = coinsAfter - coinsBefore;
|
|
stats.successfulPickpockets++;
|
|
stats.coinsEarned += gained;
|
|
console.log(`Pickpocketed ${target.name}! +${gained} coins (total: ${stats.coinsEarned})`);
|
|
return 'success';
|
|
}
|
|
|
|
// Check for failure message
|
|
for (const msg of currentState.gameMessages) {
|
|
if (msg.tick > startTick) {
|
|
const text = msg.text.toLowerCase();
|
|
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
|
|
stats.failedPickpockets++;
|
|
console.log(`Failed pickpocket - stunned for ${STUN_MS}ms`);
|
|
// Wait out the stun duration
|
|
await new Promise(r => setTimeout(r, STUN_MS));
|
|
stats.stunTimeMs += STUN_MS;
|
|
return 'stunned';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unknown result
|
|
return 'failed';
|
|
} catch {
|
|
return 'error';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main thieving loop
|
|
*/
|
|
async function thievingLoop(ctx: ScriptContext): Promise<void> {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) throw new Error('No initial state');
|
|
|
|
// Initialize stats
|
|
const stats: ThievingStats = {
|
|
pickpocketAttempts: 0,
|
|
successfulPickpockets: 0,
|
|
failedPickpockets: 0,
|
|
coinsEarned: 0,
|
|
startCoins: countCoins(ctx),
|
|
startThievingXp: state.skills.find(s => s.name === 'Thieving')?.experience ?? 0,
|
|
foodEaten: 0,
|
|
stunTimeMs: 0,
|
|
};
|
|
|
|
// v8: Setup state for Al-Kharid access
|
|
const setup: SetupState = {
|
|
soldShortbow: false,
|
|
paidToll: false,
|
|
boughtKebabs: false,
|
|
tollAttempted: false,
|
|
};
|
|
|
|
console.log('=== Thieving GP Maximizer v11 Started ===');
|
|
console.log(`Starting coins: ${stats.startCoins}`);
|
|
console.log(`Starting thieving XP: ${stats.startThievingXp}`);
|
|
|
|
// Main loop
|
|
while (true) {
|
|
const currentState = ctx.sdk.getState();
|
|
if (!currentState) {
|
|
console.warn('Lost game state');
|
|
break;
|
|
}
|
|
|
|
// Dismiss any dialogs first
|
|
if (currentState.dialog.isOpen) {
|
|
console.log('Dismissing dialog...');
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 600));
|
|
continue;
|
|
}
|
|
|
|
// Check HP and eat if needed (conservative - only eat at HP <= 3)
|
|
if (shouldEat(ctx)) {
|
|
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
|
|
if (food) {
|
|
console.log(`HP low - eating ${food.name}`);
|
|
await ctx.bot.eatFood(food);
|
|
stats.foodEaten++;
|
|
continue;
|
|
}
|
|
// No food - if in Al-Kharid, try to buy more kebabs
|
|
if (setup.paidToll && isInAlKharid(ctx)) {
|
|
const coins = countCoins(ctx);
|
|
if (coins >= 5) {
|
|
console.log('Out of food in Al-Kharid - buying more kebabs...');
|
|
await buyKebabs(ctx, 5);
|
|
continue;
|
|
}
|
|
}
|
|
// No food and can't buy - continue anyway
|
|
}
|
|
|
|
// v10: Simple zone switching based on level
|
|
// Level 1-9: Men (3 GP), Level 10+: Farmers (9 GP)
|
|
const thievingLevel = currentState.skills.find(s => s.name === 'Thieving')?.baseLevel ?? 1;
|
|
|
|
// Determine best zone based on level (no setup needed!)
|
|
const bestZone = getBestZone(thievingLevel);
|
|
|
|
// Move to best zone if not there
|
|
if (!isInZone(ctx, bestZone)) {
|
|
const zoneName = bestZone === ZONES.farmers ? 'Farmers' : 'Lumbridge';
|
|
console.log(`Level ${thievingLevel}: Moving to ${zoneName} (${bestZone.coins} GP each)...`);
|
|
const arrived = await walkToWithDoors(ctx, bestZone.x, bestZone.z);
|
|
if (!arrived) {
|
|
console.warn('Failed to reach zone, trying again...');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Find target in current zone
|
|
const target = findPickpocketTarget(ctx, bestZone.targetPattern);
|
|
|
|
if (!target) {
|
|
// No targets nearby - walk TO the zone center (not random)
|
|
// This is where the target spawns, so walking there helps
|
|
console.log(`No targets nearby - walking to ${bestZone === ZONES.farmers ? 'Farmer spawn' : 'zone'}...`);
|
|
await ctx.bot.walkTo(bestZone.x, bestZone.z);
|
|
continue;
|
|
}
|
|
|
|
// Attempt pickpocket
|
|
const result = await attemptPickpocket(ctx, target, stats);
|
|
|
|
// Log periodic stats every 10 attempts
|
|
if (stats.pickpocketAttempts % 10 === 0 && stats.pickpocketAttempts > 0) {
|
|
logStats(ctx, stats);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log current thieving statistics
|
|
*/
|
|
function logStats(ctx: ScriptContext, stats: ThievingStats): void {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) return;
|
|
|
|
const currentThievingXp = state.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
|
|
const xpGained = currentThievingXp - stats.startThievingXp;
|
|
const successRate = stats.pickpocketAttempts > 0
|
|
? ((stats.successfulPickpockets / stats.pickpocketAttempts) * 100).toFixed(1)
|
|
: '0.0';
|
|
|
|
console.log(`--- Stats after ${stats.pickpocketAttempts} attempts ---`);
|
|
console.log(`Success rate: ${successRate}% (${stats.successfulPickpockets}/${stats.pickpocketAttempts})`);
|
|
console.log(`Coins earned: ${stats.coinsEarned} GP`);
|
|
console.log(`Thieving XP gained: +${xpGained}`);
|
|
console.log(`Food eaten: ${stats.foodEaten}`);
|
|
console.log(`Time stunned: ${(stats.stunTimeMs / 1000).toFixed(1)}s`);
|
|
}
|
|
|
|
// Main script
|
|
async function main() {
|
|
const username = `tg${Math.random().toString(36).slice(2, 7)}`;
|
|
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
|
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
|
|
|
try {
|
|
await runScript(async (ctx) => {
|
|
try {
|
|
await thievingLoop(ctx);
|
|
} finally {
|
|
// Log final stats
|
|
const state = ctx.sdk.getState();
|
|
if (state) {
|
|
const startThievingXp = 0; // We start at 0
|
|
const thieving = state.skills.find(s => s.name === 'Thieving');
|
|
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
|
|
|
console.log('=== Final Results ===');
|
|
console.log(`Thieving: Level ${thieving?.baseLevel ?? 1} (${thieving?.experience ?? 0} XP)`);
|
|
console.log(`Total coins in inventory: ${coins?.count ?? 0}`);
|
|
}
|
|
}
|
|
}, {
|
|
connection: { bot: session.bot, sdk: session.sdk },
|
|
timeout: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
} finally {
|
|
await session.cleanup();
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|