All scripts

AIO Thiever

by SolemnVanguard ·Thieving

RSCFalador

All in one thiever

0

Installed

1.1

Version

25 Aug 2026

Updated

About this script

AIO Pickpocketer: pick a target NPC type from the settings dropdown, Man, Farmer, Warrior, Workman, Rogue, Guard, Knight, Watchman, Paladin, Gnome Local, Gnome Guard, Gnome Trainer, Gnome Child, Gnome Waiter, Blurberry Barman, or Hero, and it pickpockets only that type, auto-flees if it ends up in combat, and halts with a warning on teleport or a suspicious health drop.

Script still in progress, open for suggestions

Builds

VersionKindPublishedSizeWhat changed
1.1 current text script 25 Aug 2026 11 KB First upload.

Source of 1.1

Published by the author. This is the current build, exactly as it will be delivered.

script      "AIO Pickpocketer"
author      "you"
version     "1.1"
description "Pickpockets a chosen NPC type for Thieving xp and coins"
category    "Thieving"
every       600

setting ThievingTarget target = ThievingTarget.MAN "Which NPC type to pickpocket"
setting int searchRadius = 15 "How far to search for a pickpocketable target"
setting int healthDropHaltPercent = 15 "Halt if health drops this many percentage points in one pass (a failed pickpocket normally only stuns you - it shouldn't cost health at all, so any real drop means something is actually fighting back)"

enum ThievingTarget {
    MAN, FARMER, WARRIOR, WORKMAN, ROGUE, GUARD, KNIGHT, WATCHMAN,
    PALADIN, GNOME_LOCAL, GNOME_GUARD, GNOME_TRAINER, GNOME_CHILD,
    GNOME_WAITER, BLURBERRY_BARMAN, HERO
}

# All-in-one pickpocketer - pick the target type from the dropdown above and
# it goes after that one. Every name in the npc cache is reused by several
# different things (an unrelated higher-level lookalike, a non-interactive
# copy with no pickpocket option at all, a same-named unique character,
# etc) - exactly like the original "Man" script had to special-case ids
# 11/72/318 and reject id 24, EVERY target below is pinned to its own
# hand-verified list of pickpocketable ids, cross-checked against the
# bundled npc data one by one:
#
#   Man              11, 72, 318
#   Farmer           63, 319
#   Warrior          86, 159, 320
#   Workman          722, 738
#   Rogue            342
#   Guard            65, 100, 321, 503, 524, 526, 527
#   Knight           322
#   Watchman         574   (the only pickpocketable Watchman in the data is
#                           "Yanille Watchman" - that's what this searches for)
#   Paladin          323, 632, 633
#   Gnome Local      592, 593
#   Gnome Guard      582
#   Gnome Trainer    576, 578, 579
#   Gnome Child      583, 585, 586, 591
#   Gnome Waiter     581
#   Blurberry Barman 580   (the game data spells it "Blurberry", not
#                           "Blueberry" - same npc from Blurberry's Bar)
#   Hero             324
#
# Coins are the only normal drop across all of these, and coins stack - so
# unlike a mining/fishing script there is no bank leg here at all: the
# inventory never actually fills up from this. invIsFull() is still
# watched for as a pure safety net in case that assumption is ever wrong
# for a particular target, and the script just pauses (rather than
# banking somewhere it doesn't know) if it ever fires.
#
# Pickpocketing uses the same batch system mining/fishing do - the script
# waits for batchActive() to clear before clicking again, and briefly
# waits for it to turn true right after a click (so the very next pass
# doesn't race ahead and double-click before the engine's own batch state
# has caught up).
#
# There's no confirmed chat-message text for a successful vs a failed/
# stunned pickpocket attempt, so this doesn't try to react to one beyond
# the batch window - it just clicks pickpocket once the batch is clear,
# and tries again. Turn on "Debug: print every chat line" below if you
# want to find the exact wording so a future version can react to it
# directly.
#
# Safety nets: halts (red warning) if your tile jumps more than 20 tiles
# between passes (teleport), or if health drops more than
# healthDropHaltPercent in a single pass (a failed pickpocket shouldn't
# cost health at all - a real drop means something is actually fighting
# back).
#
# If the target (or anything else) turns hostile and inCombat() goes
# true, the script does not keep clicking pickpocket into a fight - there
# is no built-in flee/retreat command in this engine, so it hand-rolls
# one: it finds the attacker with nearestNpc(inCombat: true) (combat
# locks fighters onto adjacent tiles, so it will be right next to you),
# works out which compass-ish direction is away from it relative to your
# own tile, and walkTo()s a spot several tiles further in that direction.
# This repeats every pass for as long as inCombat() stays true, then
# normal pickpocketing resumes automatically once it clears.

var lastTile = tile(0, 0)
var haveLastTile = false
var teleported = false
var pendingJump = false

var lastHealthPercent = -1
var healthHalted = false

var attempts = 0

fun teleportCheck() {
    let now = myTile()
    if haveLastTile {
        let jumped = distance(lastTile, now)
        if jumped > 20 {
            if pendingJump {
                print("Jumped", jumped, "tiles, confirmed on second check - halting")
                teleported = true
                lastTile = now
            } else {
                print("Jumped", jumped, "tiles - unconfirmed, checking again next pass")
                pendingJump = true
            }
        } else {
            pendingJump = false
            lastTile = now
        }
    } else {
        lastTile = now
        haveLastTile = true
    }
}

fun healthDropCheck() {
    if not skillsAvailable() { return }
    let hp = healthPercent()
    if lastHealthPercent >= 0 and hp < lastHealthPercent - healthDropHaltPercent {
        print("Health dropped from", lastHealthPercent, "to", hp, "in one pass - halting")
        healthHalted = true
    }
    lastHealthPercent = hp
}

# The exact npc-cache name to search on for the currently selected target.
# Matching is case-insensitive (per nearestNpc/npcsByDistance), so casing
# here doesn't have to be exact - only the words do.
fun targetSearchName(ThievingTarget t) -> string {
    if t == ThievingTarget.MAN { return "Man" }
    if t == ThievingTarget.FARMER { return "Farmer" }
    if t == ThievingTarget.WARRIOR { return "Warrior" }
    if t == ThievingTarget.WORKMAN { return "Workman" }
    if t == ThievingTarget.ROGUE { return "Rogue" }
    if t == ThievingTarget.GUARD { return "Guard" }
    if t == ThievingTarget.KNIGHT { return "Knight" }
    if t == ThievingTarget.WATCHMAN { return "Yanille Watchman" }
    if t == ThievingTarget.PALADIN { return "Paladin" }
    if t == ThievingTarget.GNOME_LOCAL { return "Gnome Local" }
    if t == ThievingTarget.GNOME_GUARD { return "Gnome Guard" }
    if t == ThievingTarget.GNOME_TRAINER { return "Gnome Trainer" }
    if t == ThievingTarget.GNOME_CHILD { return "Gnome Child" }
    if t == ThievingTarget.GNOME_WAITER { return "Gnome Waiter" }
    if t == ThievingTarget.BLURBERRY_BARMAN { return "Blurberry Barman" }
    return "Hero"
}

# The hand-verified id whitelist for the currently selected target - see
# the table in the header comment for where every one of these came from.
fun idMatchesTarget(int id, ThievingTarget t) -> bool {
    if t == ThievingTarget.MAN { return id == 11 or id == 72 or id == 318 }
    if t == ThievingTarget.FARMER { return id == 63 or id == 319 }
    if t == ThievingTarget.WARRIOR { return id == 86 or id == 159 or id == 320 }
    if t == ThievingTarget.WORKMAN { return id == 722 or id == 738 }
    if t == ThievingTarget.ROGUE { return id == 342 }
    if t == ThievingTarget.GUARD {
        if id == 65 or id == 100 or id == 321 or id == 503 { return true }
        return id == 524 or id == 526 or id == 527
    }
    if t == ThievingTarget.KNIGHT { return id == 322 }
    if t == ThievingTarget.WATCHMAN { return id == 574 }
    if t == ThievingTarget.PALADIN { return id == 323 or id == 632 or id == 633 }
    if t == ThievingTarget.GNOME_LOCAL { return id == 592 or id == 593 }
    if t == ThievingTarget.GNOME_GUARD { return id == 582 }
    if t == ThievingTarget.GNOME_TRAINER { return id == 576 or id == 578 or id == 579 }
    if t == ThievingTarget.GNOME_CHILD {
        return id == 583 or id == 585 or id == 586 or id == 591
    }
    if t == ThievingTarget.GNOME_WAITER { return id == 581 }
    if t == ThievingTarget.BLURBERRY_BARMAN { return id == 580 }
    return id == 324   # Hero
}

fun isPickpocketable(npc candidate) -> bool {
    if not idMatchesTarget(candidate.id, target) { return false }
    if candidate.inCombat or candidate.recentlyInCombat { return false }
    return true
}

fun pickTarget() -> npc? {
    let searchName = targetSearchName(target)
    for candidate in npcsByDistance(name: searchName, within: searchRadius) {
        if isPickpocketable(candidate) { return candidate }
    }
    return none
}

fun signOf(int v) -> int {
    if v > 0 { return 1 }
    if v < 0 { return -1 }
    return 0
}

# A tile several squares beyond "from", in whichever direction is away
# from "threat". Falls back to due-east if the two tiles are exactly on
# top of each other (signOf would otherwise give 0,0 and go nowhere).
fun fleeTileAwayFrom(tile from, tile threat) -> tile {
    let dy = signOf(from.y - threat.y)
    let dxRaw = signOf(from.x - threat.x)
    let dx = fleeXFallback(dxRaw, dy)
    return translate(from, dx * 6, dy * 6)
}

# signOf(0,0) would send both dx and dy to 0 (go nowhere) - fall back to
# due-east in that one case only.
fun fleeXFallback(int dxRaw, int dy) -> int {
    if dxRaw == 0 and dy == 0 { return 1 }
    return dxRaw
}

on message(line) {
    if uiToggled("debugChat") { print("MSG:", line) }
}

on start {
    if not skillsAvailable() {
        print("Skill hooks not ready yet - health/level reads may be stale")
    }

    uiToggle("debugChat", "Debug: print every chat line", false)
}

state thieving {
    teleportCheck()
    if teleported { return 999999 }
    healthDropCheck()
    if healthHalted { return 999999 }

    if invIsFull() {
        print("Inventory is full (shouldn't normally happen from just coins) - pausing")
        return 3000
    }

    if inCombat() {
        let threat = nearestNpc(within: 3, inCombat: true)
        if threat != none {
            let away = fleeTileAwayFrom(myTile(), threat.tile)
            print("In combat with", threat.name, "- fleeing toward", away.x, away.y)
            walkTo(away)
        } else {
            print("In combat but can't spot the attacker - waiting")
        }
        return 700
    }

    if batchActive() { return 300 }

    let mark = pickTarget()
    if mark == none {
        print("No pickpocketable", targetSearchName(target), "nearby - waiting")
        return 1200
    }

    if reach(mark) == "DRAWN" {
        if interact(mark, "pickpocket") {
            attempts = attempts + 1
            wait until batchActive() timeout 1200 else { }
        }
    }
    return random(600, 900)
}

on render {
    if teleported or healthHalted {
        let flashOn = wrap(nowMillis(), 0, 999) < 500
        let bg = "#cc0000ee"
        if not flashOn { bg = "#660000ee" }
        paintRoundFill(4, 300, 220, 60, 12, bg)
        if teleported {
            paintTextCenteredSized(114, 322, "TELEPORTED", "white", 16)
        } else {
            paintTextCenteredSized(114, 322, "HEALTH DROP", "white", 16)
        }
        paintTextCenteredSized(114, 344, "SCRIPT STOPPED", "white", 12)
    }
}