diff --git a/src/interceptor.js b/src/interceptor.js index f8fc4fe..8f04908 100644 --- a/src/interceptor.js +++ b/src/interceptor.js @@ -5,15 +5,16 @@ * compared against the previous one, and `onTrain({attr, delta, ts})` * is invoked. * - * `parseTrainResponse(text, url)` is intentionally permissive and - * returns `{ newValue, attr } | null`. The default implementation - * tries JSON first, then a regex fallback. + * `parseTrainResponse(text, url)` returns: + * { delta, attr } — when a "You gained X.XX " message is found + * { newValue, attr } — when a propertyValue span or JSON newValue is found + * null — when nothing usable is found */ export function startRequestInterceptor({ prevValue, currentAttr, onTrain, onParseFail }) { let lastValue = prevValue; let warnedFor = null; - const handle = (text, url) => { + function handle(text, url) { const parsed = parseTrainResponse(text, url, currentAttr); if (!parsed) { if (warnedFor !== url) { @@ -22,11 +23,22 @@ export function startRequestInterceptor({ prevValue, currentAttr, onTrain, onPar } return; } - if (parsed.attr && currentAttr && parsed.attr !== currentAttr) return; - const delta = parsed.newValue - lastValue; - lastValue = parsed.newValue; - if (delta > 0) onTrain({ attr: parsed.attr, delta, ts: Date.now() }); - }; + + let delta; + if (typeof parsed.delta === 'number' && parsed.delta > 0) { + delta = parsed.delta; + } else if (typeof parsed.newValue === 'number' && parsed.newValue > 0) { + delta = parsed.newValue - lastValue; + lastValue = parsed.newValue; + } else { + return; + } + + if (delta <= 0) return; + const attr = parsed.attr || currentAttr; + if (!attr) return; + onTrain({ attr, delta, ts: Date.now() }); + } wrapXhr(handle); wrapFetch(handle); @@ -37,23 +49,31 @@ export function startRequestInterceptor({ prevValue, currentAttr, onTrain, onPar } function parseTrainResponse(text, url, fallbackAttr) { - // Try JSON + // Strategy 1: look for the "gained" message in the response. + // Format: "You gained " (e.g. "You gained 10,885.76 dexterity"). + // Torn sometimes prefixes with other text (e.g. "You gained 10,885.76 dexterity"), + // so we match the number-and-attribute-name pattern directly. + const gainedMatch = text.match(/[Yy]ou\s+gained\s+([\d,]+(?:\.\d+)?)\s+(strength|defense|speed|dexterity|endurance|intelligence)\b/i); + if (gainedMatch) { + const delta = parseFloat(gainedMatch[1].replace(/,/g, '')); + const attr = gainedMatch[2].toLowerCase(); + if (Number.isFinite(delta) && delta >= 0) { + return { delta, attr }; + } + } + // Strategy 2: JSON with newValue + attr. try { const j = JSON.parse(text); - // Torn historically returns an HTML fragment; if it's JSON, look - // for a known shape. This is a placeholder — adjust after manual - // verification. if (j && typeof j === 'object' && 'newValue' in j && 'attr' in j) { return { newValue: Number(j.newValue), attr: String(j.attr) }; } } catch { /* not JSON */ } - - // Regex fallback: scan text for a number formatted like an attribute. - // If we find one and the caller passed a fallbackAttr, use it; otherwise - // the caller can choose to ignore the result. - const m = text.match(/(\d{1,3}(?:,\d{3})+|\d{4,})/); - if (m) { - const newValue = parseInt(m[1].replace(/,/g, ''), 10); + // Strategy 3: regex fallback (last resort). Don't use the first number + // blindly; look specifically for the propertyValue span content, which + // is the authoritative source. + const propertyValueMatch = text.match(/class="propertyValue[^"]*"[^>]*>([\d,]+(?:\.\d+)?) 0) { return { newValue, attr: fallbackAttr || null }; }