Greasy Fork is available in English.
HUD de pickpocket autónomo — aparece apenas na página de crimes/pickpocketing. Seleção inteligente de alvos, logs em tempo real, nerve display, anti-padrão.
// ==UserScript==
// @name Torn — Pickpocket HUD
// @namespace https://torn.com/
// @version 1.0
// @description HUD de pickpocket autónomo — aparece apenas na página de crimes/pickpocketing. Seleção inteligente de alvos, logs em tempo real, nerve display, anti-padrão.
// @author TeuNome
// @match https://www.torn.com/*
// @icon https://www.torn.com/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* ══════════════════════════════════════════════════════
SÓ ACTIVA NA PÁGINA DE PICKPOCKET
══════════════════════════════════════════════════════ */
function isPickpocketPage() {
return location.href.includes("sid=crimes") && location.hash.includes("pickpocketing");
}
function isCrimesPage() {
return location.href.includes("sid=crimes");
}
// Aguarda hash change caso o utilizador navegue directamente para crimes
if (!isPickpocketPage() && !isCrimesPage()) return;
/* ══════════════════════════════════════════════════════
CONFIGURAÇÃO
══════════════════════════════════════════════════════ */
const API_KEY = "merda"; // ← substitui pelo teu
const PICKPOCKET_URL = "https://www.torn.com/page.php?sid=crimes#/pickpocketing";
const WAIT_POLL_MS = 3000;
const SCAN_LOG_INTERVAL = 2;
const REFRESH_MIN_SEC = 3 * 60;
const REFRESH_MAX_SEC = 5 * 60;
const STATE_TTL_MS = 30000;
/* ── localStorage keys ── */
const LOGS_KEY = "tph_logs";
const STATS_KEY = "tph_stats";
const DROP_STATS_KEY = "tph_dropStats";
const PAUSED_KEY = "tph_paused";
const SESSION_MODE_KEY = "tph_sessionMode";
const NERVE_COUNTER_KEY = "tph_nerveCounter";
const TARGET_FILTERS_KEY= "tph_targetFilters";
const VICTIM_CFG_KEY = "tph_victimCfg";
const SCORE_MODE_KEY = "tph_scoreMode";
/* ══════════════════════════════════════════════════════
BASE DE DADOS DE ALVOS
══════════════════════════════════════════════════════ */
const VICTIM_DB = [
{
id: "drunk_woman",
label: "Drunk Woman",
emoji: "🍹",
priority: 1,
difficulty: 1,
cashMin: 274,
cashMax: 3380,
timerSec: 42,
statuses: ["stumbling", "distracted"],
goodStatus: ["stumbling", "distracted"],
requireBuild: "heavyset",
requireRazor: true,
unique: "2x Bottle of Pumpkin Brew",
uniqueVal: 800,
riskHosp: true
},
{
id: "drunk_man",
label: "Drunk Man",
emoji: "🍺",
priority: 1,
difficulty: 1,
cashMin: 216,
cashMax: 3769,
timerSec: 42,
statuses: ["stumbling", "distracted"],
goodStatus: ["stumbling", "distracted"],
requireRazor: true,
unique: "6x Bottle of Beer",
uniqueVal: 900
},
{
id: "businessman",
label: "Businessman",
emoji: "💼",
priority: 2,
difficulty: 2,
cashMin: 1380,
cashMax: 9600,
timerSec: 25,
statuses: ["walking", "on phone"],
goodStatus: ["on phone"],
unique: "2x Speed",
uniqueVal: 1200,
riskJail: true
},
{
id: "businesswoman",
label: "Businesswoman",
emoji: "👠",
priority: 2,
difficulty: 2,
cashMin: 1100,
cashMax: 6000,
timerSec: 28,
statuses: ["walking", "on phone"],
goodStatus: ["on phone"],
unique: "Glasses",
uniqueVal: 500,
riskJail: true
},
{
id: "classy_lady",
label: "Classy Lady",
emoji: "💎",
priority: 2,
difficulty: 3,
cashMin: 1020,
cashMax: 79400,
timerSec: 25,
statuses: ["walking", "on phone"],
goodStatus: ["on phone"],
unique: "High Heels",
uniqueVal: 2000,
riskJail: true
},
{
id: "cyclist",
label: "Cyclist",
emoji: "🚴",
priority: 2,
difficulty: 3,
cashMin: 720,
cashMax: 10400,
timerSec: 10,
statuses: ["cycling"],
goodStatus: [],
unique: "Mountain Bike",
uniqueVal: 5000,
riskHosp: true,
riskJail: true
},
{
id: "elderly_man",
label: "Elderly Man",
emoji: "👴",
priority: 1,
difficulty: 1,
cashMin: 460,
cashMax: 5000,
timerSec: 55,
statuses: ["walking"],
goodStatus: [],
unique: "Bag of Tootsie Rolls",
uniqueVal: 300
},
{
id: "elderly_woman",
label: "Elderly Woman",
emoji: "👵",
priority: 1,
difficulty: 1,
cashMin: 460,
cashMax: 4900,
timerSec: 55,
statuses: ["walking"],
goodStatus: [],
unique: "Opium",
uniqueVal: 800,
riskJail: true
},
{
id: "gang_member",
label: "Gang Member",
emoji: "🔫",
priority: 2,
difficulty: 3,
cashMin: 1660,
cashMax: 7000,
timerSec: 70,
statuses: ["loitering"],
goodStatus: ["loitering"],
requireRazor: true,
unique: "Spray Cans / Wire Cutters",
uniqueVal: 1500,
riskJail: true
},
{
id: "homeless",
label: "Homeless Person",
emoji: "🧍",
priority: 1,
difficulty: 1,
cashMin: 70,
cashMax: 2921,
timerSec: 120,
statuses: ["loitering"],
goodStatus: ["loitering"],
unique: "4x Morphine",
uniqueVal: 1000,
riskJail: true
},
{
id: "jogger",
label: "Jogger",
emoji: "🏃",
priority: 2,
difficulty: 2,
cashMin: 560,
cashMax: 4550,
timerSec: 20,
statuses: ["walking", "jogging"],
goodStatus: ["walking"],
requireRazor: true,
unique: "Crocozade",
uniqueVal: 400,
riskJail: true
},
{
id: "junkie",
label: "Junkie",
emoji: "💉",
priority: 1,
difficulty: 1,
cashMin: 202,
cashMax: 4800,
timerSec: 60,
statuses: ["stumbling", "loitering"],
goodStatus: ["stumbling"],
unique: "Drug bundle",
uniqueVal: 1500,
riskJail: true
},
{
id: "laborer",
label: "Laborer",
emoji: "🪓",
priority: 2,
difficulty: 2,
cashMin: 560,
cashMax: 4700,
timerSec: 40,
statuses: ["walking", "distracted", "on phone"],
goodStatus: ["distracted", "on phone"],
unique: "Megaphone",
uniqueVal: 700,
riskJail: true
},
{
id: "mobster",
label: "Mobster",
emoji: "🕴️",
priority: 3,
difficulty: 4,
cashMin: 10000,
cashMax: 700000,
timerSec: 30,
statuses: ["walking"],
goodStatus: [],
requireRazor: true,
unique: "Huge Cash Drop",
uniqueVal: 600000,
riskJound: true
},
{
id: "police",
label: "Police Officer",
emoji: "👮",
priority: 3,
difficulty: 4,
cashMin: 0,
cashMax: 2000,
timerSec: 25,
statuses: ["walking", "running"],
goodStatus: ["walking"],
unique: "Police Badge",
uniqueVal: 2000,
riskJail: true
},
{
id: "postal_worker",
label: "Postal Worker",
emoji: "📦",
priority: 2,
difficulty: 2,
cashMin: 746,
cashMax: 5792,
timerSec: 25,
statuses: ["walking", "distracted"],
goodStatus: ["distracted"],
unique: "Lottery Voucher",
uniqueVal: 1200,
riskJail: true
},
{
id: "rich_kid",
label: "Rich Kid",
emoji: "🧑🎧",
priority: 2,
difficulty: 3,
cashMin: 1200,
cashMax: 8000,
timerSec: 32,
statuses: ["walking", "on phone", "listening to music"],
goodStatus: ["listening to music"],
requireRazor: true,
unique: "Six Pack Alcohol",
uniqueVal: 900,
riskJail: true
},
{
id: "sex_worker",
label: "Sex Worker",
emoji: "💋",
priority: 2,
difficulty: 3,
cashMin: 530,
cashMax: 5600,
timerSec: 80,
statuses: ["distracted", "on phone", "soliciting"],
goodStatus: ["soliciting"],
unique: "Thong",
uniqueVal: 400,
riskJail: true
},
{
id: "student",
label: "Student",
emoji: "🎓",
priority: 2,
difficulty: 2,
cashMin: 312,
cashMax: 3631,
timerSec: 25,
statuses: ["walking", "running", "on phone", "listening to music"],
goodStatus: ["on phone", "listening to music"],
requireRazor: true,
unique: "Tech bundle",
uniqueVal: 1500,
riskJail: true
},
{
id: "thug",
label: "Thug",
emoji: "👊",
priority: 2,
difficulty: 2,
cashMin: 800,
cashMax: 6000,
timerSec: 20,
statuses: ["walking", "running"],
goodStatus: ["walking"],
unique: "Weapon bundle",
uniqueVal: 1200,
riskJail: true
},
{
id: "young_man",
label: "Young Man",
emoji: "🧑",
priority: 1,
difficulty: 1,
cashMin: 550,
cashMax: 3970,
timerSec: 25,
statuses: ["walking", "on phone", "listening to music"],
goodStatus: ["on phone", "listening to music"],
unique: "Ketamine",
uniqueVal: 600,
riskJail: true
},
{
id: "young_woman",
label: "Young Woman",
emoji: "👩",
priority: 1,
difficulty: 1,
cashMin: 830,
cashMax: 4960,
timerSec: 25,
statuses: ["walking", "on phone", "listening to music", "distracted"],
goodStatus: ["distracted", "on phone"],
unique: "Ecstasy",
uniqueVal: 700,
riskJail: true
}
];
const ALL_STATUSES = [
{ id: "stumbling", label: "Stumbling", emoji: "🥴", mult: 1.5 },
{ id: "distracted", label: "Distracted", emoji: "😵", mult: 1.3 },
{ id: "on phone", label: "On Phone", emoji: "📱", mult: 1.2 },
{ id: "listening to music", label: "Listening to Music", emoji: "🎧", mult: 1.2 },
{ id: "talking", label: "Talking", emoji: "💬", mult: 1.2 },
{ id: "loitering", label: "Loitering", emoji: "🧍", mult: 1.2 },
{ id: "soliciting", label: "Soliciting", emoji: "🤑", mult: 1.3 },
{ id: "walking", label: "Walking", emoji: "🚶", mult: 1.0 },
{ id: "jogging", label: "Jogging", emoji: "🏃", mult: 0.7 },
{ id: "running", label: "Running", emoji: "💨", mult: 0.4 },
{ id: "cycling", label: "Cycling", emoji: "🚴", mult: 0.5 },
{ id: "patrolling", label: "Patrolling", emoji: "👮", mult: 0.9 },
];
const ALL_BUILDS = [
{ id: "skinny", label: "Skinny", emoji: "🦴" },
{ id: "average", label: "Average", emoji: "🧍" },
{ id: "heavyset", label: "Heavyset", emoji: "🪨" },
{ id: "muscular", label: "Muscular", emoji: "💪" },
{ id: "overweight", label: "Overweight", emoji: "🫃" },
];
const VICTIM_DROPS = {
drunk_woman: [
{ id: "candy_kisses", name: "Bag of Candy Kisses", emoji: "🍬", heavysetOnly: true },
{ id: "tootsie_rolls", name: "Bag of Tootsie Rolls", emoji: "🍫", heavysetOnly: true },
{ id: "lollipop", name: "Lollipop", emoji: "🍭", heavysetOnly: true },
{ id: "pumpkin_brew", name: "Bottle of Pumpkin Brew", emoji: "🎃", unique: true },
{ id: "cash", name: "Cash", emoji: "💵" },
],
drunk_man: [
{ id: "beer", name: "Bottle of Beer", emoji: "🍺" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
businessman: [
{ id: "speed", name: "Speed", emoji: "💊" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
businesswoman: [
{ id: "glasses", name: "Glasses", emoji: "👓" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
classy_lady: [
{ id: "heels", name: "High Heels", emoji: "👠" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
cyclist: [
{ id: "bike", name: "Mountain Bike", emoji: "🚴" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
homeless: [
{ id: "morphine", name: "Morphine", emoji: "💉" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
student: [
{ id: "tech", name: "Tech bundle", emoji: "📱" },
{ id: "cash", name: "Cash", emoji: "💵" },
],
};
const STATUS_MULT = {
"stumbling": 1.5, "distracted": 1.3, "soliciting": 1.3,
"loitering": 1.2, "on phone": 1.2, "listening to music": 1.2, "talking": 1.2,
"walking": 1.0, "patrolling": 0.9, "jogging": 0.7, "cycling": 0.5, "running": 0.4,
};
/* ══════════════════════════════════════════════════════
HELPERS
══════════════════════════════════════════════════════ */
function randBetween(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function formatTime(sec) {
sec = Math.max(0, Math.round(sec));
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
}
function pad(n) { return String(n).padStart(2, "0"); }
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
function waitFor(fn, maxMs = 12000) {
return new Promise((res, rej) => {
const t0 = Date.now();
const iv = setInterval(() => {
const el = fn();
if (el) { clearInterval(iv); res(el); }
else if (Date.now() - t0 >= maxMs) { clearInterval(iv); rej(`Timeout ${maxMs}ms`); }
}, 300);
});
}
/* ══════════════════════════════════════════════════════
ESTADO: PAUSA, MODO SESSÃO, MODO SCORE
══════════════════════════════════════════════════════ */
function isPaused() { return localStorage.getItem(PAUSED_KEY) === "1"; }
function setPaused(v) { localStorage.setItem(PAUSED_KEY, v ? "1" : "0"); }
function getSessionMode() { return localStorage.getItem(SESSION_MODE_KEY) || "wait"; }
function setSessionMode(m) { localStorage.setItem(SESSION_MODE_KEY, m); }
function getScoreMode() { return localStorage.getItem(SCORE_MODE_KEY) || "auto"; }
function setScoreMode(m) { localStorage.setItem(SCORE_MODE_KEY, m); }
/* ══════════════════════════════════════════════════════
FILTROS DE ALVO (status + build)
══════════════════════════════════════════════════════ */
function buildDefaultFilters() {
const filters = {};
VICTIM_DB.forEach(v => {
const statuses = {};
ALL_STATUSES.forEach(s => {
statuses[s.id] = v.statuses.includes(s.id) && (v.goodStatus.length === 0 || v.goodStatus.includes(s.id));
});
const builds = {};
ALL_BUILDS.forEach(b => { builds[b.id] = v.requireBuild ? b.id === v.requireBuild : true; });
filters[v.id] = { statuses, builds };
});
return filters;
}
function getTargetFilters() {
try {
const s = JSON.parse(localStorage.getItem(TARGET_FILTERS_KEY));
if (s && typeof s === "object") return s;
} catch {}
return buildDefaultFilters();
}
function saveTargetFilters(f) { localStorage.setItem(TARGET_FILTERS_KEY, JSON.stringify(f)); }
function isStatusAllowed(victimId, status) {
return getTargetFilters()[victimId]?.statuses?.[status.toLowerCase()] === true;
}
function isBuildAllowed(victimId, build) {
return getTargetFilters()[victimId]?.builds?.[build.toLowerCase()] === true;
}
function toggleStatusFilter(victimId, statusId) {
const f = getTargetFilters();
if (!f[victimId]) f[victimId] = buildDefaultFilters()[victimId];
f[victimId].statuses[statusId] = !f[victimId].statuses[statusId];
saveTargetFilters(f);
}
function toggleBuildFilter(victimId, buildId) {
const f = getTargetFilters();
if (!f[victimId]) f[victimId] = buildDefaultFilters()[victimId];
f[victimId].builds[buildId] = !f[victimId].builds[buildId];
saveTargetFilters(f);
}
/* ══════════════════════════════════════════════════════
LOGS
══════════════════════════════════════════════════════ */
const CAT_CLR = {
crime: "#5c2db0", scan: "#1a4a3a", drop: "#1a5a2a",
system: "#2a2a4a", nerve: "#3a1a6a", warn: "#6a3a00"
};
function getLogs() { try { return JSON.parse(localStorage.getItem(LOGS_KEY)) || []; } catch { return []; } }
function addLog(category, msg) {
const logs = getLogs();
const ts = new Date().toLocaleTimeString("pt-PT", { hour:"2-digit", minute:"2-digit", second:"2-digit" });
logs.unshift({ ts, category, msg });
if (logs.length > 300) logs.length = 300;
localStorage.setItem(LOGS_KEY, JSON.stringify(logs));
renderLogs();
}
function clearLogs() { localStorage.removeItem(LOGS_KEY); renderLogs(); }
/* ══════════════════════════════════════════════════════
ESTATÍSTICAS
══════════════════════════════════════════════════════ */
function getStats() {
try { return JSON.parse(localStorage.getItem(STATS_KEY)) || defaultStats(); } catch { return defaultStats(); }
}
function defaultStats() {
return { pickpockets: { count: 0, nerveSpent: 0, successCount: 0, byVictim: {} } };
}
function saveStats(s) { localStorage.setItem(STATS_KEY, JSON.stringify(s)); }
function recordPickpocket(victimId, nerveSpent, success) {
const s = getStats();
s.pickpockets.count++;
s.pickpockets.nerveSpent += nerveSpent;
if (success) s.pickpockets.successCount++;
if (!s.pickpockets.byVictim[victimId]) s.pickpockets.byVictim[victimId] = { count: 0, success: 0 };
s.pickpockets.byVictim[victimId].count++;
if (success) s.pickpockets.byVictim[victimId].success++;
saveStats(s);
if (s.pickpockets.count % 5 === 0) {
addLog("crime", `🦹 #${s.pickpockets.count} crimes · ${s.pickpockets.nerveSpent} nerve gasto`);
}
updateStatsDisplay();
}
/* ══════════════════════════════════════════════════════
DROPS
══════════════════════════════════════════════════════ */
function getDropStats() { try { return JSON.parse(localStorage.getItem(DROP_STATS_KEY)) || {}; } catch { return {}; } }
function saveDropStats(d) { localStorage.setItem(DROP_STATS_KEY, JSON.stringify(d)); }
function recordDrop(victimId, dropId) {
const d = getDropStats();
if (!d[victimId]) d[victimId] = {};
d[victimId][dropId] = (d[victimId][dropId] || 0) + 1;
saveDropStats(d);
const drop = VICTIM_DROPS[victimId]?.find(x => x.id === dropId);
if (drop) {
addLog("drop", `${drop.emoji} ${victimId} → ${drop.name} (x${d[victimId][dropId]})`);
}
}
function parseOutcomeForDrop(modalEl, victimId) {
if (!modalEl || !victimId) return null;
const text = modalEl.textContent.toLowerCase();
const drops = VICTIM_DROPS[victimId] || [];
for (const drop of drops) {
if (text.includes(drop.name.toLowerCase())) {
return drop.id;
}
}
if (/\$[\d,]+/.test(modalEl.textContent) && !text.includes("failed")) {
return "cash";
}
return null;
}
/* ══════════════════════════════════════════════════════
NERVE — LEITURA DO DOM + API
══════════════════════════════════════════════════════ */
let currentNerve = 0;
let currentNerveMax = 30;
let apiLoaded = false;
function readNerveFromDOM() {
const stats = document.querySelectorAll('[class*="bar-stats"]');
for (const stat of stats) {
const nameEl = stat.querySelector('[class*="bar-name"]');
const valEl = stat.querySelector('[class*="bar-value"]');
if (nameEl && valEl && nameEl.textContent.trim().toLowerCase().startsWith("nerve")) {
const m = valEl.textContent.trim().match(/(\d+)\/(\d+)/);
if (m) { currentNerve = parseInt(m[1], 10); currentNerveMax = parseInt(m[2], 10); return true; }
}
}
const names = document.querySelectorAll('[class*="bar-name"]');
for (const nameEl of names) {
if (nameEl.textContent.trim().toLowerCase().startsWith("nerve")) {
const parent = nameEl.parentElement;
const valEl = parent && parent.querySelector('[class*="bar-value"]');
if (valEl) {
const m = valEl.textContent.trim().match(/(\d+)\/(\d+)/);
if (m) { currentNerve = parseInt(m[1], 10); currentNerveMax = parseInt(m[2], 10); return true; }
}
}
}
return false;
}
async function fetchNerveFromAPI() {
try {
const res = await fetch(`https://api.torn.com/user/?selections=bars&key=${API_KEY}`);
const data = await res.json();
if (data?.nerve) {
currentNerve = data.nerve.current;
currentNerveMax = data.nerve.maximum;
apiLoaded = true;
}
} catch (e) { console.error("[TPH] API:", e); }
}
/* ══════════════════════════════════════════════════════
PARSE DE CARDS DE VÍTIMAS
══════════════════════════════════════════════════════ */
function parseVictimCards() {
const wrappers = document.querySelectorAll('[class*="crimeOptionWrapper"]');
const parsed = [];
for (const wrap of wrappers) {
const titleDiv = wrap.querySelector('[class*="titleAndProps"] [style*="color"]');
const rawTitle = (titleDiv ? titleDiv.textContent.trim() : "").replace(/\s*\(.*?\)\s*$/, "").trim();
const activityEl = wrap.querySelector('[class*="activity___"]');
let rawStatus = "";
if (activityEl) {
for (const node of activityEl.childNodes) {
if (node.nodeType === Node.TEXT_NODE) { const t = node.textContent.trim(); if (t) { rawStatus = t; break; } }
}
}
const buildEl = wrap.querySelector('[class*="physicalProps___"] [aria-hidden="true"]');
const rawBuild = buildEl ? buildEl.textContent.trim() : "";
const clockEl = wrap.querySelector('[class*="clock___"]');
const clockSec = clockEl ? (parseInt(clockEl.textContent) || 99) : 99;
const btn = wrap.querySelector('button[class*="commitButton"]');
if (!btn) continue;
const matchedVictim =
VICTIM_DB.find(v => rawTitle.toLowerCase() === v.label.toLowerCase()) ||
VICTIM_DB.find(v => rawTitle.toLowerCase().includes(v.label.toLowerCase()) || v.label.toLowerCase().includes(rawTitle.toLowerCase()));
parsed.push({ btn, rawTitle, rawStatus: rawStatus.toLowerCase(), rawBuild, clockSec, victimId: matchedVictim?.id || null, victim: matchedVictim || null });
}
return parsed;
}
function cardMeetsRequirements(card) {
const v = card.victim;
if (!v) return false;
if (!isStatusAllowed(v.id, card.rawStatus)) return false;
const buildWord = card.rawBuild.split(/\s+/)[0].toLowerCase();
if (buildWord && !isBuildAllowed(v.id, buildWord)) return false;
return true;
}
/* ══════════════════════════════════════════════════════
SCAN LOG DE VÍTIMAS VISÍVEIS
══════════════════════════════════════════════════════ */
function logVisibleVictims(cards) {
if (!cards.length) { addLog("scan", "👁 Nenhum alvo visível no ecrã"); return; }
cards.forEach(c => {
const meetsReq = c.victim ? cardMeetsRequirements(c) : false;
const icon = c.victim ? c.victim.emoji : "❓";
let reason;
if (!c.victim) reason = "⬜ fora da lista";
else if (!meetsReq) {
const needStatus = !isStatusAllowed(c.victimId, c.rawStatus);
const buildWord = c.rawBuild.split(/\s+/)[0].toLowerCase();
const needBuild = buildWord && !isBuildAllowed(c.victimId, buildWord);
reason = needStatus ? `⏳ status "${c.rawStatus}" não permitido`
: needBuild ? `⏳ build "${buildWord}" não permitido`
: `⏳ req. não cumprido`;
} else reason = "✅ CANDIDATO";
const mult = STATUS_MULT[c.rawStatus] || 1.0;
const multClr = mult >= 1.2 ? "#00e676" : mult >= 1.0 ? "#ffd966" : "#ff6666";
addLog("scan", `${icon} ${c.rawTitle} · <span style="color:${multClr};">${c.rawStatus || "?"}</span> · ${c.rawBuild.split(" ")[0] || "?"} · ⏱${c.clockSec}s → ${reason}`);
});
}
/* ══════════════════════════════════════════════════════
SELECÇÃO DE MELHOR ALVO
══════════════════════════════════════════════════════ */
let nervePickCounter = parseInt(localStorage.getItem(NERVE_COUNTER_KEY) || "0", 10);
function incNerveCounter() { nervePickCounter++; localStorage.setItem(NERVE_COUNTER_KEY, nervePickCounter); return nervePickCounter; }
function resetNerveCounter() { nervePickCounter = 0; localStorage.setItem(NERVE_COUNTER_KEY, "0"); }
function selectBestVictim(doLogScan = false) {
const cards = parseVictimCards();
if (doLogScan) logVisibleVictims(cards);
if (!cards.length) return null;
// A cada 4 crimes → crime aleatório anti-padrão
if (nervePickCounter > 0 && nervePickCounter % 4 === 0) {
const pool = cards.filter(c => c.victim);
if (!pool.length) return null;
const pick = pool[Math.floor(Math.random() * pool.length)];
addLog("crime", `🎲 Aleatório (turno ${nervePickCounter}) → ${pick.rawTitle} · ${pick.rawStatus}`);
return pick;
}
// Selecção por prioridade + filtros
const sorted = VICTIM_DB.slice().sort((a, b) => (a.priority || 99) - (b.priority || 99));
for (const dbEntry of sorted) {
const candidates = cards.filter(c => c.victimId === dbEntry.id && cardMeetsRequirements(c));
if (candidates.length) {
candidates.sort((a, b) => b.clockSec - a.clockSec);
const pick = candidates[0];
addLog("crime", `🎯 Atacar: ${pick.rawTitle} · ${pick.rawStatus} · ⏱${pick.clockSec}s`);
return pick;
}
}
return null;
}
/* ══════════════════════════════════════════════════════
FECHAR MODAL DE RESULTADO + DETECTAR DROP
══════════════════════════════════════════════════════ */
async function closeOutcomeModal(victimId) {
const modalSel = '[class*="outcomeWrapper___"]';
const t0 = Date.now();
let modal = null;
while (Date.now() - t0 < 4000) {
const m = document.querySelector(modalSel);
if (m && m.children.length > 0) { modal = m; break; }
await delay(200);
}
await delay(300);
if (modal) {
const dropId = parseOutcomeForDrop(modal, victimId);
if (dropId) recordDrop(victimId, dropId);
}
const closeSelectors = [
'[class*="outcomeWrapper"] button', '[class*="outcome"] button',
'[class*="modal"] button[class*="close"]', '[class*="closeButton"]',
'[aria-label="Close"]', '[class*="continue"]',
];
for (const sel of closeSelectors) {
const btn = document.querySelector(sel);
if (btn) { btn.click(); await delay(300); return; }
}
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", keyCode: 27, bubbles: true }));
await delay(300);
const overlay = document.querySelector('[class*="overlay"], [class*="Overlay"]');
if (overlay) overlay.click();
await delay(300);
}
/* ══════════════════════════════════════════════════════
LOOP DE PICKPOCKET
══════════════════════════════════════════════════════ */
let pickpocketRunning = false;
async function runPickpocketLoop() {
if (pickpocketRunning) return;
if (!isPickpocketPage()) return;
pickpocketRunning = true;
const refreshAt = Date.now() + randBetween(REFRESH_MIN_SEC, REFRESH_MAX_SEC) * 1000;
const sessionStart = Date.now();
let sessionCrimes = 0;
let scanIter = 0;
let consecutiveErr = 0;
addLog("crime", `▶ Sessão iniciada — refresh em ~${Math.round((refreshAt - Date.now()) / 60000)}m · modo: ${getSessionMode()}`);
updateStatusBadge("▶ A correr", "#00e676");
try {
await delay(2000);
while (true) {
/* ── Pausa ── */
if (isPaused()) { updateStatusBadge("⏸ Pausa", "#ffd966"); await delay(1000); continue; }
/* ── Refresh periódico ── */
if (Date.now() >= refreshAt) {
const elapsed = Math.round((Date.now() - sessionStart) / 1000);
addLog("crime", `🔄 Refresh após ${formatTime(elapsed)} (${sessionCrimes} crimes) — a recarregar`);
pickpocketRunning = false;
location.href = PICKPOCKET_URL;
return;
}
/* ── Lê nerve ── */
readNerveFromDOM();
updateNerveDisplay();
const hasNerve = currentNerve >= 5;
/* ── Sem nerve ── */
if (!hasNerve) {
if (getSessionMode() === "leave") {
addLog("crime", `⚡ Nerve ${currentNerve}/5 — modo "sair" — sessão terminada`);
updateStatusBadge("💤 Nerve baixo", "#ff6666");
break;
}
updateStatusBadge(`⏳ Nerve ${currentNerve}/${currentNerveMax}`, "#ffd966");
if (scanIter % SCAN_LOG_INTERVAL === 0) {
const cards = parseVictimCards();
logVisibleVictims(cards);
}
scanIter++;
await delay(WAIT_POLL_MS);
continue;
}
/* ── Selecciona alvo ── */
const doLog = (scanIter % SCAN_LOG_INTERVAL === 0);
scanIter++;
const pick = selectBestVictim(doLog);
if (!pick) {
if (doLog) addLog("scan", `⏳ Sem alvos prioritários (nerve: ${currentNerve})`);
updateStatusBadge("🔍 A procurar alvo…", "#6c5ce7");
await delay(WAIT_POLL_MS);
continue;
}
/* ── Timer mínimo ── */
if (pick.clockSec < 3) {
addLog("scan", `⚠️ ${pick.rawTitle} a desaparecer (${pick.clockSec}s) — saltar`);
await delay(500);
continue;
}
/* ── Executa crime ── */
updateStatusBadge(`🦹 ${pick.rawTitle}`, "#5c2db0");
try {
incNerveCounter();
pick.btn.click();
sessionCrimes++;
consecutiveErr = 0;
recordPickpocket(pick.victimId || "unknown", 5, true);
await closeOutcomeModal(pick.victimId);
let waited = 0;
while (waited < 6000) {
await delay(500); waited += 500;
if (document.querySelector('button[class*="commitButton"]')) break;
}
} catch (crimeErr) {
consecutiveErr++;
addLog("warn", `⚠️ Erro no crime #${sessionCrimes}: ${crimeErr}`);
if (consecutiveErr >= 5) {
addLog("warn", "❌ 5 erros consecutivos — a recarregar");
pickpocketRunning = false;
location.href = PICKPOCKET_URL;
return;
}
await delay(3000);
}
}
} catch (err) {
addLog("warn", `❌ Erro fatal: ${err}`);
}
addLog("crime", `✅ Sessão terminada: ${sessionCrimes} crimes · ${sessionCrimes * 5} nerve gasto`);
updateStatusBadge("⏹ Parado", "#3a3a6a");
pickpocketRunning = false;
resetNerveCounter();
}
/* ══════════════════════════════════════════════════════
CONSTRUÇÃO DO HUD
══════════════════════════════════════════════════════ */
const box = document.createElement("div");
box.id = "tph-box";
box.style.cssText = [
"position:fixed;top:0;right:0;bottom:0;width:360px",
"background:rgba(6,6,12,0.98);color:#d0d0e8",
"z-index:999999;font-family:'Segoe UI',Arial,sans-serif",
"box-shadow:-6px 0 40px rgba(0,0,0,0.95)",
"display:flex;flex-direction:column;overflow:hidden",
"border-left:1px solid #1a1a2e;user-select:none",
"transition:transform .3s ease",
].join(";");
box.innerHTML = `
<!-- ── HEADER ── -->
<div style="background:#08081a;padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #1a1a2e;flex-shrink:0;">
<div>
<div style="font-size:11px;font-weight:800;letter-spacing:2.5px;color:#6c5ce7;">🦹 PICKPOCKET HUD</div>
<div id="tph-status-badge" style="font-size:10px;color:#3a3a6a;margin-top:2px;">iniciando…</div>
</div>
<div style="display:flex;gap:5px;align-items:center;">
<button id="tph-pause" style="background:#3a1a00;border:1px solid #7a3a00;color:#ffd966;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:10px;font-weight:800;">⏸</button>
<button id="tph-start" style="background:#0e3a0e;border:1px solid #1a7a1a;color:#00e676;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:10px;font-weight:800;">▶ START</button>
<button id="tph-collapse" style="background:none;border:1px solid #2a2a3e;color:#444;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px;">◀</button>
</div>
</div>
<!-- ── PAUSA BANNER ── -->
<div id="tph-pause-banner" style="display:none;background:#2a1000;border-bottom:2px solid #ffd966;padding:5px 12px;text-align:center;font-size:10px;font-weight:800;color:#ffd966;flex-shrink:0;">
⏸ EM PAUSA — podes navegar livremente
</div>
<!-- ── TABS ── -->
<div style="display:flex;background:#08081a;border-bottom:1px solid #1a1a2e;flex-shrink:0;">
<button class="tph-tab active" data-tab="nerve" style="flex:1;">NERVE</button>
<button class="tph-tab" data-tab="alvos" style="flex:1;">ALVOS</button>
<button class="tph-tab" data-tab="drops" style="flex:1;">DROPS</button>
<button class="tph-tab" data-tab="stats" style="flex:1;">STATS</button>
<button class="tph-tab" data-tab="logs" style="flex:1;">LOGS</button>
</div>
<!-- ── SCROLL AREA ── -->
<div id="tph-scroll" style="flex:1;overflow-y:auto;padding:12px;scrollbar-width:thin;scrollbar-color:#252538 #08081a;">
<!-- NERVE TAB -->
<div id="tph-tab-nerve">
<div class="tph-card">
<div class="tph-label">⚡ NERVE ATUAL</div>
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px;">
<div id="tph-nerve-val" style="font-size:36px;font-weight:800;letter-spacing:1px;color:#e0e0e0;line-height:1;">--/--</div>
<div id="tph-nerve-phase" style="font-size:11px;color:#3a3a6a;"></div>
</div>
<div style="background:#101020;border-radius:5px;height:10px;overflow:hidden;margin-bottom:5px;">
<div id="tph-nerve-bar" style="height:100%;width:0%;background:#5c2db0;border-radius:5px;transition:width .9s linear;"></div>
</div>
<div id="tph-nerve-sub" style="font-size:10px;color:#3a3a6a;"></div>
</div>
<div class="tph-card" style="margin-top:10px;">
<div class="tph-label" style="margin-bottom:8px;">⚙ CONFIGURAÇÃO DA SESSÃO</div>
<div style="font-size:10px;color:#3a3a5a;margin-bottom:6px;">Modo quando sem nerve:</div>
<div style="display:flex;gap:6px;margin-bottom:10px;">
<button id="tph-mode-wait" class="tph-btn" style="flex:1;font-size:10px;background:#1a3a1a;">🟢 Aguardar</button>
<button id="tph-mode-leave" class="tph-btn" style="flex:1;font-size:10px;background:#1a1a2e;">🔴 Sair</button>
</div>
<div id="tph-session-desc" style="font-size:10px;color:#2a2a4a;line-height:1.6;"></div>
</div>
<div class="tph-card" style="margin-top:10px;">
<div class="tph-label" style="margin-bottom:6px;">🎲 ANTI-PADRÃO</div>
<div style="font-size:10px;color:#2a2a4a;line-height:1.6;">
A cada <strong style="color:#ffd966;">4 crimes</strong>, o 4.º é sempre <strong style="color:#ffd966;">completamente aleatório</strong>.<br>
Refresh automático a cada <strong style="color:#6c5ce7;">3–5 min</strong> (URL sempre limpo).
</div>
<div style="margin-top:8px;font-size:10px;color:#3a3a5a;">Crimes até próximo aleatório:</div>
<div id="tph-counter-display" style="font-size:20px;font-weight:800;color:#6c5ce7;line-height:1.2;">—</div>
</div>
</div>
<!-- ALVOS TAB -->
<div id="tph-tab-alvos" style="display:none;">
<div id="tph-victims-wrap"></div>
</div>
<!-- DROPS TAB -->
<div id="tph-tab-drops" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<div class="tph-label" style="margin-bottom:0;">🍹 DROPS DRUNK WOMAN</div>
<button id="tph-clear-drops" style="padding:2px 8px;background:#160808;border:1px solid #3a1010;color:#993333;border-radius:4px;cursor:pointer;font-size:9px;">Repor</button>
</div>
<div id="tph-drops-list"></div>
</div>
<!-- STATS TAB -->
<div id="tph-tab-stats" style="display:none;">
<div class="tph-card" style="margin-bottom:10px;">
<div class="tph-label" style="margin-bottom:10px;">🦹 PICKPOCKETS</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
<div class="tph-stat-box"><div class="tph-stat-val" id="st-count">0</div><div class="tph-stat-lbl">crimes</div></div>
<div class="tph-stat-box"><div class="tph-stat-val" id="st-nerve">0</div><div class="tph-stat-lbl">nerve gasto</div></div>
<div class="tph-stat-box"><div class="tph-stat-val" id="st-succ">—</div><div class="tph-stat-lbl">sucesso%</div></div>
</div>
</div>
<div class="tph-card" style="margin-bottom:10px;">
<div class="tph-label" style="margin-bottom:10px;">👤 POR ALVO</div>
<div id="st-by-victim" style="font-size:11px;display:flex;flex-direction:column;gap:5px;"></div>
</div>
<button id="tph-reset-stats" style="width:100%;padding:8px;background:#160808;border:1px solid #3a1010;color:#993333;border-radius:6px;cursor:pointer;font-size:11px;">⚠ Repor estatísticas</button>
</div>
<!-- LOGS TAB -->
<div id="tph-tab-logs" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<div style="font-size:10px;color:#2a2a4a;">Últimas 300 entradas</div>
<button id="tph-clear-logs" style="padding:3px 10px;background:#160808;border:1px solid #3a1010;color:#993333;border-radius:4px;cursor:pointer;font-size:10px;">Limpar</button>
</div>
<div id="tph-logs-list" style="display:flex;flex-direction:column;gap:3px;font-size:11px;"></div>
</div>
</div><!-- /scroll -->
<style>
#tph-box * { box-sizing:border-box; }
#tph-scroll::-webkit-scrollbar { width:4px; }
#tph-scroll::-webkit-scrollbar-track { background:#08081a; }
#tph-scroll::-webkit-scrollbar-thumb { background:#202030; border-radius:3px; }
.tph-card { background:#0c0c1c;border:1px solid #1a1a2e;border-radius:8px;padding:12px; }
.tph-label { font-size:9px;letter-spacing:1.5px;color:#2a2a5a;text-transform:uppercase;margin-bottom:5px; }
.tph-btn { width:100%;padding:8px 0;border:none;border-radius:6px;color:#d0d0e8;cursor:pointer;font-size:12px;font-weight:700;transition:filter .15s; }
.tph-btn:hover { filter:brightness(1.35); }
.tph-tab { padding:7px 0;border:none;background:none;color:#2a2a4a;font-size:9px;font-weight:800;cursor:pointer;border-bottom:2px solid transparent;letter-spacing:.6px;text-transform:uppercase; }
.tph-tab.active { background:#0c0c1c !important;color:#6c5ce7 !important;border-bottom:2px solid #6c5ce7 !important; }
.tph-stat-box { background:#08081a;border:1px solid #1a1a2e;border-radius:6px;padding:8px;text-align:center; }
.tph-stat-val { font-size:20px;font-weight:800;color:#6c5ce7;line-height:1; }
.tph-stat-lbl { font-size:9px;color:#2a2a4a;margin-top:3px;text-transform:uppercase;letter-spacing:.4px; }
.tph-log { background:#0c0c1c;border-radius:4px;padding:4px 9px;border-left:2px solid #1a1a2e;line-height:1.5;font-size:10px; }
.tph-log-ts { color:#1e1e3a;font-size:9px;margin-right:5px; }
.tph-tinput { width:100%;background:#0c0c1a;border:1px solid #20203a;color:#d0d0e8;padding:5px;border-radius:5px;font-size:11px; }
</style>`;
document.body.appendChild(box);
/* ── Colapsar ── */
let collapsed = false;
document.getElementById("tph-collapse").onclick = () => {
collapsed = !collapsed;
box.style.transform = collapsed ? "translateX(360px)" : "translateX(0)";
document.getElementById("tph-collapse").textContent = collapsed ? "▶" : "◀";
};
/* ── Pausa ── */
function updatePauseUI() {
const paused = isPaused();
const btn = document.getElementById("tph-pause");
const banner = document.getElementById("tph-pause-banner");
if (paused) {
btn.textContent = "▶";
btn.style.background = "#0e3a0e"; btn.style.borderColor = "#1a7a1a"; btn.style.color = "#00e676";
if (banner) banner.style.display = "block";
addLog("crime", "⏸ Pausado");
} else {
btn.textContent = "⏸";
btn.style.background = "#3a1a00"; btn.style.borderColor = "#7a3a00"; btn.style.color = "#ffd966";
if (banner) banner.style.display = "none";
addLog("crime", "▶ Retomado");
}
}
document.getElementById("tph-pause").onclick = () => { setPaused(!isPaused()); updatePauseUI(); };
updatePauseUI();
/* ── Start manual ── */
document.getElementById("tph-start").onclick = () => {
if (isPickpocketPage()) { runPickpocketLoop(); }
else { addLog("warn", "⚠️ Navega para a página de pickpocket primeiro!"); }
};
/* ── Tabs ── */
const TAB_NAMES = ["nerve","alvos","drops","stats","logs"];
function switchTab(name) {
TAB_NAMES.forEach(t => {
document.getElementById(`tph-tab-${t}`).style.display = t === name ? "block" : "none";
});
document.querySelectorAll(".tph-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === name));
}
document.querySelectorAll(".tph-tab").forEach(b => b.addEventListener("click", () => switchTab(b.dataset.tab)));
switchTab("nerve");
/* ── Modo sessão ── */
function updateSessionModeUI() {
const m = getSessionMode();
const bw = document.getElementById("tph-mode-wait"), bl = document.getElementById("tph-mode-leave");
if (bw) bw.style.background = m === "wait" ? "#0e4a0e" : "#1a3a1a";
if (bl) bl.style.background = m === "leave" ? "#4a0e0e" : "#1a1a2e";
const desc = document.getElementById("tph-session-desc");
if (desc) desc.textContent = m === "wait"
? "🟢 Fica na página mesmo sem nerve. Ataca assim que aparecer alvo prioritário com ≥5 nerve."
: "🔴 Sai da sessão quando nerve < 5.";
}
document.getElementById("tph-mode-wait").onclick = () => { setSessionMode("wait"); updateSessionModeUI(); };
document.getElementById("tph-mode-leave").onclick = () => { setSessionMode("leave"); updateSessionModeUI(); };
updateSessionModeUI();
/* ── Botões ── */
document.getElementById("tph-clear-logs").onclick = () => clearLogs();
document.getElementById("tph-clear-drops").onclick = () => { if (confirm("Repor drops?")) { localStorage.removeItem(DROP_STATS_KEY); renderDropStats(); } };
document.getElementById("tph-reset-stats").onclick = () => { if (confirm("Repor estatísticas?")) { localStorage.removeItem(STATS_KEY); updateStatsDisplay(); } };
/* ══════════════════════════════════════════════════════
STATUS BADGE (header)
══════════════════════════════════════════════════════ */
function updateStatusBadge(text, color = "#3a3a6a") {
const el = document.getElementById("tph-status-badge");
if (el) { el.textContent = text; el.style.color = color; }
}
/* ══════════════════════════════════════════════════════
NERVE DISPLAY
══════════════════════════════════════════════════════ */
function updateNerveDisplay() {
const valEl = document.getElementById("tph-nerve-val");
const phaseEl = document.getElementById("tph-nerve-phase");
const barEl = document.getElementById("tph-nerve-bar");
const subEl = document.getElementById("tph-nerve-sub");
const cntEl = document.getElementById("tph-counter-display");
if (!valEl) return;
valEl.textContent = `${currentNerve}/${currentNerveMax}`;
const pct = currentNerveMax > 0 ? (currentNerve / currentNerveMax) * 100 : 0;
if (currentNerve >= currentNerveMax && currentNerveMax > 0) {
valEl.style.color = "#00e676";
phaseEl.textContent = "nerve cheio!"; phaseEl.style.color = "#00e676";
barEl.style.width = "100%"; barEl.style.background = "#00e676";
subEl.textContent = "Pronto para atacar";
} else if (currentNerve >= 5) {
valEl.style.color = "#d0d0e8";
phaseEl.textContent = `${currentNerveMax - currentNerve} em falta`; phaseEl.style.color = "#3a3a6a";
barEl.style.width = pct + "%"; barEl.style.background = pct > 70 ? "#7c3dd6" : "#4a1a90";
subEl.textContent = `${Math.round(pct)}% — nerve suficiente para atacar`;
} else {
valEl.style.color = "#ff6666";
phaseEl.textContent = "nerve baixo"; phaseEl.style.color = "#ff4444";
barEl.style.width = pct + "%"; barEl.style.background = "#4a1a1a";
subEl.textContent = `Precisa de ≥5 nerve para atacar`;
}
// Contador anti-padrão
if (cntEl) {
const nextRandom = 4 - (nervePickCounter % 4);
cntEl.textContent = nextRandom === 4 ? "4 (próximo é aleatório!)" : nextRandom;
cntEl.style.color = nextRandom === 1 ? "#ffd966" : "#6c5ce7";
}
}
/* ══════════════════════════════════════════════════════
TAB ALVOS — FILTROS DE STATUS E BUILD
══════════════════════════════════════════════════════ */
function renderVictimFilters() {
const wrap = document.getElementById("tph-victims-wrap");
if (!wrap) return;
wrap.innerHTML = "";
const filters = getTargetFilters();
VICTIM_DB.forEach(v => {
const vf = filters[v.id] || buildDefaultFilters()[v.id];
const card = document.createElement("div");
card.className = "tph-card";
card.style.marginBottom = "10px";
/* Header do alvo */
card.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<span style="font-size:24px;">${v.emoji}</span>
<div style="flex:1;">
<div style="font-weight:800;font-size:14px;color:#c0c0e0;">${v.label}</div>
<div style="font-size:9px;color:#3a3a6a;margin-top:2px;">$${v.cashMin.toLocaleString()}–$${v.cashMax.toLocaleString()} · ⏱~${v.timerSec}s · ${v.riskHosp ? "🏥 risco hosp" : ""}</div>
</div>
</div>
${v.requireRazor ? `<div style="background:#0c0c1a;border-radius:5px;padding:6px 8px;margin-bottom:10px;border-left:2px solid #ffd966;font-size:10px;color:#ffd966;">⭐ ${v.unique} <span style="color:#6c5ce7;">✂️ Razor no inv.</span></div>` : ""}
${v.notes ? `<div style="font-size:9px;color:#2a2a4a;margin-bottom:8px;line-height:1.5;">${v.notes}</div>` : ""}`;
/* Secção de status */
const statusSec = document.createElement("div");
statusSec.innerHTML = `<div class="tph-label" style="margin-bottom:6px;">🎭 Atacar quando status:</div>`;
const statusGrid = document.createElement("div");
statusGrid.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:3px;margin-bottom:6px;";
ALL_STATUSES.forEach(s => {
const inVictim = v.statuses.includes(s.id);
const checked = vf.statuses?.[s.id] === true;
const multColor = s.mult >= 1.2 ? "#00e676" : s.mult >= 1.0 ? "#ffd966" : "#ff6666";
const label = document.createElement("label");
label.style.cssText = `display:flex;align-items:center;gap:4px;padding:4px 6px;border-radius:4px;cursor:${inVictim ? "pointer" : "default"};background:${inVictim && checked ? "#0e1e0e" : "#080812"};border:1px solid ${inVictim ? (checked ? "#2a4a2a" : "#1a1a2e") : "#0e0e18"};opacity:${inVictim ? "1" : "0.3"};font-size:10px;`;
label.innerHTML = `<input type="checkbox" ${checked ? "checked" : ""} ${!inVictim ? "disabled" : ""} data-vid="${v.id}" data-sid="${s.id}" class="tph-status-cb" style="cursor:${inVictim ? "pointer" : "default"};"><span>${s.emoji} ${s.label}</span><span style="margin-left:auto;font-size:9px;color:${multColor};">×${s.mult.toFixed(1)}</span>`;
statusGrid.appendChild(label);
});
statusSec.appendChild(statusGrid);
/* Botões atalho */
const sBtns = document.createElement("div");
sBtns.style.cssText = "display:flex;gap:4px;margin-bottom:10px;";
sBtns.innerHTML = `
<button class="tph-btn tph-s-all" data-vid="${v.id}" style="flex:1;padding:4px;font-size:9px;background:#0e2a0e;border:1px solid #1a4a1a;">✓ Todos</button>
<button class="tph-btn tph-s-none" data-vid="${v.id}" style="flex:1;padding:4px;font-size:9px;background:#2a0e0e;border:1px solid #4a1a1a;">✗ Nenhum</button>
<button class="tph-btn tph-s-good" data-vid="${v.id}" style="flex:1;padding:4px;font-size:9px;background:#0e0e2e;border:1px solid #2a2a5a;">★ Bons</button>`;
statusSec.appendChild(sBtns);
card.appendChild(statusSec);
/* Secção de build */
const buildSec = document.createElement("div");
buildSec.innerHTML = `<div class="tph-label" style="margin-bottom:6px;">🧍 Atacar quando físico:</div>`;
const buildGrid = document.createElement("div");
buildGrid.style.cssText = "display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px;";
ALL_BUILDS.forEach(b => {
const checked = vf.builds?.[b.id] === true;
const isExclusive = v.requireBuild === b.id;
const label = document.createElement("label");
label.style.cssText = `display:flex;align-items:center;gap:4px;padding:4px 8px;border-radius:4px;cursor:pointer;background:${checked ? "#0e1e0e" : "#080812"};border:1px solid ${checked ? (isExclusive ? "#6c5ce7" : "#2a4a2a") : "#1a1a2e"};font-size:10px;`;
label.innerHTML = `<input type="checkbox" ${checked ? "checked" : ""} data-vid="${v.id}" data-bid="${b.id}" class="tph-build-cb" style="cursor:pointer;"><span>${b.emoji} ${b.label}${isExclusive ? ` <span style="color:#6c5ce7;font-size:9px;">★</span>` : ""}</span>`;
buildGrid.appendChild(label);
});
buildSec.appendChild(buildGrid);
buildSec.innerHTML += `<div style="font-size:9px;color:#1e1e3a;margin-bottom:6px;">★ Build com drops exclusivos</div>`;
card.appendChild(buildSec);
/* Resumo activo */
const summary = document.createElement("div");
summary.className = `tph-sum-${v.id}`;
summary.style.cssText = "background:#060610;border-radius:4px;padding:5px 8px;font-size:9px;color:#3a3a6a;line-height:1.6;";
card.appendChild(summary);
wrap.appendChild(card);
/* Eventos */
card.querySelectorAll(".tph-status-cb").forEach(cb => {
cb.addEventListener("change", () => { toggleStatusFilter(cb.dataset.vid, cb.dataset.sid); refreshVictimSummary(cb.dataset.vid); });
});
card.querySelectorAll(".tph-build-cb").forEach(cb => {
cb.addEventListener("change", () => { toggleBuildFilter(cb.dataset.vid, cb.dataset.bid); refreshVictimSummary(cb.dataset.vid); });
});
card.querySelector(".tph-s-all").addEventListener("click", () => {
const f = getTargetFilters();
v.statuses.forEach(s => { f[v.id].statuses[s] = true; }); saveTargetFilters(f); renderVictimFilters();
});
card.querySelector(".tph-s-none").addEventListener("click", () => {
const f = getTargetFilters();
ALL_STATUSES.forEach(s => { f[v.id].statuses[s.id] = false; }); saveTargetFilters(f); renderVictimFilters();
});
card.querySelector(".tph-s-good").addEventListener("click", () => {
const f = getTargetFilters();
ALL_STATUSES.forEach(s => { f[v.id].statuses[s.id] = v.goodStatus.includes(s.id); }); saveTargetFilters(f); renderVictimFilters();
});
refreshVictimSummary(v.id);
});
}
function refreshVictimSummary(victimId) {
const v = VICTIM_DB.find(x => x.id === victimId);
const f = getTargetFilters();
const vf = f[victimId] || buildDefaultFilters()[victimId];
const el = document.querySelector(`.tph-sum-${victimId}`);
if (!v || !el) return;
const activeS = ALL_STATUSES.filter(s => v.statuses.includes(s.id) && vf.statuses?.[s.id]);
const activeB = ALL_BUILDS.filter(b => vf.builds?.[b.id]);
el.innerHTML = (activeS.length && activeB.length)
? `🎯 Status aceites: <span style="color:#00e676;">${activeS.map(s => s.emoji + " " + s.label).join(", ")}</span><br>🧍 Builds aceites: <span style="color:#6c5ce7;">${activeB.map(b => b.emoji + " " + b.label).join(", ")}</span>`
: `<span style="color:#ff4444;">⚠️ Nenhum estado/build seleccionado — não vai atacar!</span>`;
}
renderVictimFilters();
/* ══════════════════════════════════════════════════════
DROPS DISPLAY
══════════════════════════════════════════════════════ */
function renderDropStats() {
const wrap = document.getElementById("tph-drops-list");
if (!wrap) return;
const data = getDropStats();
if (!Object.keys(data).length) {
wrap.innerHTML = `<div style="color:#1e1e3a;text-align:center;padding:20px;">Sem drops ainda.</div>`;
return;
}
wrap.innerHTML = Object.entries(data).map(([victimId, drops]) => {
const victim = VICTIM_DB.find(v => v.id === victimId);
const total = Object.values(drops).reduce((a,b)=>a+b,0);
return `
<div style="margin-bottom:10px;">
<div style="font-weight:800;color:#6c5ce7;margin-bottom:4px;">
${victim ? victim.emoji + " " + victim.label : victimId}
<span style="font-size:9px;color:#2a2a4a;">(${total})</span>
</div>
${
Object.entries(drops).map(([dropId,count])=>{
const drop = VICTIM_DROPS[victimId]?.find(d=>d.id===dropId);
if (!drop) return "";
return `<div style="font-size:11px;padding-left:10px;">
${drop.emoji} ${drop.name}
<span style="color:#6c5ce7;">x${count}</span>
</div>`;
}).join("")
}
</div>`;
}).join("");
}
/* ══════════════════════════════════════════════════════
STATS DISPLAY
══════════════════════════════════════════════════════ */
function updateStatsDisplay() {
const s = getStats();
const pp = s.pickpockets;
const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
set("st-count", pp.count);
set("st-nerve", pp.nerveSpent);
set("st-succ", pp.count > 0 ? Math.round((pp.successCount / pp.count) * 100) + "%" : "—");
const bvEl = document.getElementById("st-by-victim");
if (bvEl) {
bvEl.innerHTML = Object.entries(pp.byVictim).map(([id, data]) => {
const v = VICTIM_DB.find(x => x.id === id);
const pct = data.count > 0 ? Math.round((data.success / data.count) * 100) : 0;
return `<div style="display:flex;justify-content:space-between;background:#080812;padding:5px 8px;border-radius:4px;border-left:2px solid #1a1a2e;">
<span>${v ? v.emoji + " " + v.label : id}</span>
<span style="color:#6c5ce7;">${data.count}× <span style="color:#2a2a5a;font-size:9px;">${pct}%</span></span>
</div>`;
}).join("") || `<div style="color:#1e1e3a;text-align:center;padding:10px;">Sem dados ainda.</div>`;
}
}
updateStatsDisplay();
/* ══════════════════════════════════════════════════════
LOGS DISPLAY
══════════════════════════════════════════════════════ */
function renderLogs() {
const c = document.getElementById("tph-logs-list");
if (!c) return;
const logs = getLogs();
c.innerHTML = "";
if (!logs.length) { c.innerHTML = `<div style="color:#1e1e3a;text-align:center;padding:20px;font-size:11px;">Sem registos.</div>`; return; }
logs.forEach(e => {
const el = document.createElement("div");
el.className = "tph-log";
el.style.borderLeftColor = CAT_CLR[e.category] || "#1a1a2e";
el.innerHTML = `<span class="tph-log-ts">${e.ts}</span>${e.msg}`;
c.appendChild(el);
});
}
renderLogs();
/* ══════════════════════════════════════════════════════
TICK — ACTUALIZA NERVE DISPLAY 1×/s
══════════════════════════════════════════════════════ */
setInterval(() => {
readNerveFromDOM();
updateNerveDisplay();
}, 1000);
/* Fetch nerve da API a cada 30s */
setInterval(fetchNerveFromAPI, 30000);
/* ══════════════════════════════════════════════════════
ARRANQUE AUTOMÁTICO
Se já estamos na página de pickpocket → começa sozinho
══════════════════════════════════════════════════════ */
async function init() {
await fetchNerveFromAPI();
updateNerveDisplay();
updateStatsDisplay();
if (isPickpocketPage()) {
addLog("system", "🟢 HUD iniciado — página de pickpocket detectada — a arrancar sessão");
updateStatusBadge("🔄 A iniciar…", "#6c5ce7");
await delay(1500);
runPickpocketLoop();
} else if (isCrimesPage()) {
addLog("system", "⚠️ Página de crimes detectada — navega para #/pickpocketing para iniciar");
updateStatusBadge("⚠️ Navega para pickpocket", "#ffd966");
// Aguarda hash change para pickpocket
window.addEventListener("hashchange", () => {
if (isPickpocketPage() && !pickpocketRunning) {
addLog("system", "✅ Pickpocket detectado via hashchange — a arrancar");
runPickpocketLoop();
}
});
}
}
init();
addLog("system", "🟣 Pickpocket HUD v1.0 carregado — apenas crimes/pickpocketing");
})();