fix(dom): scope value search near attribute, whole-word matching, panel-scoped gym name

This commit is contained in:
dev
2026-06-01 16:38:22 -05:00
parent e647fbf545
commit aec9c40835
+42 -9
View File
@@ -11,21 +11,23 @@ export function currentAttribute() {
// Torn displays it as a capitalized word (e.g. "Strength") near the // Torn displays it as a capitalized word (e.g. "Strength") near the
// top of the gym form. // top of the gym form.
const KNOWN = ['strength', 'defense', 'speed', 'dexterity', 'endurance', 'intelligence']; 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"]'); const headers = document.querySelectorAll('h1, h2, h3, h4, .title, .gym-title, [class*="gym"]');
let attr = null; let attr = null;
let attrEl = null;
for (const el of headers) { for (const el of headers) {
const t = (el.textContent || '').trim().toLowerCase(); const t = (el.textContent || '').trim().toLowerCase();
for (const k of KNOWN) { const m = t.match(ATTR_RE);
if (t.includes(k)) { attr = k; break; } if (m) { attr = m[1]; attrEl = el; break; }
}
if (attr) break;
} }
if (!attr) return null; if (!attr) return null;
// Current value: look for the prominent number on the page that is // Current value: look for the prominent number on the page that is
// formatted like a Torn attribute (e.g. "14,328,501"). // formatted like a Torn attribute (e.g. "14,328,501"). Search near
const valEl = findValueElement(); // 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; if (!valEl) return null;
const current = parseNumber(valEl.textContent); const current = parseNumber(valEl.textContent);
if (current == null) return null; if (current == null) return null;
@@ -37,10 +39,34 @@ export function currentAttribute() {
return { attr, current, 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;
}
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; }
}
}
return best;
}
function findValueElement() { function findValueElement() {
// Fallback only used when no element is found near the attribute.
// Walk all elements; pick the largest formatted number on the page. // Walk all elements; pick the largest formatted number on the page.
// Torn renders the current attribute as a big number near the
// "Property" label.
const candidates = document.querySelectorAll('*'); const candidates = document.querySelectorAll('*');
let best = null; let best = null;
let bestN = -Infinity; let bestN = -Infinity;
@@ -57,18 +83,25 @@ function findValueElement() {
function findGymName() { function findGymName() {
// Look for a known set of Torn gym name fragments. Adjust as needed. // 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 = [ const known = [
'Total Bastion', 'Frontline Fitness', 'Gym 300', 'Gym 500', 'Total Bastion', 'Frontline Fitness', 'Gym 300', 'Gym 500',
'Baldr\'s Gym', 'Sportscience Laboratory', 'Premier Fitness', 'Baldr\'s Gym', 'Sportscience Laboratory', 'Premier Fitness',
'Chrome Gym', 'Mr. Miyagi\'s', 'Power House', 'Chrome Gym', 'Mr. Miyagi\'s', 'Power House',
]; ];
const all = document.querySelectorAll('p, span, div, h1, h2, h3, h4, li'); for (const root of roots) {
const all = root.querySelectorAll('h1, h2, h3, h4, p, span, div, li');
for (const el of all) { for (const el of all) {
if (el.children.length > 0) continue;
const t = (el.textContent || '').trim(); const t = (el.textContent || '').trim();
for (const name of known) { for (const name of known) {
if (t.includes(name)) return name; if (t.includes(name)) return name;
} }
} }
}
return null; return null;
} }