// ==UserScript==
// @name Loot Finder
// @namespace Zega
// @version 1.2.1
// @description No color for ≤50k; Orange 50,001–89,999; Red ≥90k; BLUE for Mastercrafted-like items. Fast Scan, legend, progress bar, top-row fix, MC confirmation, mouse-movement shield. Slot-sticky highlights with icon-signature guards and debounced forgetting so colors persist correctly after moves.
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php*
// @match https://www.fairview.deadfrontier.com/onlinezombiemmo/index.php*
// @run-at document-start
// @all-frames true
// @grant none
//
// Changes in 1.2.1:
// - Moving one “identical-looking” item could unhighlight another identical item. Fixed by:
// (a) Migrating slot memory when indices shift (slot-key refresh).
// (b) Debouncing mastercrafted “forget” on temporary signature mismatches during drag/reflow.
// ==/UserScript==
(function () {
'use strict';
/* ---------- config & constants ---------- */
const GREEN_MAX = 50000;
const ORANGE_MAX = 89999;
const COLORS = {
orange: 'rgba(255,165,0,.55)',
red: 'rgba(220,40,40,.55)',
blue: 'rgba(60,140,255,.55)',
border: 'rgba(255,255,255,.95)'
};
/* ---------- storage keys ---------- */
const LS_KEY_TYPES_V2 = 'df_scrap_color_types_v2'; // key: `${type}||${iconSig}` -> 'orange'|'red'
const LS_KEY_ENABLED = 'df_scrap_enabled_v1';
const LS_KEY_MC_SLOTS = 'df_mc_slots_v3'; // { [pageKey]: { [slotKey]: {sig} } }
const SS_KEY_SLOT_COL = 'df_slot_colors_v2'; // sessionStorage: { [pageKey]: { [slotKey]: {sig, type, col, val} } }
// retire old broad type memory
try { localStorage.removeItem('df_scrap_color_types_v1'); } catch {}
/* ---------- page key (separate per DF page) ---------- */
const PAGE_KEY = (()=>{ try{
const u = new URL(location.href);
const gp = new URLSearchParams(u.search).get('page') || '';
return u.pathname + '?page=' + gp;
}catch{ return location.pathname; } })();
/* ---------- tiny JSON helpers ---------- */
const jget = (s,k,def)=>{ try{ const v=s.getItem(k); return v?JSON.parse(v):def; }catch{ return def; } };
const jset = (s,k,v)=>{ try{ s.setItem(k, JSON.stringify(v)); }catch{} };
/* ---------- enable/disable ---------- */
function loadEnabled(){ try{ const v = localStorage.getItem(LS_KEY_ENABLED); return v==null? true : v==='1'; }catch{ return true; } }
function saveEnabled(v){ try{ localStorage.setItem(LS_KEY_ENABLED, v?'1':'0'); }catch{} }
let ENABLED = loadEnabled();
/* ---------- memories ---------- */
const COLOR_TYPES = jget(localStorage, LS_KEY_TYPES_V2, {}); // `${type}||${sig}` -> 'orange'|'red'
const ALL_MC = jget(localStorage, LS_KEY_MC_SLOTS, {});
if (!ALL_MC[PAGE_KEY]) ALL_MC[PAGE_KEY] = {};
const MC_SLOTS = ALL_MC[PAGE_KEY]; // slotKey -> { sig }
const saveMc = ()=>{ ALL_MC[PAGE_KEY] = MC_SLOTS; jset(localStorage, LS_KEY_MC_SLOTS, ALL_MC); };
const ALL_SLOT_COL = jget(sessionStorage, SS_KEY_SLOT_COL, {});
if (!ALL_SLOT_COL[PAGE_KEY]) ALL_SLOT_COL[PAGE_KEY] = {};
const SLOT_COL = ALL_SLOT_COL[PAGE_KEY]; // slotKey -> { sig, type, col, val }
const saveSlot = ()=>{ ALL_SLOT_COL[PAGE_KEY] = SLOT_COL; jset(sessionStorage, SS_KEY_SLOT_COL, ALL_SLOT_COL); };
/* ---------- mouse + scan gate ---------- */
let mouseX=0, mouseY=0, SCANNING=false;
const onMove = e => { if (!SCANNING){ mouseX=e.clientX; mouseY=e.clientY; } };
document.addEventListener('mousemove', onMove, true);
document.addEventListener('mouseover', onMove, true);
/* ---------- tooltip parsing ---------- */
const htmlToText = s => { const t=document.createElement('textarea'); t.innerHTML=s||''; return t.value.replace(/\u00A0/g,' ').replace(/<br\s*\/?>/gi,'\n').replace(/<[^>]+>/g,' '); };
function readTooltipFromDoc(doc){
const ids=['dhtmltooltip','tiplayer','toolTip','tooltip'];
for(const id of ids){
const el = doc.getElementById && doc.getElementById(id);
if(el && el.offsetParent !== null && (el.textContent||'').length) return el.innerHTML||el.textContent;
}
const cand = Array.from(doc.querySelectorAll('div')).find(d =>
d.offsetParent !== null && (d.textContent||'').length < 800 && /Scrap\s*(Price|Value)/i.test(d.textContent||'')
);
return cand ? (cand.innerHTML||cand.textContent) : null;
}
function liveTooltipHTML(){
let h = readTooltipFromDoc(document); if(h) return h;
try{ if (window.parent && window.parent!==window){ h=readTooltipFrom(window.parent.document); if(h) return h; } }catch{}
try{
const top = window.top||window;
h=readTooltipFrom(top.document); if(h) return h;
for(let i=0;i<top.frames.length;i++){ try{ h=readTooltipFrom(top.frames[i].document); if(h) return h; }catch{} }
}catch{}
return null;
}
function parseScrapStrict(text){
if(!text) return null;
const s = htmlToText(text);
const m = s.match(/Scrap\s*(?:Price|Value)\s*:\s*\$?\s*([0-9][0-9,.\s]*)/i);
if (!m) return null;
const n = Number(String(m[1]).replace(/[,.\s]/g,''));
return Number.isFinite(n) ? n : null;
}
function isMastercrafted(html){
if(!html) return false;
if (/(color\s*[:=]\s*["']?\s*(?:#?ffff00|#?ff0|yellow))/i.test(html)) return true;
const txt = htmlToText(html);
if (/\+\s*\d+\s+(Accuracy|Critical|Reload|Reload Speed|Recoil|Damage|Fortitude|Agility|Dodging|Blocking|Running|Searching|Looting|SPR|DPS)/i.test(txt)) return true;
return false;
}
/* ---------- geometry/helpers ---------- */
const isSquareish = r => r && r.width>=35 && r.width<=160 && r.height>=35 && r.height<=160 && (r.width/r.height)>0.75 && (r.width/r.height)<1.33;
function pickBoxFrom(el){
let p=el;
for(let i=0;i<8 && p && p!==document.body;i++,p=p.parentElement){
const r=p.getBoundingClientRect(); if(isSquareish(r)) return p;
}
return el;
}
function hasBgImage(node){ if(!node) return false; const bg=getComputedStyle(node).backgroundImage; return !!(bg && bg!=='none'); }
function boxHasIconFromItem(itemEl){ return !!(itemEl && hasBgImage(itemEl)); }
// Robust icon signature: image + position + size
function getIconSig(itemEl){
try{
const cs = getComputedStyle(itemEl);
const img = cs.backgroundImage || '';
const pos = (cs.backgroundPosition || ((cs.backgroundPositionX||'')+' '+(cs.backgroundPositionY||''))).trim();
const size = cs.backgroundSize || '';
return [img, pos, size].join('|');
}catch{ return null; }
}
function colorFor(scrap){
if (scrap <= GREEN_MAX) return 'none';
if (scrap <= ORANGE_MAX) return 'orange';
return 'red';
}
/* ---------- slot identity ---------- */
function computeSlotKey(itemEl){
const container = itemEl.closest('.playerInv, .inventory, .storage, .invGrid, td, div') || itemEl.parentElement || document.body;
let list = Array.from(container.querySelectorAll('div.item'));
let idx = list.indexOf(itemEl);
if (idx < 0) { list = Array.from(document.querySelectorAll('div.item')); idx = list.indexOf(itemEl); }
const tag = (container.tagName||'DIV');
const id = (container.id||'').slice(0,40);
const cls = (container.className||'').toString().split(/\s+/).slice(0,3).join('.');
return `${tag}#${id}.${cls}::${idx}`;
}
/* ---------- MC slot memory ---------- */
function rememberMaster(itemEl){
const slotKey = computeSlotKey(itemEl);
const sig = getIconSig(itemEl) || '';
itemEl.dataset.dfSlotKey = slotKey;
itemEl.dataset.dfMaster = '1';
MC_SLOTS[slotKey] = { sig };
saveMc();
}
function forgetMasterAtSlot(slotKey){
if (MC_SLOTS[slotKey]){ delete MC_SLOTS[slotKey]; saveMc(); }
}
/* ---------- non-MC slot memory ---------- */
function rememberSlotColor(itemEl, col, val){
if (!itemEl) return;
if (col==='none' || col==='blue') return; // only persist orange/red
const slotKey = computeSlotKey(itemEl);
const sig = getIconSig(itemEl) || '';
const type = (itemEl.dataset && itemEl.dataset.type ? String(itemEl.dataset.type).toLowerCase() : '');
itemEl.dataset.dfSlotKey = slotKey;
SLOT_COL[slotKey] = { sig, type, col, val: Number(val)||null };
saveSlot();
}
function slotEntryMatches(itemEl, entry){
if (!entry) return false;
const sigNow = getIconSig(itemEl) || '';
const typeNow = (itemEl.dataset && itemEl.dataset.type ? String(itemEl.dataset.type).toLowerCase() : '');
if (entry.sig && entry.sig !== sigNow) return false;
if (entry.type && entry.type !== typeNow) return false;
return true;
}
/* ---------- painter ---------- */
function clearBox(box){
if (!box) return;
box.style.outline=''; box.style.boxShadow=''; box.style.borderRadius='';
box.removeAttribute('data-df-scrap-painted');
box.removeAttribute('data-df-scrap-color');
const pill = box.querySelector('.df-scrap-pill'); if (pill) pill.remove();
}
function paintBox(box, scrap, color){
if (!box) return;
if (color === 'none'){ clearBox(box); return; }
if (getComputedStyle(box).position === 'static') box.style.position='relative';
box.dataset.dfScrapPainted='1';
box.dataset.dfScrapValue = String(scrap);
box.dataset.dfScrapColor = color;
const col = COLORS[color];
box.style.outline = `2px solid ${COLORS.border}`;
box.style.boxShadow = `0 0 0 4px ${col} inset, 0 0 10px 0 ${col}`;
box.style.borderRadius = '6px';
let tag = box.querySelector('.df-scrap-pill');
if(!tag){ tag = document.createElement('div'); tag.className='df-scrap-pill'; Object.assign(tag.style,{display:'none'}); box.appendChild(tag); }
}
/* ---------- migrate slot keys when indices shift ---------- */
function refreshSlotKeys(){
document.querySelectorAll('div.item').forEach(item=>{
const oldKey = item.dataset.dfSlotKey;
const newKey = computeSlotKey(item);
if (!oldKey){ item.dataset.dfSlotKey = newKey; return; }
if (oldKey === newKey) return;
const sig = getIconSig(item) || '';
// Move MC memory if signature still matches
const mc = MC_SLOTS[oldKey];
if (mc && mc.sig === sig){
delete MC_SLOTS[oldKey];
MC_SLOTS[newKey] = mc;
item.dataset.dfMaster = '1';
saveMc();
}
// Move non-MC slot color if signature still matches
const sc = SLOT_COL[oldKey];
if (sc && sc.sig === sig){
delete SLOT_COL[oldKey];
SLOT_COL[newKey] = sc;
saveSlot();
}
item.dataset.dfSlotKey = newKey;
});
}
/* ---------- re-appliers ---------- */
const MC_MISMATCH_SINCE = {}; // slotKey -> timestamp
const MC_FORGET_DELAY = 800; // ms
function paintAllMasters(){
document.querySelectorAll('div.item').forEach(item=>{
const slotKey = computeSlotKey(item); // use current key
const sigNow = getIconSig(item) || '';
const entry = MC_SLOTS[slotKey];
// if nothing stored for this slot, clear debounce and skip
if (!entry){ delete MC_MISMATCH_SINCE[slotKey]; return; }
// If icon temporarily blank/hidden during drag, don't forget yet
const box = pickBoxFrom(item);
const rect = box && box.getBoundingClientRect();
const visible = !!(rect && rect.width>0 && rect.height>0);
if (!visible || !sigNow){ return; }
// Debounced forgetting when signature differs
if (entry.sig !== sigNow){
const now = performance.now();
if (!MC_MISMATCH_SINCE[slotKey]){ MC_MISMATCH_SINCE[slotKey] = now; return; }
if (now - MC_MISMATCH_SINCE[slotKey] < MC_FORGET_DELAY) return;
delete MC_MISMATCH_SINCE[slotKey];
forgetMasterAtSlot(slotKey);
return;
}
delete MC_MISMATCH_SINCE[slotKey];
// Paint if matches
item.dataset.dfMaster = '1';
item.dataset.dfSlotKey = slotKey;
if (rect && isSquareish(rect) && boxHasIconFromItem(item)){
const val = Number(item.dataset.dfScrapValue) || GREEN_MAX;
paintBox(box, val, 'blue');
}
});
}
function paintAllBySlot(){
document.querySelectorAll('div.item').forEach(item=>{
const slotKey = computeSlotKey(item);
const entry = SLOT_COL[slotKey];
if (!entry) return;
if (!slotEntryMatches(item, entry)){ delete SLOT_COL[slotKey]; saveSlot(); return; }
const box = pickBoxFrom(item);
const rect = box && box.getBoundingClientRect();
if (rect && isSquareish(rect) && boxHasIconFromItem(item)){
const val = Number(item.dataset.dfScrapValue);
paintBox(box, Number.isFinite(val)? val : (entry.val||GREEN_MAX), entry.col);
}
});
}
function paintAllByType(type, color){
if (!type || color==='none') return;
const key = String(type).toLowerCase();
document.querySelectorAll('div.item').forEach(item=>{
const sk = computeSlotKey(item);
if (MC_SLOTS[sk]) return; // don't override MC
if (SLOT_COL[sk] && slotEntryMatches(item, SLOT_COL[sk])) return; // nor explicit slot color
const t = item.dataset && item.dataset.type ? item.dataset.type.toLowerCase() : '';
if (t !== key) return;
const box = pickBoxFrom(item);
const rect = box && box.getBoundingClientRect();
if (rect && isSquareish(rect) && boxHasIconFromItem(item)){
const val = Number(item.dataset.dfScrapValue) || GREEN_MAX;
paintBox(box, val, color);
}
});
}
/* ---------- hover-driven loop ---------- */
function tick(){
try{
if (!ENABLED || SCANNING) return;
const tipHTML = liveTooltipHTML(); if(!tipHTML) return;
const target = document.elementFromPoint(mouseX, mouseY); if(!target) return;
const itemEl = target.closest && target.closest('div.item'); if(!itemEl) return;
const scrap = parseScrapStrict(tipHTML); if(scrap==null) return;
const box = pickBoxFrom(itemEl);
const rect = box && box.getBoundingClientRect();
if(!(rect && isSquareish(rect) && boxHasIconFromItem(itemEl))) return;
const master = isMastercrafted(tipHTML);
itemEl.dataset.dfScrapValue = String(scrap);
const sig = getIconSig(itemEl) || '';
const type = (itemEl.dataset && itemEl.dataset.type) ? itemEl.dataset.type.toLowerCase() : '';
if (master){
rememberMaster(itemEl);
paintBox(box, scrap, 'blue');
const sk = itemEl.dataset.dfSlotKey || computeSlotKey(itemEl);
if (SLOT_COL[sk]){ delete SLOT_COL[sk]; saveSlot(); }
} else {
const col = colorFor(scrap);
paintBox(box, scrap, col);
if (col!=='none') rememberSlotColor(itemEl, col, scrap);
if (col!=='none' && type){
const tkey = `${type}||${sig}`;
if (COLOR_TYPES[tkey] !== col){
COLOR_TYPES[tkey] = col;
jset(localStorage, LS_KEY_TYPES_V2, COLOR_TYPES);
}
}
}
}catch{}
}
setInterval(tick, 120);
/* ---------- reapply flow ---------- */
function reapplyAll(){
if (!ENABLED) return;
refreshSlotKeys(); // NEW: keep memories lined up with shifted indices
paintAllMasters();
paintAllBySlot();
Object.entries(COLOR_TYPES).forEach(([k,col])=>{
if (!col || col==='none') return;
const [type, sig] = k.split('||');
document.querySelectorAll('div.item').forEach(item=>{
const t = (item.dataset && item.dataset.type) ? item.dataset.type.toLowerCase() : '';
if (t !== (type||'')) return;
const sk = computeSlotKey(item);
if (MC_SLOTS[sk]) return;
if (SLOT_COL[sk] && slotEntryMatches(item, SLOT_COL[sk])) return;
if ((getIconSig(item)||'') !== (sig||'')) return;
const box = pickBoxFrom(item);
const rect = box && box.getBoundingClientRect();
if (rect && isSquareish(rect) && boxHasIconFromItem(item)){
const val = Number(item.dataset.dfScrapValue) || GREEN_MAX;
paintBox(box, val, col);
}
});
});
}
/* ---------- debounced observer ---------- */
let reapplyScheduled=false;
function scheduleReapply(){
if (reapplyScheduled) return;
reapplyScheduled=true;
requestAnimationFrame(()=>{ reapplyScheduled=false; reapplyAll(); });
}
const mo = new MutationObserver(()=>{ try{ scheduleReapply(); }catch{} });
/* ---------- scan helpers / quick scan ---------- */
function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }
function dispatchMouse(el, type, x, y){
const ev=new MouseEvent(type,{bubbles:true,cancelable:true,clientX:x,clientY:y,view:window});
try{ el.dispatchEvent(ev); }catch{}
}
async function hoverForTooltip(el, baseWait=20, duringScan=false){
const r=el.getBoundingClientRect();
if(!r || r.width===0 || r.height===0) return null;
let x=Math.max(2,Math.min(window.innerWidth-2,Math.floor(r.left+r.width*0.5)));
let y=Math.max(2,Math.min(window.innerHeight-2,Math.floor(r.top +r.height*0.6)));
const target = duringScan ? el : (document.elementFromPoint(x,y) || el);
dispatchMouse(target,'mouseover',x,y);
dispatchMouse(target,'mousemove',x,y);
await sleep(baseWait);
let best=liveTooltipHTML();
if(!best || !isMastercrafted(best)){
for(let k=0;k<2;k++){
y=Math.min(window.innerHeight-2, y+1+k);
dispatchMouse(target,'mousemove',x,y);
await sleep(baseWait+8);
const h=liveTooltipHTML();
if ((h && (!best || h.length>best.length)) || isMastercrafted(h)) best=h;
if (isMastercrafted(best)) break;
}
}
return { html: best };
}
async function confirmMastercrafted(el, baseWait=20){
const first = await hoverForTooltip(el, baseWait, true);
const tip1 = first && first.html;
const isMC1 = !!tip1 && isMastercrafted(tip1);
if (!isMC1) return { tip: tip1, master:false };
const tip2 = liveTooltipHTML();
const isMC2 = !!tip2 && isMastercrafted(tip2);
const s1 = parseScrapStrict(tip1);
const s2 = parseScrapStrict(tip2);
const ok = (s1==null || s2==null) ? true : (s1===s2);
return { tip: tip2||tip1, master: (isMC1 && isMC2 && ok) };
}
let scanning=false;
async function quickScan(updateLabel){
if (!ENABLED || scanning) return;
scanning=true; SCANNING=true;
const items = Array.from(document.querySelectorAll('div.item')).filter(it=>{
const b=pickBoxFrom(it); const r=b&&b.getBoundingClientRect();
return r && r.width>0 && r.height>0 && r.bottom>0 && r.top<window.innerHeight;
});
const MAX = Math.min(items.length, 120);
const HOVER=20, PAUSE=2;
for(let i=0;i<MAX;i++){
const it=items[i];
try{
const { tip, master } = await confirmMastercrafted(it, HOVER);
if (tip){
const scrap = parseScrapStrict(tip);
if (scrap!=null){
const box = pickBoxFrom(it);
if (box && boxHasIconFromItem(it)){
it.dataset.dfScrapValue = String(scrap);
if (master){
rememberMaster(it);
paintBox(box, scrap, 'blue');
const sk = it.dataset.dfSlotKey || computeSlotKey(it);
if (SLOT_COL[sk]){ delete SLOT_COL[sk]; saveSlot(); }
}else{
const col = colorFor(scrap);
paintBox(box, scrap, col);
if (col!=='none') rememberSlotColor(it, col, scrap);
const sig = getIconSig(it) || '';
const type = (it.dataset && it.dataset.type) ? it.dataset.type.toLowerCase() : '';
if (col!=='none' && type){
const tkey = `${type}||${sig}`;
if (COLOR_TYPES[tkey] !== col){
COLOR_TYPES[tkey] = col;
jset(localStorage, LS_KEY_TYPES_V2, COLOR_TYPES);
}
}
}
}
}
}
}catch{}
if (updateLabel) updateLabel(`Scanning ${i+1}/${MAX}…`);
await sleep(PAUSE);
}
reapplyAll();
SCANNING=false; scanning=false;
if (updateLabel) updateLabel('Scan Items');
}
/* ---------- toggle + boot UI ---------- */
function toggleEnabled(btn){
ENABLED=!ENABLED; saveEnabled(ENABLED);
if (ENABLED){
try{ if (document.body) mo.observe(document.body, {subtree:true, childList:true}); }catch{}
reapplyAll();
}else{
try{ mo.disconnect(); }catch{}
document.querySelectorAll('[data-df-scrap-painted="1"]').forEach(el=>{
el.style.outline=''; el.style.boxShadow=''; el.style.borderRadius='';
el.removeAttribute('data-df-scrap-painted'); el.removeAttribute('data-df-scrap-color');
});
}
if (btn) btn.textContent = ENABLED ? 'Scrap Highlight: ON' : 'Scrap Highlight: OFF';
}
document.addEventListener('DOMContentLoaded', ()=>{
try{ if (ENABLED && document.body) mo.observe(document.body, {subtree:true, childList:true}); }catch{}
if (ENABLED) reapplyAll();
// Recalculate after player moves items
['dragstart','dragend','drop','mouseup'].forEach(ev =>
document.addEventListener(ev, ()=>{ scheduleReapply(); }, true)
);
const wrap=document.createElement('div');
Object.assign(wrap.style,{
position:'fixed', left:'8px', bottom:'8px', zIndex:2147483647,
display:'flex', gap:'6px', alignItems:'center'
});
const toggleBtn=document.createElement('button');
toggleBtn.textContent = ENABLED ? 'Scrap Highlight: ON' : 'Scrap Highlight: OFF';
Object.assign(toggleBtn.style,{
font:'12px system-ui, Arial, sans-serif', padding:'6px 10px',
background:'#111', color:'#fff', border:'1px solid rgba(255,255,255,.25)',
borderRadius:'6px', opacity:'0.9', cursor:'pointer'
});
toggleBtn.addEventListener('mouseenter',()=>toggleBtn.style.opacity='1');
toggleBtn.addEventListener('mouseleave',()=>toggleBtn.style.opacity='0.9');
toggleBtn.addEventListener('click',()=>toggleEnabled(toggleBtn));
const scanBtn=document.createElement('button');
scanBtn.textContent='Scan Items';
Object.assign(scanBtn.style,{
font:'12px system-ui, Arial, sans-serif', padding:'6px 10px',
background:'#153b7a', color:'#fff', border:'1px solid rgba(255,255,255,.25)',
borderRadius:'6px', opacity:'0.9', cursor:'pointer'
});
const setScanLabel=(t)=>{ scanBtn.textContent=t; };
scanBtn.addEventListener('mouseenter',()=>scanBtn.style.opacity='1');
scanBtn.addEventListener('mouseleave',()=>scanBtn.style.opacity='0.9');
scanBtn.addEventListener('click', async ()=>{
if (scanning) return;
const prev=scanBtn.textContent;
setScanLabel('Scanning…');
scanBtn.disabled=true;
try{ await quickScan(setScanLabel); } finally { scanBtn.disabled=false; setScanLabel(prev); }
});
wrap.appendChild(toggleBtn);
wrap.appendChild(scanBtn);
document.body.appendChild(wrap);
});
})();