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>
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
/**
|
|
* Magic Trainer Script
|
|
*
|
|
* Goal: Train Magic to level 10+ starting from a fresh Lumbridge spawn.
|
|
*
|
|
* Strategy:
|
|
* - LUMBRIDGE_SPAWN preset provides: Air x25, Mind x15, Water x6, Earth x4, Body x2
|
|
* - Wind Strike = 1 Air + 1 Mind = 5.5 XP per cast
|
|
* - 15 Mind runes = 15 casts = 82.5 XP (gets to ~level 2-3)
|
|
* - Level 10 requires 1,154 XP = ~210 Wind Strikes
|
|
* - Need to find more runes or supplement with melee
|
|
*
|
|
* Plan:
|
|
* 1. Walk to chicken coop near Lumbridge (3235, 3295)
|
|
* 2. Cast Wind Strike on chickens until runes run out
|
|
* 3. Log progress and track XP gains
|
|
*/
|
|
|
|
import { runScript, type ScriptContext } from '../../sdk/runner';
|
|
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
|
|
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
|
|
import type { NearbyNpc } from '../../sdk/types';
|
|
|
|
// Spell component IDs
|
|
const Spells = {
|
|
WIND_STRIKE: 1152,
|
|
WATER_STRIKE: 1154,
|
|
EARTH_STRIKE: 1156,
|
|
};
|
|
|
|
// Locations
|
|
const CHICKEN_COOP = { x: 3235, z: 3295 }; // Near Lumbridge
|
|
|
|
// XP required for each level
|
|
const MAGIC_XP_TABLE = [
|
|
0, 0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, // 0-10
|
|
1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, // 11-20
|
|
];
|
|
|
|
/**
|
|
* Find the best target for magic training
|
|
*/
|
|
function findTarget(ctx: ScriptContext): NearbyNpc | null {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) return null;
|
|
|
|
// Prefer chickens (weakest), then rats, then goblins
|
|
const targets = state.nearbyNpcs
|
|
.filter(npc =>
|
|
/chicken/i.test(npc.name) ||
|
|
/rat/i.test(npc.name) ||
|
|
/goblin/i.test(npc.name)
|
|
)
|
|
.filter(npc => !npc.inCombat) // Don't target NPCs already fighting
|
|
.filter(npc => npc.distance <= 10) // Within reasonable range
|
|
.sort((a, b) => {
|
|
// Priority: chicken > rat > goblin
|
|
const priority = (name: string) => {
|
|
if (/chicken/i.test(name)) return 0;
|
|
if (/rat/i.test(name)) return 1;
|
|
return 2;
|
|
};
|
|
const pDiff = priority(a.name) - priority(b.name);
|
|
if (pDiff !== 0) return pDiff;
|
|
return a.distance - b.distance;
|
|
});
|
|
|
|
return targets[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get rune counts
|
|
*/
|
|
function getRuneCounts(ctx: ScriptContext): { air: number; mind: number; water: number; earth: number } {
|
|
const airRunes = ctx.sdk.findInventoryItem(/^air rune$/i);
|
|
const mindRunes = ctx.sdk.findInventoryItem(/^mind rune$/i);
|
|
const waterRunes = ctx.sdk.findInventoryItem(/^water rune$/i);
|
|
const earthRunes = ctx.sdk.findInventoryItem(/^earth rune$/i);
|
|
|
|
return {
|
|
air: airRunes?.count ?? 0,
|
|
mind: mindRunes?.count ?? 0,
|
|
water: waterRunes?.count ?? 0,
|
|
earth: earthRunes?.count ?? 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine which spell to cast based on available runes and magic level
|
|
*/
|
|
function getBestSpell(ctx: ScriptContext): { spell: number; name: string; xp: number } | null {
|
|
const runes = getRuneCounts(ctx);
|
|
const magicLevel = ctx.sdk.getSkill('Magic')?.baseLevel ?? 1;
|
|
|
|
// Wind Strike: 1 Air + 1 Mind, level 1, 5.5 XP
|
|
if (runes.air >= 1 && runes.mind >= 1) {
|
|
return { spell: Spells.WIND_STRIKE, name: 'Wind Strike', xp: 5.5 };
|
|
}
|
|
|
|
// Water Strike: 1 Water + 1 Air + 1 Mind, level 5, 7.5 XP
|
|
if (magicLevel >= 5 && runes.water >= 1 && runes.air >= 1 && runes.mind >= 1) {
|
|
return { spell: Spells.WATER_STRIKE, name: 'Water Strike', xp: 7.5 };
|
|
}
|
|
|
|
// Earth Strike: 2 Earth + 1 Air + 1 Mind, level 9, 9.5 XP
|
|
if (magicLevel >= 9 && runes.earth >= 2 && runes.air >= 1 && runes.mind >= 1) {
|
|
return { spell: Spells.EARTH_STRIKE, name: 'Earth Strike', xp: 9.5 };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
interface MagicStats {
|
|
startXp: number;
|
|
startLevel: number;
|
|
casts: number;
|
|
hits: number;
|
|
misses: number; // Splashes
|
|
lastCastTime: number;
|
|
startTime: number;
|
|
}
|
|
|
|
/**
|
|
* Main magic training loop
|
|
*/
|
|
async function magicTrainingLoop(ctx: ScriptContext): Promise<void> {
|
|
const state = ctx.sdk.getState();
|
|
if (!state) throw new Error('No initial state');
|
|
|
|
// Initialize stats
|
|
const magicSkill = ctx.sdk.getSkill('Magic');
|
|
const stats: MagicStats = {
|
|
startXp: magicSkill?.experience ?? 0,
|
|
startLevel: magicSkill?.baseLevel ?? 1,
|
|
casts: 0,
|
|
hits: 0,
|
|
misses: 0,
|
|
lastCastTime: 0,
|
|
startTime: Date.now(),
|
|
};
|
|
|
|
console.log('=== Magic Trainer ===');
|
|
console.log(`Goal: Train Magic from level ${stats.startLevel} (need 1,154 XP for level 10)`);
|
|
|
|
// Log rune inventory
|
|
const runes = getRuneCounts(ctx);
|
|
console.log(`Runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
|
|
console.log(`Potential casts: ~${Math.min(runes.air, runes.mind)} Wind Strikes (~${Math.min(runes.air, runes.mind) * 5.5} XP)`);
|
|
|
|
// Walk to chicken coop
|
|
console.log('Walking to chicken coop...');
|
|
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
|
|
|
|
// Open the gate to get inside the coop
|
|
console.log('Opening gate...');
|
|
const gateResult = await ctx.bot.openDoor(/gate/i);
|
|
if (gateResult.success) {
|
|
console.log(`Gate opened: ${gateResult.message}`);
|
|
// Walk inside the coop
|
|
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
|
|
} else {
|
|
console.log(`Gate: ${gateResult.message}`);
|
|
}
|
|
|
|
let noTargetCount = 0;
|
|
let failedCastCount = 0;
|
|
const MAX_NO_TARGET_ATTEMPTS = 30; // Exit if no targets for too long
|
|
const MAX_FAILED_CASTS = 5; // Exit if too many failed casts in a row
|
|
|
|
// Main loop
|
|
while (true) {
|
|
const currentState = ctx.sdk.getState();
|
|
if (!currentState) {
|
|
console.warn('Lost game state');
|
|
break;
|
|
}
|
|
|
|
// Dismiss any blocking dialogs (level-up, etc.)
|
|
if (currentState.dialog.isOpen) {
|
|
console.log('Dismissing dialog...');
|
|
await ctx.sdk.sendClickDialog(0);
|
|
continue;
|
|
}
|
|
|
|
// Check if we've reached level 10
|
|
const currentMagic = ctx.sdk.getSkill('Magic');
|
|
const currentLevel = currentMagic?.baseLevel ?? 1;
|
|
if (currentLevel >= 10) {
|
|
console.log('*** GOAL REACHED: Magic level 10! ***');
|
|
break;
|
|
}
|
|
|
|
// Check if we have runes
|
|
const spell = getBestSpell(ctx);
|
|
if (!spell) {
|
|
console.log('Out of runes! Cannot cast any more spells.');
|
|
break;
|
|
}
|
|
|
|
// Find a target
|
|
const target = findTarget(ctx);
|
|
if (!target) {
|
|
noTargetCount++;
|
|
if (noTargetCount >= MAX_NO_TARGET_ATTEMPTS) {
|
|
console.log(`No targets found after ${noTargetCount} attempts, exiting.`);
|
|
break;
|
|
}
|
|
if (noTargetCount % 5 === 0) {
|
|
console.log(`No targets nearby (attempt ${noTargetCount}/${MAX_NO_TARGET_ATTEMPTS}), walking around...`);
|
|
// Walk to chicken coop center
|
|
await ctx.bot.walkTo(
|
|
CHICKEN_COOP.x + Math.floor(Math.random() * 6) - 3,
|
|
CHICKEN_COOP.z + Math.floor(Math.random() * 6) - 3
|
|
);
|
|
}
|
|
await new Promise(r => setTimeout(r, 500));
|
|
continue;
|
|
}
|
|
noTargetCount = 0;
|
|
|
|
// Don't spam casts - wait between attempts
|
|
const now = Date.now();
|
|
if (now - stats.lastCastTime < 2500) {
|
|
await new Promise(r => setTimeout(r, 300));
|
|
continue;
|
|
}
|
|
|
|
// Walk closer if target is far (magic range is ~10 but need clear LOS)
|
|
if (target.distance > 5) {
|
|
console.log(`Walking toward ${target.name} at (${target.x}, ${target.z}), dist: ${target.distance}`);
|
|
// Walk to within ~3 tiles of the target
|
|
await ctx.bot.walkTo(target.x, target.z);
|
|
await new Promise(r => setTimeout(r, 500));
|
|
continue;
|
|
}
|
|
|
|
// Cast spell on target using high-level API
|
|
if (stats.casts % 5 === 0 || stats.casts === 0) {
|
|
const currentRunes = getRuneCounts(ctx);
|
|
console.log(`Casting ${spell.name} on ${target.name} (cast #${stats.casts + 1}, dist=${target.distance}, runes: air=${currentRunes.air}, mind=${currentRunes.mind})`);
|
|
}
|
|
|
|
const castResult = await ctx.bot.castSpellOnNpc(target, spell.spell);
|
|
stats.casts++;
|
|
stats.lastCastTime = now;
|
|
|
|
if (castResult.success) {
|
|
failedCastCount = 0; // Reset on success
|
|
if (castResult.hit) {
|
|
stats.hits++;
|
|
console.log(`HIT! ${castResult.message}`);
|
|
}
|
|
// Splash is still success, just no XP
|
|
} else if (castResult.reason === 'out_of_reach') {
|
|
// Gate likely closed - try to open it
|
|
console.log(`Can't reach target - trying to open gate`);
|
|
const gateResult = await ctx.bot.openDoor(/gate/i);
|
|
console.log(`Gate: ${gateResult.message}`);
|
|
if (gateResult.success) {
|
|
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
|
|
}
|
|
failedCastCount++;
|
|
} else if (castResult.reason === 'no_runes') {
|
|
console.log('Out of runes!');
|
|
break;
|
|
} else {
|
|
// Unknown failure reason
|
|
failedCastCount++;
|
|
console.log(`Cast failed: ${castResult.message} (reason: ${castResult.reason})`);
|
|
}
|
|
|
|
// Exit if too many consecutive failures
|
|
if (failedCastCount >= MAX_FAILED_CASTS) {
|
|
console.log(`Too many failed casts (${failedCastCount}), exiting.`);
|
|
break;
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
|
|
// Final report
|
|
logFinalStats(ctx, stats);
|
|
}
|
|
|
|
/**
|
|
* Log final training statistics
|
|
*/
|
|
function logFinalStats(ctx: ScriptContext, stats: MagicStats): void {
|
|
const currentMagic = ctx.sdk.getSkill('Magic');
|
|
const finalXp = currentMagic?.experience ?? 0;
|
|
const finalLevel = currentMagic?.baseLevel ?? 1;
|
|
const xpGained = finalXp - stats.startXp;
|
|
const elapsed = (Date.now() - stats.startTime) / 1000;
|
|
const xpPerHour = elapsed > 0 ? Math.round((xpGained / elapsed) * 3600) : 0;
|
|
|
|
console.log('=== Final Results ===');
|
|
console.log(`Magic: Level ${stats.startLevel} -> ${finalLevel}`);
|
|
console.log(`XP: ${stats.startXp} -> ${finalXp} (+${xpGained})`);
|
|
console.log(`Casts: ${stats.casts} (Hits: ${stats.hits}, Splashes: ${stats.casts - stats.hits})`);
|
|
console.log(`Duration: ${elapsed.toFixed(1)}s`);
|
|
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
|
|
|
|
// Log remaining runes
|
|
const runes = getRuneCounts(ctx);
|
|
console.log(`Remaining runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
|
|
}
|
|
|
|
// Main script
|
|
async function main() {
|
|
// Create fresh account
|
|
const username = `mg${Math.random().toString(36).slice(2, 7)}`;
|
|
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
|
|
|
|
// Launch browser
|
|
const session = await launchBotWithSDK(username, { usePuppeteer: true });
|
|
|
|
try {
|
|
await runScript(async (ctx) => {
|
|
try {
|
|
await magicTrainingLoop(ctx);
|
|
} finally {
|
|
// Log final state
|
|
const state = ctx.sdk.getState();
|
|
if (state) {
|
|
const magic = state.skills.find(s => s.name === 'Magic');
|
|
console.log(`Final Magic: Level ${magic?.baseLevel ?? '?'}, XP ${magic?.experience ?? '?'}`);
|
|
}
|
|
}
|
|
}, {
|
|
connection: { bot: session.bot, sdk: session.sdk },
|
|
timeout: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
} finally {
|
|
await session.cleanup();
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|