/** * Reads the gym page DOM and returns: * { attr: 'strength'|'speed'|..., current: number, gym: string } * or `null` if the page doesn't look like a Torn gym page. * * Torn's gym page is a React app using CSS modules with hash suffixes * (e.g. class="strength___iXqEf", class="propertyValue___IYxjf"). This * scraper targets Torn's actual structure rather than guessing at selectors. */ const KNOWN_ATTRS = ['strength', 'defense', 'speed', 'dexterity', 'endurance', 'intelligence']; export function currentAttribute() { const li = findActiveAttributeLi(); if (!li) return null; const attr = extractAttrFromLi(li); if (!attr) return null; const current = extractValueFromLi(li); if (current == null) return null; const gym = findGymName() || 'Unknown gym'; return { attr, current, gym }; } function findActiveAttributeLi() { // Priority 1: the
  • with the "success" class (just trained). const lis = document.querySelectorAll('ul[class*="properties"] > li[class*="success"]'); for (const li of lis) { if (extractAttrFromLi(li)) return li; } // Priority 2: the
  • corresponding to the .gained message's attribute. const gained = document.querySelector('[class*="gained"]'); if (gained) { const text = (gained.textContent || '').toLowerCase(); for (const attr of KNOWN_ATTRS) { if (text.includes(attr)) { const li = document.querySelector('ul[class*="properties"] > li[class^="' + attr + '___"]'); if (li) return li; } } } // Priority 3: the first
  • in the properties list. const all = document.querySelectorAll('ul[class*="properties"] > li'); for (const li of all) { if (extractAttrFromLi(li)) return li; } return null; } function extractAttrFromLi(li) { const cls = li.className || ''; for (const attr of KNOWN_ATTRS) { if (cls.split(/\s+/).some((c) => c.startsWith(attr + '___'))) return attr; } return null; } function extractValueFromLi(li) { const valueSpan = li.querySelector('[class^="propertyValue"]'); if (!valueSpan) return null; return parseNumber(valueSpan.textContent); } function findGymName() { // Find the currently selected gym button. It has the "active" class. const activeBtn = document.querySelector('button[class*="gymButton"][class*="active"]'); if (activeBtn) { const label = activeBtn.getAttribute('aria-label') || ''; // aria-label format: ". Membership cost - $X. Energy usage - N per train." // The gym name is everything before the first ". ". const dot = label.indexOf('. '); if (dot !== -1) return label.slice(0, dot); return label; // no period, return whole label as fallback } return null; } function parseNumber(text) { if (!text) return null; const cleaned = text.replace(/,/g, '').trim(); if (!/^\d+(\.\d+)?$/.test(cleaned)) return null; const n = parseFloat(cleaned); return Number.isFinite(n) ? Math.floor(n) : null; }