83 lines
2.9 KiB
JavaScript
83 lines
2.9 KiB
JavaScript
/**
|
|
* 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 <li> 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 <li> 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 <li> 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: "<Gym Name>. 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;
|
|
}
|