您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculate suggested resale prices (quantile, median, cluster) on TornW3B
// ==UserScript== // @name Market Resale Helper TornW3B // @namespace http://tampermonkey.net/ // @version 2025-08-26 v2 // @description Calculate suggested resale prices (quantile, median, cluster) on TornW3B // @match *://weav3r.dev/item/* // @icon https://www.google.com/s2/favicons?sz=64&domain=weav3r.dev // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; console.log("✅ Market Helper v6 started"); // --- Config defaults --- let BULK_QTY_MIN = 20; let BULK_PCT_MIN = 0.10; // --- Utils --- const fmt = (v) => (v !== null && v !== undefined) ? v.toLocaleString() : "—"; const diffPct = (p, marketMin) => (p && marketMin) ? ((p - marketMin) / marketMin * 100).toFixed(1) + "%" : "—"; // --- Pricing methods --- function calcQuantile(offers, fraction) { offers.sort((a,b) => a.price - b.price); const total = offers.reduce((s,o) => s + o.qty, 0); let cumulative = 0; const threshold = total * fraction; for (let o of offers) { cumulative += o.qty; if (cumulative >= threshold) return o.price - 1; } return offers[offers.length-1].price; } function calcMedian(offers) { offers.sort((a,b) => a.price - b.price); const total = offers.reduce((s,o) => s + o.qty, 0); const half = Math.floor(total / 2); let cumulative = 0; for (let o of offers) { cumulative += o.qty; if (cumulative >= half) return o.price; } return offers[Math.floor(offers.length/2)].price; } function calcCluster(offers) { offers.sort((a,b) => a.price - b.price); let bestPrice = offers[0].price; let bestCount = 0; for (let i = 0; i < offers.length; i++) { const p = offers[i].price; const upper = p * 1.05; let count = 0; for (let j = i; j < offers.length; j++) { if (offers[j].price <= upper) count += offers[j].qty; else break; } if (count > bestCount) { bestCount = count; bestPrice = p; } } return bestPrice - 1; } function calcClusterCapped(offers, marketMin) { let cluster = calcCluster(offers); let cap = Math.round(marketMin * 1.5); return Math.min(cluster, cap); } // --- Bulk detection --- function detectBulkAnchor(offers, marketMin) { const totalQty = offers.reduce((s,o) => s + o.qty, 0); offers.sort((a,b) => a.price - b.price); for (let i = 0; i < offers.length; i++) { let bandQty = 0; const p = offers[i].price; const upper = p * 1.05; for (let j = i; j < offers.length; j++) { if (offers[j].price <= upper) bandQty += offers[j].qty; else break; } if (bandQty >= BULK_QTY_MIN || bandQty >= totalQty * BULK_PCT_MIN) { if (p <= marketMin * 1.5) { return {price: p, qty: bandQty}; } } } return null; } // --- Parse offers --- function parseOffers() { let offers = []; let inferredMarkets = []; // grab all grid rows let rows = document.querySelectorAll("div.grid"); rows.forEach((row, idx) => { let cols = row.querySelectorAll(":scope > div"); if (cols.length === 5) { // skip header row (first one with "Player/Qty/Price") if (idx === 0) return; // qty let qty = parseInt(cols[1].innerText.replace(/[^0-9]/g, ""), 10); // price let priceSpan = cols[2].querySelector("span"); let priceText = priceSpan ? priceSpan.textContent.trim() : "0"; let price = parseInt(priceText.replace(/[^0-9]/g, ""), 10); // vsMarket let vsSpan = cols[3].querySelector("span"); let vsText = vsSpan ? vsSpan.textContent.trim() : "0"; // "-12%" or "+25%" let vsMarket = parseFloat(vsText.replace("%","").replace("+","")) || 0; if (!isNaN(price) && !isNaN(qty)) { let inferred = price / (1 + vsMarket/100); inferredMarkets.push(inferred); console.log("Row parsed:", { qty, price, rawVs: vsText, vsMarket, inferredMarket: inferred.toFixed(2) }); offers.push({price, qty, vsMarket}); } } }); // median inferred market inferredMarkets.sort((a,b) => a-b); let marketMin = inferredMarkets.length > 0 ? Math.round(inferredMarkets[Math.floor(inferredMarkets.length/2)]) : null; console.log("✅ Parsed offers:", offers.length, "| MarketMin:", marketMin); return {offers, marketMin}; } // --- Trim dataset --- function trimOffers(offers) { offers.sort((a,b) => a.price - b.price); return offers.filter(o => o.vsMarket <= 200); // remove >+100% vs market } // --- Auto-scroll --- function autoScrollTable(callback) { let container = document.querySelector("div.overflow-y-auto"); if (!container) { callback(); return; } let lastCount = 0, tries = 0; const interval = setInterval(() => { container.scrollBy(0, 800); let rows = document.querySelectorAll("div.grid"); let dataRows = Array.from(rows).filter(r => r.querySelectorAll(":scope > div").length === 5); if (dataRows.length > lastCount) { lastCount = dataRows.length; tries = 0; } else tries++; if (tries > 5) { clearInterval(interval); container.scrollTo(0, 0); console.log("✅ Auto-scroll finished with", dataRows.length, "rows"); callback(); } }, 500); } // --- Show results --- function injectResult() { let parsed = parseOffers(); let offers = parsed.offers; let marketMin = parsed.marketMin; if (offers.length === 0 || !marketMin) return; let relevant = trimOffers(offers); console.log("📊 Relevant offers:", relevant.length, "of", offers.length); let fast = calcQuantile(relevant, 0.1); let medium = calcQuantile(relevant, 0.3); let slow = calcQuantile(relevant, 0.6); let median = calcMedian(relevant); let cluster = calcCluster(relevant); let capped = calcClusterCapped(relevant, marketMin); let bulk = detectBulkAnchor(relevant, marketMin); let final = bulk ? bulk.price : capped; let avgPrice = Math.round( relevant.reduce((s,o) => s + o.price * o.qty, 0) / relevant.reduce((s,o) => s + o.qty, 0) ); let box = document.getElementById("market-helper-box"); if (!box) { box = document.createElement("div"); box.id = "market-helper-box"; box.style.cssText = ` padding:10px;margin:10px 0; border:2px solid #4caf50;background:#e8f5e9; font-size:14px;border-radius:8px; `; let container = document.querySelector("div.overflow-x-auto"); if (container) container.prepend(box); else document.body.prepend(box); // controls let controls = document.createElement("div"); controls.style.margin = "5px 0"; controls.innerHTML = ` Bulk min qty: <input id="bulkQtyInput" type="number" value="${BULK_QTY_MIN}" style="width:60px"> Bulk min %: <input id="bulkPctInput" type="number" value="${BULK_PCT_MIN*100}" style="width:60px">% `; box.before(controls); document.getElementById("bulkQtyInput").addEventListener("change", e=>{ BULK_QTY_MIN = parseInt(e.target.value,10) || 20; injectResult(); }); document.getElementById("bulkPctInput").addEventListener("change", e=>{ BULK_PCT_MIN = (parseFloat(e.target.value)||10)/100; injectResult(); }); } box.innerHTML = ` <b>💡 Smart Resale Analysis (${relevant.length}/${offers.length} offers):</b><br> ⭐ Final Recommended: <b style="color:darkgreen;font-size:16px">${fmt(final)}</b> (${diffPct(final, marketMin)} vs market)<br> Bulk Anchor: <b>${bulk ? fmt(bulk.price)+" ("+bulk.qty+" items)" : "—"}</b><br> <hr> Fast (10%): <b>${fmt(fast)}</b> (${diffPct(fast, marketMin)})<br> Medium (30%): <b>${fmt(medium)}</b> (${diffPct(medium, marketMin)})<br> Slow (60%): <b>${fmt(slow)}</b> (${diffPct(slow, marketMin)})<br> Median: <b>${fmt(median)}</b> (${diffPct(median, marketMin)})<br> Cluster: <b>${fmt(cluster)}</b> (${diffPct(cluster, marketMin)})<br> Cluster+Cap: <b>${fmt(capped)}</b> (${diffPct(capped, marketMin)})<br> <hr> Market min (from vsMarket): <b>${fmt(marketMin)}</b><br> Average (trimmed): <b>${fmt(avgPrice)}</b> `; } // --- Runner --- function runFullCycle() { autoScrollTable(() => injectResult()); } setTimeout(runFullCycle, 20000); setInterval(runFullCycle, 300000); })();