diff --git a/src/dom.js b/src/dom.js index c64c12f..347a49c 100644 --- a/src/dom.js +++ b/src/dom.js @@ -3,102 +3,77 @@ * { attr: 'strength'|'speed'|..., current: number, gym: string } * or `null` if the page doesn't look like a Torn gym page. * - * The selectors below are best-effort matches for torn.com/gym.php - * and will need adjustment if Torn changes the markup. + * 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']; +const KNOWN_GYMS = [ + 'Total Bastion', 'Frontline Fitness', 'Premier Fitness', 'Average Joes', + "Woody's Workout Club", "Baldr's Gym", 'Sportscience Laboratory', + 'Chrome Gym', "Mr. Miyagi's", 'Power House', 'Gym 300', 'Gym 400', 'Gym 500', 'Gym 600', + 'Elite Gym', "David's Gym", +]; + export function currentAttribute() { - // The attribute name is shown in the gym page header. - // Torn displays it as a capitalized word (e.g. "Strength") near the - // top of the gym form. - const KNOWN = ['strength', 'defense', 'speed', 'dexterity', 'endurance', 'intelligence']; - const ATTR_RE = new RegExp('\\b(' + KNOWN.join('|') + ')\\b'); - - const headers = document.querySelectorAll('h1, h2, h3, h4, .title, .gym-title, [class*="gym"]'); - let attr = null; - let attrEl = null; - for (const el of headers) { - const t = (el.textContent || '').trim().toLowerCase(); - const m = t.match(ATTR_RE); - if (m) { attr = m[1]; attrEl = el; break; } - } + const li = findActiveAttributeLi(); + if (!li) return null; + const attr = extractAttrFromLi(li); if (!attr) return null; - - // Current value: look for the prominent number on the page that is - // formatted like a Torn attribute (e.g. "14,328,501"). Search near - // the attribute element so we don't pick up unrelated global numbers. - let valEl = findValueNear(attrEl); - if (!valEl) valEl = findValueElement(); // fallback: whole-page scan - if (!valEl) return null; - const current = parseNumber(valEl.textContent); + const current = extractValueFromLi(li); if (current == null) return null; - - // Gym name: any element on the page containing the word "Gym" or - // "Bastion" / "Frontline" / etc. Torn's gym names vary. const gym = findGymName() || 'Unknown gym'; - return { attr, current, gym }; } -function findValueNear(el) { - // Look at the element itself, then up to a few ancestors, then their descendants. - // Prefer the largest formatted number within ~2 parent levels. - const scope = []; - let cur = el; - for (let depth = 0; depth < 3 && cur; depth++) { - scope.push(cur); - cur = cur.parentElement; +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; } - let best = null; - let bestN = -Infinity; - for (const root of scope) { - const candidates = root.querySelectorAll('*'); - for (const c of candidates) { - if (c.children.length > 0) continue; - const t = (c.textContent || '').trim(); - if (!/^[\d,]+(\.\d+)?$/.test(t)) continue; - const n = parseNumber(t); - if (n == null || n < 1) continue; - if (n > bestN) { best = c; bestN = n; } + // 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; + } } } - return best; + // 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 findValueElement() { - // Fallback only used when no element is found near the attribute. - // Walk all elements; pick the largest formatted number on the page. - const candidates = document.querySelectorAll('*'); - let best = null; - let bestN = -Infinity; - for (const el of candidates) { - if (el.children.length > 0) continue; - const t = (el.textContent || '').trim(); - if (!/^[\d,]+(\.\d+)?$/.test(t)) continue; - const n = parseNumber(t); - if (n == null || n < 1) continue; - if (n > bestN) { best = el; bestN = n; } +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 best; + return null; +} + +function extractValueFromLi(li) { + const valueSpan = li.querySelector('[class^="propertyValue"]'); + if (!valueSpan) return null; + return parseNumber(valueSpan.textContent); } function findGymName() { - // Look for a known set of Torn gym name fragments. Adjust as needed. - // Prefer an element that looks like the gym panel so we don't match - // against global widgets (news, sidebar, ads). - const panel = document.querySelector('.gym, #gym, [class*="gym-"], [class*="Gym"]'); - const roots = panel ? [panel, document.body] : [document.body]; - const known = [ - 'Total Bastion', 'Frontline Fitness', 'Gym 300', 'Gym 500', - 'Baldr\'s Gym', 'Sportscience Laboratory', 'Premier Fitness', - 'Chrome Gym', 'Mr. Miyagi\'s', 'Power House', - ]; - for (const root of roots) { - const all = root.querySelectorAll('h1, h2, h3, h4, p, span, div, li'); - for (const el of all) { - if (el.children.length > 0) continue; - const t = (el.textContent || '').trim(); - for (const name of known) { - if (t.includes(name)) return name; + // Gym names live in aria-labels of