Tracks successful Place, Stoke, Dampen actions. Fixed for current Torn class hashes.
// ==UserScript==
// @name Arson Action Tracker
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Tracks successful Place, Stoke, Dampen actions. Fixed for current Torn class hashes.
// @author Allenone[2033011]
// @match https://www.torn.com/page.php?sid=crimes*
// @license MIT
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
let data = GM_getValue('arsonCounts', {}) || {};
// Helper: find element by partial class prefix (survives hash rotations)
function qS(parent, prefix) {
return (parent || document).querySelector(`[class*="${prefix}"]`);
}
function qSA(parent, prefix) {
return (parent || document).querySelectorAll(`[class*="${prefix}"]`);
}
function getTargetKey(targetCard) {
const titleAndScenario = qS(targetCard, 'titleAndScenario___');
if (!titleAndScenario) return null;
const title = titleAndScenario.querySelector('div:first-child')?.textContent.trim();
const scenario = qS(titleAndScenario, 'scenario___')?.textContent.trim();
return title && scenario ? `${title} - ${scenario}` : null;
}
function updateCounters(div, counts) {
if (!counts) {
div.innerHTML = '';
return;
}
div.innerHTML = `
Placed: ${counts.placed}<br>
Stoked: ${counts.stoked}<br>
Dampened: ${counts.dampened}
`;
}
function attachToTarget(targetCard) {
const key = getTargetKey(targetCard);
if (!key) return;
if (!data[key]) data[key] = { placed: 0, stoked: 0, dampened: 0 };
// Find or create counter display
let countersDiv = targetCard.querySelector('.custom-counters');
if (!countersDiv) {
countersDiv = document.createElement('div');
countersDiv.className = 'custom-counters';
Object.assign(countersDiv.style, {
fontSize: 'smaller',
lineHeight: '1.2',
paddingLeft: 'inherit',
textAlign: 'right',
minWidth: '80px',
marginLeft: 'auto',
paddingRight: '4px',
whiteSpace: 'nowrap'
});
// Use partial class match for titleSection
const titleSection = qS(targetCard, 'titleSection___');
if (titleSection) {
titleSection.appendChild(countersDiv);
titleSection.style.display = 'flex';
titleSection.style.alignItems = 'center';
titleSection.style.justifyContent = 'space-between';
}
}
updateCounters(countersDiv, data[key]);
// Find the actual commit button (the <button> element, not the section wrapper)
const commitBtn = targetCard.querySelector('button[class*="commitButton___"]');
if (commitBtn && !commitBtn.dataset.listenerAdded) {
commitBtn.addEventListener('click', () => {
// Determine intended action from button title or aria-label
let action = '';
const actionSpan = qS(commitBtn, 'title___');
if (actionSpan) {
action = actionSpan.textContent.trim().toLowerCase();
} else if (commitBtn.ariaLabel) {
action = commitBtn.ariaLabel.split(',')[0].trim().toLowerCase();
}
// Watch for outcome after click
const outcomeWrapper = qS(targetCard, 'outcomeWrapper___');
if (!outcomeWrapper) return;
const observer = new MutationObserver((mutations, obs) => {
// Look for outcome content - check for success/fail text
const outcomeText = outcomeWrapper.textContent.trim().toLowerCase();
if (outcomeText.length > 0) {
// Check if it's a success outcome
if (outcomeText.includes('success') || outcomeWrapper.querySelector('[class*="reward"]') || outcomeWrapper.querySelector('[class*="success"]')) {
if (action.includes('place')) data[key].placed++;
else if (action.includes('stoke')) data[key].stoked++;
else if (action.includes('dampen')) data[key].dampened++;
else if (action.includes('collect')) {
delete data[key];
GM_setValue('arsonCounts', data);
updateCounters(countersDiv, null);
obs.disconnect();
return;
}
updateCounters(countersDiv, data[key]);
GM_setValue('arsonCounts', data);
}
obs.disconnect();
}
});
observer.observe(outcomeWrapper, { childList: true, subtree: true });
// Safety timeout - disconnect observer after 10 seconds
setTimeout(() => observer.disconnect(), 10000);
});
commitBtn.dataset.listenerAdded = 'true';
}
}
function init() {
// Use partial class match for virtualList and virtualItem
const targets = qSA(document, 'virtualItem___');
targets.forEach(target => {
// Only process items that have content (virtual list may have empty placeholders)
if (!target.dataset.processed && target.innerHTML.length > 10) {
attachToTarget(target);
target.dataset.processed = 'true';
}
});
}
// Only run on arson page
function isArsonPage() {
return window.location.hash.includes('/arson');
}
const observer = new MutationObserver(() => {
if (isArsonPage()) init();
});
observer.observe(document.body, { childList: true, subtree: true });
if (isArsonPage()) init();
})();