Market Resale Helper TornW3B

Calculate suggested resale prices (quantile, median, cluster) on TornW3B

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);

})();