💫 Points Maker (Optimized + Abroad Color + Flights)

Optimized: Points-style PDA panel showing Torn display + abroad stock. Collapsible. Abroad stock color-coded. Safe polling, debounced layout, select plane.

2025/11/26のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         💫 Points Maker (Optimized + Abroad Color + Flights)
// @namespace    http://tampermonkey.net/
// @version      1.2.7
// @description  Optimized: Points-style PDA panel showing Torn display + abroad stock. Collapsible. Abroad stock color-coded. Safe polling, debounced layout, select plane.
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  // CONFIG
  const PANEL_ID = 'points_maker_pda';
  const POLL_INTERVAL_MS = 45 * 1000;
  const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
  const PROM_URL = 'https://api.prombot.co.uk/api/travel';
  const SPECIAL_DRUG = 'Xanax';
  const MAX_ABROAD = { flowers: 50, plushies: 50, drugs: 50 };
  const PLANES = ['Plane A', 'Plane B', 'Plane C']; // example planes

  // DATA
  const FLOWERS = {
    "Dahlia": { short: "Dahlia", loc: "MX 🇲🇽", country: "Mexico" },
    "Orchid": { short: "Orchid", loc: "HW 🏝️", country: "Hawaii" },
    "African Violet": { short: "Violet", loc: "SA 🇿🇦", country: "South Africa" },
    "Cherry Blossom": { short: "Cherry", loc: "JP 🇯🇵", country: "Japan" },
    "Peony": { short: "Peony", loc: "CN 🇨🇳", country: "China" },
    "Ceibo Flower": { short: "Ceibo", loc: "AR 🇦🇷", country: "Argentina" },
    "Edelweiss": { short: "Edelweiss", loc: "CH 🇨🇭", country: "Switzerland" },
    "Crocus": { short: "Crocus", loc: "CA 🇨🇦", country: "Canada" },
    "Heather": { short: "Heather", loc: "UK 🇬🇧", country: "United Kingdom" },
    "Tribulus Omanense": { short: "Tribulus", loc: "AE 🇦🇪", country: "UAE" },
    "Banana Orchid": { short: "Banana", loc: "KY 🇰🇾", country: "Cayman Islands" }
  };

  const PLUSHIES = {
    "Sheep Plushie": { short: "Sheep", loc: "B.B 🏪", country: "Torn City" },
    "Teddy Bear Plushie": { short: "Teddy", loc: "B.B 🏪", country: "Torn City" },
    "Kitten Plushie": { short: "Kitten", loc: "B.B 🏪", country: "Torn City" },
    "Jaguar Plushie": { short: "Jaguar", loc: "MX 🇲🇽", country: "Mexico" },
    "Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦", country: "Canada" },
    "Nessie Plushie": { short: "Nessie", loc: "UK 🇬🇧", country: "United Kingdom" },
    "Red Fox Plushie": { short: "Fox", loc: "UK 🇬🇧", country: "United Kingdom" },
    "Monkey Plushie": { short: "Monkey", loc: "AR 🇦🇷", country: "Argentina" },
    "Chamois Plushie": { short: "Chamois", loc: "CH 🇨🇭", country: "Switzerland" },
    "Panda Plushie": { short: "Panda", loc: "CN 🇨🇳", country: "China" },
    "Lion Plushie": { short: "Lion", loc: "SA 🇿🇦", country: "South Africa" },
    "Camel Plushie": { short: "Camel", loc: "AE 🇦🇪", country: "UAE" },
    "Stingray Plushie": { short: "Stingray", loc: "KY 🇰🇾", country: "Cayman Islands" }
  };

  const COUNTRY_NAME_TO_CODE = {
    'JAPAN': 'JAP', 'MEXICO': 'MEX', 'CANADA': 'CAN', 'CHINA': 'CHI', 'UNITED KINGDOM': 'UNI',
    'ARGENTINA': 'ARG', 'SWITZERLAND': 'SWI', 'HAWAII': 'HAW', 'UAE': 'UAE', 'CAYMAN ISLANDS': 'CAY',
    'SOUTH AFRICA': 'SOU', 'S.A': 'SOU', 'SA': 'SOU', 'TORN': 'BB', 'B.B': 'BB'
  };

  function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m])); }
  function colorForPercent(value, max) { if (!max || max === 0) return '#bdbdbd'; const pct = (value / max) * 100; if (pct >= 75) return '#00c853'; if (pct >= 40) return '#3399ff'; return '#ff1744'; }

  GM_addStyle(`
    #${PANEL_ID} { position: fixed; top: 42px; left: 100px; width: 280px; background: #0b0b0b; color: #eaeaea; font-family: "DejaVu Sans Mono", monospace; font-size: 9px; border: 1px solid #444; border-radius: 6px; z-index: 999999; box-shadow: 0 6px 16px rgba(0,0,0,0.5); max-height: 65vh; overflow-y: auto; line-height: 1.1; }
    #${PANEL_ID} .header { background: #121212; padding: 4px 6px; cursor: pointer; font-weight:700; font-size:10px; border-bottom:1px solid #333; user-select:none; display:flex; align-items:center; gap:6px; }
    #${PANEL_ID} .controls { padding:6px; display:flex; gap:6px; flex-wrap:wrap; }
    #${PANEL_ID} .controls button, #${PANEL_ID} .controls select { font-size:9px; padding:2px 6px; background:#171717; color:#eaeaea; border:1px solid #333; border-radius:3px; cursor:pointer; }
    #${PANEL_ID} .controls button:hover, #${PANEL_ID} .controls select:hover { background:#222; }
    #${PANEL_ID} .summary-line { font-weight:700; margin:6px; font-size:10px; color:#dfe7ff; }
    #${PANEL_ID} .low-line { color:#ff4d4d; font-weight:700; margin:6px; font-size:10px; }
    #${PANEL_ID} .group-title { font-weight:700; margin:6px 6px 2px 6px; font-size:9.5px; }
    #${PANEL_ID} ul.item-list { margin:4px 6px 8px 12px; padding:0; list-style:none; }
    #${PANEL_ID} li.item-row { display:flex; align-items:center; gap:6px; padding:2px 0; white-space:nowrap; }
    #${PANEL_ID} .item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
    #${PANEL_ID} .item-total { flex:0 0 36px; text-align:right; color:#cfe8c6; }
    #${PANEL_ID} .item-av { flex:0 0 60px; text-align:right; color:#f7b3b3; }
    #${PANEL_ID} .item-loc { flex:0 0 36px; text-align:right; color:#bcbcbc; font-size:8.5px; }
    #${PANEL_ID} #tc_status { font-size:9px; color:#bdbdbd; margin:6px; }
    .stock-green{ color:#00c853 !important; } .stock-orange{ color:#ff9800 !important; } .stock-red{ color:#ff1744 !important; } .stock-gray{ color:#9ea6b3 !important; }
  `);

  // Build required lists
  function buildRequiredList(mapObj) {
    const fullNames = Object.keys(mapObj);
    const shortNames = fullNames.map(fn => mapObj[fn].short);
    const locByShort = {}; const countryByShort = {};
    fullNames.forEach(fn => { const s = mapObj[fn].short; locByShort[s] = mapObj[fn].loc; countryByShort[s] = mapObj[fn].country; });
    return { fullNames, shortNames, locByShort, countryByShort };
  }
  const flowersReq = buildRequiredList(FLOWERS);
  const plushReq = buildRequiredList(PLUSHIES);

  // UI
  let statusEl, summaryEl, contentEl, planeSelect;
  function buildUI() {
    if (document.getElementById(PANEL_ID)) return;
    const root = document.createElement('div');
    root.id = PANEL_ID;
    root.innerHTML = `
      <div id="tc_header" class="header">▶ 💫 Points Maker</div>
      <div id="tc_content_wrapper">
        <div id="tc_controls" class="controls">
          <button id="tc_refresh">Refresh</button>
          <button id="tc_setkey">Set API Key</button>
          <button id="tc_resetkey">Reset Key</button>
          <select id="tc_plane">${PLANES.map(p => `<option value="${p}">${p}</option>`).join('')}</select>
        </div>
        <div id="tc_status">Waiting for API key...</div>
        <div id="tc_summary"></div>
        <div id="tc_content"></div>
      </div>
    `;
    document.body.appendChild(root);

    const headerEl = root.querySelector('#tc_header');
    const contentWrapper = root.querySelector('#tc_content_wrapper');
    let collapsed = GM_getValue(`${PANEL_ID}-collapsed`, false);
    function updateCollapse() {
      headerEl.textContent = (collapsed ? '▶ ' : '▼ ') + '💫 Points Maker';
      contentWrapper.style.display = collapsed ? 'none' : 'block';
    }
    updateCollapse();
    headerEl.addEventListener('click', () => { collapsed = !collapsed; GM_setValue(`${PANEL_ID}-collapsed`, collapsed); updateCollapse(); });

    root.querySelector('#tc_refresh').addEventListener('click', () => refreshAll(true));
    root.querySelector('#tc_setkey').addEventListener('click', () => askKey(true));
    root.querySelector('#tc_resetkey').addEventListener('click', () => {
      GM_setValue('tornAPIKey', null); apiKey = null; if (statusEl) statusEl.textContent = 'Key cleared. Click Set API Key.'; if (summaryEl) summaryEl.innerHTML=''; if (contentEl) contentEl.innerHTML=''; stopPolling();
    });

    statusEl = root.querySelector('#tc_status');
    summaryEl = root.querySelector('#tc_summary');
    contentEl = root.querySelector('#tc_content');
    planeSelect = root.querySelector('#tc_plane');
  }

  buildUI();

  // API helper
  function gmGetJson(url, timeout = 14000) {
    return new Promise((resolve, reject) => {
      try {
        GM_xmlhttpRequest({ method:'GET', url, timeout,
          onload: res => { try { resolve(JSON.parse(res.response || res.responseText)); } catch(e){reject(e);} },
          onerror: err=>reject(err), ontimeout:()=>reject(new Error('timeout'))});
      } catch(e){reject(e);}
    });
  }

  // Aggregate inventory
  function aggregateFromApiResponse(data) {
    const items={}; const push=src=>{if(!src)return; Object.values(src).forEach(e=>{if(!e)return; const name=e.name||e.item_name||e.title||e.item||null; if(!name)return; const qty=Number(e.quantity??e.qty??e.amount??1)||0; items[name]=(items[name]||0)+qty; });};
    push(data.display); push(data.inventory); return items;
  }

  async function fetchTornDisplayInventory() {
    const key = GM_getValue('tornAPIKey', null); if(!key) return null;
    try { return aggregateFromApiResponse(await gmGetJson(`https://api.torn.com/user/?selections=display,inventory&key=${encodeURIComponent(key)}`)); } catch(e){console.warn(e); return null;}
  }

  function fetchDisplayViaDOM() {
    const map={}; document.querySelectorAll('.display-item, .display_case_item, .dcItem, .item-wrap .item').forEach(el=>{
      let name='', qty=0; const nameEl=el.querySelector('.item-name, .name, .title')||el.querySelector('a')||el;
      if(nameEl) name=(nameEl.innerText||'').trim();
      const qtyEl=el.querySelector('.item-amount, .count, .qty, .quantity, .item-qty'); if(qtyEl) qty=parseInt((qtyEl.innerText||'').replace(/\D/g,''))||0;
      if(name) map[name]=(map[name]||0)+qty;
    }); return map;
  }

  function parseYata(yataData){ const map={}; if(!yataData?.stocks)return map; for(const [code,obj] of Object.entries(yataData.stocks)){ const arr=Array.isArray(obj.stocks)?obj.stocks:[]; const m={}; arr.forEach(it=>{if(it?.name)m[it.name]=Number(it.quantity??it.qty??0)||0;}); map[String(code).toUpperCase()]=m;} return map; }
  function parseProm(promData){ const map={}; if(!promData)return map; for(const [k,v] of Object.entries(promData)){ if(!v)continue; const up=String(k).trim().toUpperCase(); let code=COUNTRY_NAME_TO_CODE[up]||up; const m={}; if(Array.isArray(v.stocks)) v.stocks.forEach(it=>{if(it?.name)m[it.name]=Number(it.quantity??it.qty??0)||0;}); else Object.entries(v).forEach(([ik,iv])=>{ if(typeof iv==='object' && ('quantity' in iv||'qty' in iv||'amount' in iv)) m[ik]=Number(iv.quantity??iv.qty??iv.amount??0)||0; else if(typeof iv==='number'||!isNaN(Number(iv))) m[ik]=Number(iv)||0;}); map[code]=m;} return map; }
  function sumAcrossCountriesFor(itemName, parsedMap){ let total=0; for(const c of Object.keys(parsedMap)) total+=Number(parsedMap[c][itemName]||0); return total; }

  function countsForReq(itemsAgg, req, mapObj){ const counts={}; req.shortNames.forEach(s=>counts[s]=0); req.fullNames.forEach(fn=>{const short=mapObj[fn].short; counts[short]=(counts[short]||0)+(itemsAgg[fn]||0);}); return counts; }
  function calcSetsAndRemainderFromCounts(counts, shortNames){ const countsArr=shortNames.map(n=>counts[n]||0); const sets=countsArr.length?Math.min(...countsArr):0; const remainder={}; shortNames.forEach(n=>remainder[n]=Math.max(0,(counts[n]||0)-sets)); return {sets,remainder}; }
  function findLowest(remainder, locMap, countryMap){ const keys=Object.keys(remainder); if(!keys.length) return null; let min=Infinity; keys.forEach(k=>{if(remainder[k]<min)min=remainder[k];}); const allEqual=keys.every(k=>remainder[k]===min); if(allEqual) return null; const key=keys.find(k=>remainder[k]===min); return {short:key, rem:min, loc:locMap[key]||'', country:countryMap[key]||''}; }

  function renderUI(itemsAgg, yataRaw, promRaw){
    if(!contentEl)return;
    const flowerTotals=countsForReq(itemsAgg,flowersReq,FLOWERS);
    const plushTotals=countsForReq(itemsAgg,plushReq,PLUSHIES);
    const fCalc=calcSetsAndRemainderFromCounts(flowerTotals,flowersReq.shortNames);
    const pCalc=calcSetsAndRemainderFromCounts(plushTotals,plushReq.shortNames);
    const totalSets=fCalc.sets+pCalc.sets;
    const totalPoints=totalSets*10;
    const yataMap=parseYata(yataRaw), promMap=parseProm(promRaw);
    const flowerDisplay=fCalc.remainder, plushDisplay=pCalc.remainder;

    function pickAvFor(fullName, category){
      const yataSum=sumAcrossCountriesFor(fullName,yataMap), promSum=sumAcrossCountriesFor(fullName,promMap);
      let val=0, src=''; if(yataSum>0){val=yataSum; src='Y';} else if(promSum>0){val=promSum; src='P';} else if(yataRaw&&Object.keys(yataRaw).length){val=yataSum; src='Y';} else if(promRaw&&Object.keys(promRaw).length){val=promSum; src='P';} else {val=null; src=null;}
      let col='#f7b3b3'; if(val!=null&&category&&MAX_ABROAD[category]) col=colorForPercent(val,MAX_ABROAD[category]); return {val, col, src};}

    const lines=[]; lines.push(`<div class="summary-line">Sets:${totalSets} | Points:${totalPoints}</div>`);

    // Flowers
    lines.push(`<div class="group-title">Flowers</div><ul class="item-list">`);
    flowersReq.shortNames.forEach(s=>{
      const loc=flowersReq.locByShort[s], rem=flowerDisplay[s]||0;
      const av=pickAvFor(s,'flowers'); const color=rem>0?'stock-red':'stock-green';
      lines.push(`<li class="item-row"><span class="item-name">${escapeHtml(s)}</span><span class="item-total">${rem}</span><span class="item-av" style="color:${av.col}">${av.val||''}</span><span class="item-loc">${escapeHtml(loc)}</span></li>`);
    });
    lines.push('</ul>');

    // Plushies
    lines.push(`<div class="group-title">Plushies</div><ul class="item-list">`);
    plushReq.shortNames.forEach(s=>{
      const loc=plushReq.locByShort[s], rem=plushDisplay[s]||0;
      const av=pickAvFor(s,'plushies'); const color=rem>0?'stock-red':'stock-green';
      lines.push(`<li class="item-row"><span class="item-name">${escapeHtml(s)}</span><span class="item-total">${rem}</span><span class="item-av" style="color:${av.col}">${av.val||''}</span><span class="item-loc">${escapeHtml(loc)}</span></li>`);
    });
    lines.push('</ul>');

    contentEl.innerHTML=lines.join('');
  }

  // API KEY
  let apiKey = GM_getValue('tornAPIKey', null);
  function askKey(force) {
    const newKey = prompt('Enter Torn API key:', apiKey||'');
    if(newKey) { apiKey=newKey; GM_setValue('tornAPIKey', apiKey); if(statusEl)statusEl.textContent='API Key set!'; refreshAll(true); startPolling(); }
  }

  // POLLING
  let pollTimer=null; let isRefreshing=false;
  function startPolling(){ if(pollTimer)return; pollTimer=setTimeout(() => refreshAll(true), 200);}
  function stopPolling(){ if(!pollTimer)return; clearTimeout(pollTimer); pollTimer=null;}

  async function refreshAll(force=false){
    if(isRefreshing&&!force)return;
    isRefreshing=true;
    if(statusEl)statusEl.textContent='Fetching data...';
    const localItems = fetchDisplayViaDOM();
    const [tornItems,yataRaw,promRaw]=await Promise.all([fetchTornDisplayInventory(), gmGetJson(YATA_URL).catch(()=>null), gmGetJson(PROM_URL).catch(()=>null)]);
    const aggItems = {...localItems, ...tornItems};
    renderUI(aggItems,yataRaw,promRaw);
    if(statusEl)statusEl.textContent='Updated at '+new Date().toLocaleTimeString();
    isRefreshing=false;
    pollTimer=setTimeout(()=>refreshAll(),POLL_INTERVAL_MS);
  }

  if(!apiKey) setTimeout(()=>askKey(true),300); else {startPolling(); refreshAll(true);}
})();