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>
721 lines
25 KiB
TypeScript
721 lines
25 KiB
TypeScript
/**
|
|
* Smithing Training Script v4
|
|
*
|
|
* Goal: Train Smithing to level 10+ from a fresh Lumbridge spawn
|
|
*
|
|
* Strategy (v4):
|
|
* 1. Sell shortbow for coins (toll money)
|
|
* 2. Walk to SE Varrock mine copper/tin area (around 3289, 3363)
|
|
* 3. Mine copper/tin ore (balanced)
|
|
* 4. Walk to Al-Kharid and pay toll
|
|
* 5. Smelt bronze bars at Al-Kharid furnace
|
|
* 6. Repeat until level 10
|
|
*
|
|
* v4 Changes:
|
|
* - Back to SE Varrock mine (has copper/tin, Al-Kharid has iron/gold/scorpions)
|
|
* - Better copper/tin detection by checking specific rock locations
|
|
*/
|
|
|
|
import { runScript, type ScriptContext } from '../../sdk/runner';
|
|
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
|
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
|
|
|
// Locations
|
|
const LUMBRIDGE_SHOP = { x: 3211, z: 3247 };
|
|
const AL_KHARID_GATE = { x: 3268, z: 3228 };
|
|
const AL_KHARID_FURNACE = { x: 3274, z: 3186 };
|
|
const AL_KHARID_INSIDE = { x: 3277, z: 3227 };
|
|
|
|
// SE Varrock mine - copper/tin area is in the central-east section
|
|
// Copper rocks around (3287-3290, 3361-3366)
|
|
// Tin rocks around (3282-3286, 3364-3368)
|
|
const SE_VARROCK_MINE_COPPERTIN = { x: 3286, z: 3365 };
|
|
|
|
// Waypoints from Lumbridge to SE Varrock mine copper/tin area
|
|
// Note: We use all waypoints to ensure we reach the correct spot
|
|
const WAYPOINTS_TO_MINE = [
|
|
{ x: 3222, z: 3250 }, // North of Lumbridge
|
|
{ x: 3235, z: 3290 }, // Continue north
|
|
{ x: 3255, z: 3320 }, // Approaching mine
|
|
{ x: 3275, z: 3350 }, // Near mine entrance
|
|
SE_VARROCK_MINE_COPPERTIN, // Final destination: 3286, 3365
|
|
];
|
|
|
|
// Get smithing stats
|
|
function getSmithingStats(ctx: ScriptContext): { level: number; xp: number } {
|
|
const state = ctx.sdk.getState();
|
|
const smithing = state?.skills.find(s => s.name === 'Smithing');
|
|
return {
|
|
level: smithing?.baseLevel ?? 1,
|
|
xp: smithing?.experience ?? 0
|
|
};
|
|
}
|
|
|
|
// Get current coin count
|
|
function getCoins(ctx: ScriptContext): number {
|
|
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
|
|
return coins?.count ?? 0;
|
|
}
|
|
|
|
// Check if inside Al Kharid
|
|
function isInsideAlKharid(ctx: ScriptContext): boolean {
|
|
const state = ctx.sdk.getState();
|
|
return (state?.player?.worldX ?? 0) >= 3270;
|
|
}
|
|
|
|
// Count ore in inventory
|
|
function countOre(ctx: ScriptContext): { copper: number; tin: number; bronzeBars: number } {
|
|
const inv = ctx.sdk.getState()?.inventory ?? [];
|
|
return {
|
|
copper: inv.filter(i => /copper ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
|
tin: inv.filter(i => /tin ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
|
bronzeBars: inv.filter(i => /bronze bar/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
|
|
};
|
|
}
|
|
|
|
// Get coins by selling items at Lumbridge shop
|
|
async function getCoinsFromShop(ctx: ScriptContext): Promise<boolean> {
|
|
console.log('Selling items for toll money...');
|
|
|
|
await ctx.bot.walkTo(LUMBRIDGE_SHOP.x, LUMBRIDGE_SHOP.z);
|
|
|
|
const openResult = await ctx.bot.openShop(/shop keeper/i);
|
|
if (!openResult.success) {
|
|
console.log(`Failed to open shop: ${openResult.message}`);
|
|
return false;
|
|
}
|
|
|
|
// Sell shortbow (worth ~20gp)
|
|
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 1);
|
|
if (sellResult.success) {
|
|
console.log(sellResult.message);
|
|
}
|
|
|
|
await ctx.bot.closeShop();
|
|
|
|
const coins = getCoins(ctx);
|
|
console.log(`Have ${coins}gp after selling`);
|
|
return coins >= 10;
|
|
}
|
|
|
|
// Walk to SE Varrock mine
|
|
// The pathfinder should handle routing automatically
|
|
async function walkToMine(ctx: ScriptContext): Promise<void> {
|
|
console.log('Walking to SE Varrock mine...');
|
|
|
|
const currentPos = ctx.sdk.getState()?.player;
|
|
console.log(`From (${currentPos?.worldX}, ${currentPos?.worldZ}) to (${SE_VARROCK_MINE_COPPERTIN.x}, ${SE_VARROCK_MINE_COPPERTIN.z})`);
|
|
|
|
// Try direct walk to mine
|
|
let result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
|
|
|
|
if (!result.success) {
|
|
console.log(`Direct walk failed: ${result.message}`);
|
|
|
|
// Try via an intermediate point (around Varrock east)
|
|
console.log('Trying via Varrock east gate area...');
|
|
const intermediate = { x: 3275, z: 3340 };
|
|
result = await ctx.bot.walkTo(intermediate.x, intermediate.z);
|
|
if (result.success) {
|
|
result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
|
|
}
|
|
}
|
|
|
|
|
|
// Dismiss any dialogs
|
|
const state = ctx.sdk.getState();
|
|
if (state?.dialog.isOpen) {
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
}
|
|
|
|
const pos = ctx.sdk.getState()?.player;
|
|
console.log(`At mine: (${pos?.worldX}, ${pos?.worldZ})`);
|
|
}
|
|
|
|
// Prospect a rock to find what ore it contains
|
|
async function prospectRock(ctx: ScriptContext, rock: { x: number; z: number; id: number; optionsWithIndex: Array<{ text: string; opIndex: number }> }): Promise<string | null> {
|
|
const prospectOpt = rock.optionsWithIndex.find(o => /prospect/i.test(o.text));
|
|
if (!prospectOpt) return null;
|
|
|
|
const startTick = ctx.sdk.getState()?.tick ?? 0;
|
|
|
|
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, prospectOpt.opIndex);
|
|
await new Promise(r => setTimeout(r, 800)); // Brief wait for action
|
|
|
|
try {
|
|
const state = await ctx.sdk.waitForCondition(s => {
|
|
for (const msg of s.gameMessages) {
|
|
if (msg.tick > startTick) {
|
|
const text = msg.text.toLowerCase();
|
|
if (text.includes('rock contains') || text.includes('ore') || text.includes('no ore') || text.includes('nothing')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}, 3000); // Reduced from 5000
|
|
|
|
// Extract ore type from message
|
|
for (const msg of state.gameMessages) {
|
|
if (msg.tick > startTick) {
|
|
const text = msg.text.toLowerCase();
|
|
if (text.includes('copper')) return 'copper';
|
|
if (text.includes('tin')) return 'tin';
|
|
if (text.includes('iron')) return 'iron';
|
|
if (text.includes('gold')) return 'gold';
|
|
if (text.includes('coal')) return 'coal';
|
|
if (text.includes('no ore') || text.includes('nothing')) return 'empty';
|
|
}
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Find and mine a rock (copper or tin)
|
|
// Use prospecting to filter out iron/gold, but don't try to balance
|
|
async function mineRock(ctx: ScriptContext): Promise<boolean> {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) return false;
|
|
|
|
// Dismiss dialogs first
|
|
if (state.dialog.isOpen) {
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
}
|
|
|
|
// Find rocks with Mine option - sort by distance
|
|
const rocks = state.nearbyLocs.filter(loc =>
|
|
/rocks?/i.test(loc.name) && !/rockslide/i.test(loc.name) &&
|
|
loc.optionsWithIndex.some(o => /mine/i.test(o.text))
|
|
).sort((a, b) => a.distance - b.distance);
|
|
|
|
if (rocks.length === 0) {
|
|
console.log('No rocks nearby');
|
|
return false;
|
|
}
|
|
|
|
// Try prospecting and mining rocks
|
|
for (const rock of rocks.slice(0, 5)) {
|
|
// Prospect to check ore type
|
|
const oreType = await prospectRock(ctx, rock);
|
|
|
|
// Skip non-copper/tin or empty rocks
|
|
if (!oreType || oreType === 'empty') continue;
|
|
if (oreType !== 'copper' && oreType !== 'tin') {
|
|
console.log(`Skip ${oreType} rock`);
|
|
continue;
|
|
}
|
|
|
|
// Mine the rock immediately after prospecting
|
|
const mineOpt = rock.optionsWithIndex.find(o => /mine/i.test(o.text));
|
|
if (!mineOpt) continue;
|
|
|
|
const xpBefore = ctx.sdk.getState()?.skills.find(s => s.name === 'Mining')?.experience ?? 0;
|
|
const invCountBefore = ctx.sdk.getState()?.inventory.length ?? 0;
|
|
|
|
console.log(`Mining ${oreType} at (${rock.x}, ${rock.z})...`);
|
|
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, mineOpt.opIndex);
|
|
|
|
// Wait for mining to complete
|
|
try {
|
|
await ctx.sdk.waitForCondition(state => {
|
|
// Dismiss any dialogs
|
|
if (state.dialog.isOpen) {
|
|
ctx.sdk.sendClickDialog(0).catch(() => {});
|
|
}
|
|
|
|
// Check for XP gain or inventory change
|
|
const miningXp = state.skills.find(s => s.name === 'Mining')?.experience ?? 0;
|
|
return miningXp > xpBefore || state.inventory.length > invCountBefore;
|
|
}, 8000);
|
|
|
|
const currentOre = countOre(ctx);
|
|
console.log(`Mined! (${currentOre.copper} copper, ${currentOre.tin} tin)`);
|
|
return true;
|
|
} catch {
|
|
console.log('Mining timeout');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Mine ore until we have enough copper and tin
|
|
async function mineOre(ctx: ScriptContext, targetPairs: number): Promise<void> {
|
|
console.log(`Mining ${targetPairs} pairs of ore...`);
|
|
|
|
let consecutiveFails = 0;
|
|
|
|
while (consecutiveFails < 5) {
|
|
const ore = countOre(ctx);
|
|
const pairs = Math.min(ore.copper, ore.tin);
|
|
|
|
if (pairs >= targetPairs) {
|
|
console.log(`Have ${pairs} ore pairs, done mining`);
|
|
break;
|
|
}
|
|
|
|
// Check inventory space
|
|
const invCount = ctx.sdk.getState()?.inventory.length ?? 0;
|
|
if (invCount >= 26) { // Leave some space
|
|
console.log('Inventory nearly full');
|
|
break;
|
|
}
|
|
|
|
// Log current ore counts
|
|
console.log(`Mining... (copper: ${ore.copper}, tin: ${ore.tin})`);
|
|
|
|
// Just mine any copper or tin we find - don't try to balance
|
|
// The preference logic was causing issues where rocks respawn as different ore
|
|
const result = await mineRock(ctx);
|
|
|
|
if (!result) {
|
|
consecutiveFails++;
|
|
console.log(`Mining failed (${consecutiveFails}/5)`);
|
|
|
|
// Walk around to find rocks
|
|
const currentPos = ctx.sdk.getState()?.player;
|
|
if (currentPos) {
|
|
const offsetX = (Math.random() - 0.5) * 15;
|
|
const offsetZ = (Math.random() - 0.5) * 15;
|
|
await ctx.bot.walkTo(
|
|
Math.round(SE_VARROCK_MINE_COPPERTIN.x + offsetX),
|
|
Math.round(SE_VARROCK_MINE_COPPERTIN.z + offsetZ)
|
|
);
|
|
}
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
} else {
|
|
consecutiveFails = 0;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Pay toll and enter Al Kharid
|
|
async function enterAlKharid(ctx: ScriptContext): Promise<boolean> {
|
|
if (isInsideAlKharid(ctx)) {
|
|
console.log('Already in Al Kharid');
|
|
return true;
|
|
}
|
|
|
|
console.log('Walking to toll gate...');
|
|
await ctx.bot.walkTo(AL_KHARID_GATE.x, AL_KHARID_GATE.z);
|
|
|
|
// Find and interact with gate
|
|
const state = ctx.sdk.getState();
|
|
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 10);
|
|
|
|
if (!gate) {
|
|
console.log('Gate not found!');
|
|
return false;
|
|
}
|
|
|
|
const openOpt = gate.optionsWithIndex.find(o => /pay|open/i.test(o.text));
|
|
if (!openOpt) {
|
|
console.log('No open option on gate');
|
|
return false;
|
|
}
|
|
|
|
console.log('Interacting with gate...');
|
|
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
|
|
await new Promise(r => setTimeout(r, 800));
|
|
|
|
// Handle dialog - pay toll
|
|
for (let i = 0; i < 20; i++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) {
|
|
await new Promise(r => setTimeout(r, 150));
|
|
continue;
|
|
}
|
|
|
|
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
|
|
if (yesOpt) {
|
|
console.log('Paying toll...');
|
|
await ctx.sdk.sendClickDialog(yesOpt.index);
|
|
break;
|
|
}
|
|
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 500));
|
|
|
|
// Dismiss remaining dialogs
|
|
for (let i = 0; i < 5; i++) {
|
|
const s = ctx.sdk.getState();
|
|
if (!s?.dialog.isOpen) break;
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 500));
|
|
|
|
// Walk through gate
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
await ctx.bot.walkTo(AL_KHARID_INSIDE.x, AL_KHARID_INSIDE.z);
|
|
await new Promise(r => setTimeout(r, 500));
|
|
|
|
if (isInsideAlKharid(ctx)) {
|
|
console.log('Entered Al Kharid!');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return isInsideAlKharid(ctx);
|
|
}
|
|
|
|
// Smelt bronze bars at furnace
|
|
async function smeltBars(ctx: ScriptContext): Promise<number> {
|
|
console.log('Walking to furnace...');
|
|
await ctx.bot.walkTo(AL_KHARID_FURNACE.x, AL_KHARID_FURNACE.z);
|
|
|
|
let barsSmelted = 0;
|
|
|
|
while (true) {
|
|
const ore = countOre(ctx);
|
|
const pairs = Math.min(ore.copper, ore.tin);
|
|
|
|
if (pairs === 0) {
|
|
console.log('No more ore pairs');
|
|
break;
|
|
}
|
|
|
|
// Dismiss any dialogs
|
|
const state = ctx.sdk.getState();
|
|
if (state?.dialog.isOpen) {
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
continue;
|
|
}
|
|
|
|
// Find furnace
|
|
const furnace = state?.nearbyLocs.find(l => /furnace/i.test(l.name));
|
|
if (!furnace) {
|
|
console.log('Furnace not found');
|
|
break;
|
|
}
|
|
|
|
// Find copper ore in inventory
|
|
const copper = ctx.sdk.findInventoryItem(/copper ore/i);
|
|
if (!copper) {
|
|
console.log('No copper ore');
|
|
break;
|
|
}
|
|
|
|
const smithingXpBefore = getSmithingStats(ctx).xp;
|
|
|
|
// Use copper ore on furnace
|
|
console.log('Using ore on furnace...');
|
|
await ctx.sdk.sendUseItemOnLoc(copper.slot, furnace.x, furnace.z, furnace.id);
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// Handle the smelting interface/dialog
|
|
// The smelting interface should appear - click to make bronze bar
|
|
for (let i = 0; i < 10; i++) {
|
|
const s = ctx.sdk.getState();
|
|
|
|
// Check if dialog opened (smelting menu)
|
|
if (s?.dialog.isOpen) {
|
|
// Look for bronze bar option
|
|
const bronzeOpt = s.dialog.options.find(o => /bronze/i.test(o.text));
|
|
if (bronzeOpt) {
|
|
console.log('Selecting bronze bar...');
|
|
await ctx.sdk.sendClickDialog(bronzeOpt.index);
|
|
break;
|
|
}
|
|
|
|
// Look for "Make" or "Ok" button
|
|
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
|
|
if (makeOpt) {
|
|
await ctx.sdk.sendClickDialog(makeOpt.index);
|
|
break;
|
|
}
|
|
|
|
await ctx.sdk.sendClickDialog(0);
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
}
|
|
|
|
// Wait for smelting to complete (XP gain)
|
|
try {
|
|
await ctx.sdk.waitForCondition(state => {
|
|
if (state.dialog.isOpen) {
|
|
ctx.sdk.sendClickDialog(0).catch(() => {});
|
|
}
|
|
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
|
|
return smithingXp > smithingXpBefore;
|
|
}, 10000);
|
|
|
|
barsSmelted++;
|
|
console.log(`Smelted bar #${barsSmelted}`);
|
|
} catch {
|
|
console.log('Smelting timeout');
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
}
|
|
|
|
return barsSmelted;
|
|
}
|
|
|
|
// Find and use anvil to smith items
|
|
async function smithItems(ctx: ScriptContext): Promise<number> {
|
|
// Check for anvil near furnace first
|
|
let anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
|
|
|
if (!anvil) {
|
|
// Try walking around Al Kharid to find anvil
|
|
console.log('Looking for anvil in Al Kharid...');
|
|
|
|
// Known Al Kharid anvil location (near general store)
|
|
const alKharidAnvil = { x: 3290, z: 3179 };
|
|
await ctx.bot.walkTo(alKharidAnvil.x, alKharidAnvil.z);
|
|
|
|
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
|
}
|
|
|
|
if (!anvil) {
|
|
console.log('No anvil found, trying Varrock...');
|
|
// Walk to Varrock west anvil
|
|
await ctx.bot.walkTo(3188, 3427);
|
|
|
|
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
|
|
}
|
|
|
|
if (!anvil) {
|
|
console.log('Cannot find anvil!');
|
|
return 0;
|
|
}
|
|
|
|
console.log(`Found anvil at (${anvil.x}, ${anvil.z})`);
|
|
|
|
// Check for hammer
|
|
const hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
|
if (!hammer) {
|
|
console.log('No hammer in inventory!');
|
|
return 0;
|
|
}
|
|
|
|
let itemsSmithed = 0;
|
|
|
|
while (true) {
|
|
const ore = countOre(ctx);
|
|
if (ore.bronzeBars === 0) {
|
|
console.log('No bronze bars left');
|
|
break;
|
|
}
|
|
|
|
// Dismiss any dialogs
|
|
const state = ctx.sdk.getState();
|
|
if (state?.dialog.isOpen) {
|
|
await ctx.sdk.sendClickDialog(0);
|
|
await new Promise(r => setTimeout(r, 300));
|
|
continue;
|
|
}
|
|
|
|
// Find bronze bar
|
|
const bar = ctx.sdk.findInventoryItem(/bronze bar/i);
|
|
if (!bar) {
|
|
console.log('No bronze bar');
|
|
break;
|
|
}
|
|
|
|
const smithingXpBefore = getSmithingStats(ctx).xp;
|
|
|
|
// Use bar on anvil
|
|
console.log('Using bar on anvil...');
|
|
await ctx.sdk.sendUseItemOnLoc(bar.slot, anvil.x, anvil.z, anvil.id);
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// Handle smithing interface
|
|
// Smithing interface (id 994) uses iop not buttonType
|
|
// We need to click a specific component to make items
|
|
for (let i = 0; i < 15; i++) {
|
|
const s = ctx.sdk.getState();
|
|
|
|
// Check dialog
|
|
if (s?.dialog.isOpen) {
|
|
// Look for bronze dagger (1 bar, level 1)
|
|
const daggerOpt = s.dialog.options.find(o => /dagger/i.test(o.text));
|
|
if (daggerOpt) {
|
|
console.log('Selecting dagger...');
|
|
await ctx.sdk.sendClickDialog(daggerOpt.index);
|
|
break;
|
|
}
|
|
|
|
// Try clicking first "Make" or "Ok" option
|
|
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
|
|
if (makeOpt) {
|
|
await ctx.sdk.sendClickDialog(makeOpt.index);
|
|
break;
|
|
}
|
|
|
|
// Try first option
|
|
if (s.dialog.options.length > 0) {
|
|
await ctx.sdk.sendClickDialog(s.dialog.options[0]?.index ?? 0);
|
|
}
|
|
}
|
|
|
|
// Check interface state
|
|
if (s?.interface?.isOpen) {
|
|
console.log(`Interface open: ${s.interface.interfaceId}, options: ${s.interface.options.map(o => o.text).join(', ')}`);
|
|
|
|
// Try clicking first option
|
|
if (s.interface.options.length > 0) {
|
|
await ctx.sdk.sendClickInterfaceOption(0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
}
|
|
|
|
// Wait for smithing (XP gain)
|
|
try {
|
|
await ctx.sdk.waitForCondition(state => {
|
|
if (state.dialog.isOpen) {
|
|
ctx.sdk.sendClickDialog(0).catch(() => {});
|
|
}
|
|
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
|
|
return smithingXp > smithingXpBefore;
|
|
}, 10000);
|
|
|
|
itemsSmithed++;
|
|
console.log(`Smithed item #${itemsSmithed}`);
|
|
} catch {
|
|
console.log('Smithing timeout');
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
}
|
|
|
|
return itemsSmithed;
|
|
}
|
|
|
|
// Drop items to make space
|
|
async function dropItems(ctx: ScriptContext, pattern: RegExp): Promise<void> {
|
|
const items = ctx.sdk.getState()?.inventory.filter(i => pattern.test(i.name)) ?? [];
|
|
for (const item of items) {
|
|
await ctx.sdk.sendDropItem(item.slot);
|
|
await new Promise(r => setTimeout(r, 100));
|
|
}
|
|
}
|
|
|
|
// Equip pickaxe if not already equipped
|
|
async function equipPickaxe(ctx: ScriptContext): Promise<boolean> {
|
|
const equipped = ctx.sdk.findEquipmentItem(/pickaxe/i);
|
|
if (equipped) {
|
|
console.log('Pickaxe already equipped');
|
|
return true;
|
|
}
|
|
|
|
const pickaxe = ctx.sdk.findInventoryItem(/pickaxe/i);
|
|
if (!pickaxe) {
|
|
console.log('No pickaxe in inventory!');
|
|
return false;
|
|
}
|
|
|
|
console.log(`Equipping ${pickaxe.name}...`);
|
|
const result = await ctx.bot.equipItem(pickaxe);
|
|
return result.success;
|
|
}
|
|
|
|
// Main script
|
|
async function main() {
|
|
const username = `sm${Math.random().toString(36).slice(2, 7)}`;
|
|
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
|
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
|
|
|
try {
|
|
await runScript(async (ctx) => {
|
|
|
|
console.log('=== Smithing Training Script v2 ===');
|
|
|
|
const startStats = getSmithingStats(ctx);
|
|
console.log(`Starting Smithing: Level ${startStats.level} (${startStats.xp} XP)`);
|
|
|
|
// Equip pickaxe first
|
|
await equipPickaxe(ctx);
|
|
|
|
// Check for hammer (needed for smithing at anvil)
|
|
let hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
|
if (!hammer) {
|
|
console.log('No hammer - will only be able to smelt bars');
|
|
// Smelting still gives XP, so we can train without a hammer
|
|
}
|
|
|
|
// Main loop
|
|
let iterations = 0;
|
|
while (iterations < 20) { // Safety limit
|
|
iterations++;
|
|
const stats = getSmithingStats(ctx);
|
|
|
|
if (stats.level >= 10) {
|
|
console.log(`Goal achieved! Smithing level ${stats.level}`);
|
|
break;
|
|
}
|
|
|
|
console.log(`\n=== Iteration ${iterations}: Smithing Level ${stats.level} (${stats.xp} XP) ===`);
|
|
|
|
// Step 1: Get coins if we don't have enough (for toll)
|
|
if (getCoins(ctx) < 10 && !isInsideAlKharid(ctx)) {
|
|
await getCoinsFromShop(ctx);
|
|
}
|
|
|
|
// Step 2: Mine ore if we don't have enough
|
|
const ore = countOre(ctx);
|
|
console.log(`Inventory: ${ore.copper} copper, ${ore.tin} tin, ${ore.bronzeBars} bars`);
|
|
|
|
const pairs = Math.min(ore.copper, ore.tin);
|
|
if (pairs < 5) {
|
|
await walkToMine(ctx);
|
|
await mineOre(ctx, 8); // Mine 8 pairs
|
|
}
|
|
|
|
// Step 3: Travel to Al-Kharid for smelting
|
|
if (!isInsideAlKharid(ctx)) {
|
|
const entered = await enterAlKharid(ctx);
|
|
if (!entered) {
|
|
console.log('Failed to enter Al Kharid, retrying...');
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Step 4: Smelt bars
|
|
const oreAfterMining = countOre(ctx);
|
|
const pairsToSmelt = Math.min(oreAfterMining.copper, oreAfterMining.tin);
|
|
if (pairsToSmelt > 0) {
|
|
console.log(`Smelting ${pairsToSmelt} bronze bars...`);
|
|
const smelted = await smeltBars(ctx);
|
|
console.log(`Smelted ${smelted} bars`);
|
|
}
|
|
|
|
// Step 5: Smith items (if we have a hammer)
|
|
hammer = ctx.sdk.findInventoryItem(/hammer/i);
|
|
const barsCount = countOre(ctx);
|
|
if (hammer && barsCount.bronzeBars > 0) {
|
|
console.log(`Smithing with ${barsCount.bronzeBars} bars...`);
|
|
const smithed = await smithItems(ctx);
|
|
console.log(`Smithed ${smithed} items`);
|
|
}
|
|
|
|
// Drop smithed items to make inventory space
|
|
await dropItems(ctx, /bronze (dagger|sword|axe|mace|med helm|bolts)/i);
|
|
}
|
|
|
|
const endStats = getSmithingStats(ctx);
|
|
console.log(`\n=== Complete: Level ${endStats.level} (${endStats.xp} XP) ===`);
|
|
}, {
|
|
connection: { bot: session.bot, sdk: session.sdk },
|
|
timeout: 15 * 60 * 1000, // 15 minutes
|
|
});
|
|
} finally {
|
|
await session.cleanup();
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|