您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Director's helper for Torn companies (ES5 hard-compat): popularity, environment, effectiveness, profit tips, per-role breakdown, retail pricing advisor. No async/await. Uses GM_xmlhttpRequest. Observer ignores panel & pauses while typing.
// ==UserScript== // @name Assistant Director // @namespace https://greasyfork.org/users/your-username // @version 0.3.6 // @description Director's helper for Torn companies (ES5 hard-compat): popularity, environment, effectiveness, profit tips, per-role breakdown, retail pricing advisor. No async/await. Uses GM_xmlhttpRequest. Observer ignores panel & pauses while typing. // @author YourName // @match https://www.torn.com/* // @license MIT // @grant GM_xmlhttpRequest // @connect api.torn.com // ==/UserScript== (function () { 'use strict'; var CFG = { debug: false, panelId: 'assistant-director-panel', ls: { itemCosts: 'assistantDirector.itemCosts', collapse: 'assistantDirector.panelCollapsed', apiKey: 'assistantDirector.apiKey', lowEffThreshold: 'assistantDirector.lowEffThreshold', warnEffThreshold: 'assistantDirector.warnEffThreshold' }, retail: { targetSellThroughDaily: 0.15, elasticityStepPctPerStep: 7, minFloorMarginPct: 5, maxNudgePct: 20, currencySymbol: '$' }, staff: { lowEffDefault: 60, warnEffDefault: 75, inactivePhrases: ['inactive', 'days', 'weeks', 'month'] }, apiThrottleMs: 10000 }; // ---------- Utilities ---------- function coalesce(a, b) { return (a !== undefined && a !== null) ? a : b; } function log(){ if (CFG.debug){ var a=[].slice.call(arguments); a.unshift('[AD v0.3.6]'); console.log.apply(console,a);} } function sleep(ms, cb){ return setTimeout(cb, ms); } function clamp(v, lo, hi){ return Math.min(hi, Math.max(lo, v)); } function qs(sel, root){ return (root||document).querySelector(sel); } function qsa(sel, root){ return [].slice.call((root||document).querySelectorAll(sel)); } function getText(el){ return el ? el.textContent.trim() : ''; } function normalizeSpaces(str){ return (str+'').replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g,' ') .replace(/\s+/g,' ').trim(); } function parseNumber(str){ if (str==null) return null; var s = normalizeSpaces(str); var neg = /\(.*\)/.test(s); var n = parseFloat(s.replace(/[,$£€%]/g,'').replace(/[^\d.\-]/g,'')); if (isNaN(n)) return null; return neg ? -n : n; } function percent(v,d){ return v==null ? '—' : (v.toFixed(d||0)+'%'); } function money(v,s){ return v==null ? '—' : ((s||CFG.retail.currencySymbol)+new Intl.NumberFormat().format(v)); } var store = { get: function(key,fallback){ try{ var v = localStorage.getItem(key); return v==null ? (fallback===undefined?null:fallback) : JSON.parse(v); } catch(e){ return (fallback===undefined?null:fallback); } }, set: function(key,val){ localStorage.setItem(key, JSON.stringify(val)); }, del: function(key){ localStorage.removeItem(key); } }; function detectCurrencySymbol(){ var m = (document.body.innerText.match(/([£$€])\s?\d[\d,]*\.?\d*/) || [])[1]; return m || CFG.retail.currencySymbol; } CFG.retail.currencySymbol = detectCurrencySymbol(); function isTypingInPanel(){ var panel = document.getElementById(CFG.panelId); var ae = document.activeElement; return !!(panel && ae && panel.contains(ae) && (/^(INPUT|TEXTAREA)$/).test(ae.tagName)); } // ---------- HTTP ---------- function httpGetJson(url){ return new Promise(function(resolve,reject){ try{ GM_xmlhttpRequest({ method:'GET', url:url, headers:{'Accept':'application/json'}, onload:function(res){ try{ if (res.status<200 || res.status>=300) return reject(new Error('HTTP '+res.status)); resolve(JSON.parse(res.responseText)); }catch(e){ reject(e); } }, onerror:function(){ reject(new Error('Network error')); }, ontimeout:function(){ reject(new Error('Request timeout')); } }); }catch(e){ reject(e); } }); } // ---------- Torn API ---------- function tornApiFetch(path,key){ var url = 'https://api.torn.com/' + path + '&key=' + encodeURIComponent(key); return httpGetJson(url).then(function(data){ if (data && data.error) throw new Error(data.error.error || 'API error'); return data; }); } function safeRevenue(fin){ return (fin.revenue_daily !== undefined ? fin.revenue_daily : (fin.revenueDay !== undefined ? fin.revenueDay : null)); } function safeProfit(fin){ return (fin.profit_daily !== undefined ? fin.profit_daily : (fin.profitDay !== undefined ? fin.profitDay : null)); } function computeMargin(fin){ var rev = safeRevenue(fin); var prof = safeProfit(fin); if (rev==null || rev===0 || prof==null) return null; return (prof / rev) * 100; } function getCompanyViaApi(apiKey){ return tornApiFetch('company/?selections=profile,employees,financials,upgrades,stock,newsales,settings', apiKey) .then(function(data){ var c = data.company || data; var fin = c.financials || {}; var summary = { popularity: (c.popularity !== undefined ? c.popularity : (c.company_popularity !== undefined ? c.company_popularity : null)), environment: (c.environment !== undefined ? c.environment : (c.company_environment !== undefined ? c.company_environment : null)), effectiveness: (c.effectiveness !== undefined ? c.effectiveness : (c.employees && typeof c.employees.effectiveness === 'number' ? c.employees.effectiveness : null)), income: safeRevenue(fin), expenses: (fin.expenses_daily !== undefined ? fin.expenses_daily : (fin.expensesDay !== undefined ? fin.expensesDay : null)), profit: safeProfit(fin), margin: computeMargin(fin), vacancies: (c.positions_open !== undefined ? c.positions_open : (c.vacancies !== undefined ? c.vacancies : null)) }; var employees = []; if (Array.isArray(c.employees)){ for (var i=0;i<c.employees.length;i++){ var e1=c.employees[i]; employees.push({ name: (e1.name!==undefined?e1.name:(e1.playername!==undefined?e1.playername:'Unknown')), role: (e1.position!==undefined?e1.position:(e1.job!==undefined?e1.job:'Unknown')), effectiveness: (typeof e1.effectiveness==='number'?e1.effectiveness:(typeof e1.efficiency==='number'?e1.efficiency:null)), lastAction: (e1.last_action || e1.lastAction || '') }); } } else if (c.employees && typeof c.employees==='object'){ var keys = Object.keys(c.employees); for (var j=0;j<keys.length;j++){ var e=c.employees[keys[j]]; employees.push({ name: (e.name!==undefined?e.name:(e.playername!==undefined?e.playername:'Unknown')), role: (e.position!==undefined?e.position:(e.job!==undefined?e.job:'Unknown')), effectiveness: (typeof e.effectiveness==='number'?e.effectiveness:(typeof e.efficiency==='number'?e.efficiency:null)), lastAction: (e.last_action || e.lastAction || '') }); } } var stockRows = []; if (c.stock && typeof c.stock==='object'){ var sKeys = Object.keys(c.stock); for (var k=0;k<sKeys.length;k++){ var it=c.stock[sKeys[k]]; stockRows.push({ name: (it.name!==undefined?it.name:'Item'), price: (it.price!==undefined?it.price:null), stock: (it.in_stock!==undefined?it.in_stock:(it.stock!==undefined?it.stock:null)), soldToday: (it.sold_today!==undefined?it.sold_today:null), sold7d: (it.sold_week!==undefined?it.sold_week:(it.sold7d!==undefined?it.sold7d:null)) }); } } return { summary: summary, employees: employees, stockRows: stockRows }; }); } var lastApiTs = 0; function maybeGetApiData(apiKey){ var now = Date.now(); var needCompany = onCompanyPage() || !!isStaffTableVisible() || scrapeInventoryDOM().length>0; if (!apiKey || !needCompany) return Promise.resolve(null); if (now - lastApiTs < CFG.apiThrottleMs) return Promise.resolve(null); lastApiTs = now; return getCompanyViaApi(apiKey).catch(function(e){ console.warn('API error:', e.message); return null; }); } // ---------- Panel & Styles ---------- function injectStyles(){ if (qs('#ad-shared-styles')) return; var css=document.createElement('style'); css.id='ad-shared-styles'; css.textContent=[ '#'+CFG.panelId+'{', ' position: fixed; right: 16px; bottom: 40px; z-index: 9999;', ' width: 320px; max-height: 70vh; overflow: auto;', ' background: #0c0c0f; color: #e9e9ef; border: 1px solid #29292c; border-radius: 12px;', ' box-shadow: 0 8px 24px rgba(0,0,0,0.35); font-family: Arial, Helvetica, sans-serif;', '}', '#'+CFG.panelId+':focus{ outline:none; }', '#'+CFG.panelId+'.collapsed .ad-body, #'+CFG.panelId+'.collapsed .ad-footer{ display:none; }', '#'+CFG.panelId+'.collapsed{ height:auto; max-height:unset; }', '#'+CFG.panelId+' .ad-header{', ' display:flex; align-items:center; justify-content:space-between; gap:8px;', ' padding:10px 12px; border-bottom:1px solid #222; position:sticky; top:0; background:#0c0c0f;', '}', '#'+CFG.panelId+' .ad-title{ font-size:14px; font-weight:700; }', '#'+CFG.panelId+' .ad-controls button{', ' background:#18181b; color:#e9e9ef; border:1px solid #333; border-radius:8px; padding:4px 8px; cursor:pointer; font-size:12px;', '}', '#'+CFG.panelId+' .ad-controls button:hover{ background:#222; }', '#'+CFG.panelId+' .ad-body{ padding:10px 12px; }', '#'+CFG.panelId+' .ad-section{ border:1px solid #222; border-radius:10px; padding:8px; margin-bottom:10px; background:#101014; }', '#'+CFG.panelId+' .ad-section h3{ margin:0 0 6px; font-size:13px; font-weight:700; }', '#'+CFG.panelId+' .ad-grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px; }', '#'+CFG.panelId+' .ad-kv{ display:flex; justify-content:space-between; background:#121217; padding:6px; border-radius:6px; }', '#'+CFG.panelId+' .muted{ color:#a7a7b2; }', '#'+CFG.panelId+' .ok{ color:#7bd88f; } .warn{ color:#f2c14e; } .bad{ color:#ef6a6a; }', '#'+CFG.panelId+' .ad-chip{ display:inline-block; padding:1px 6px; border-radius:999px; background:#1d1d22; border:1px solid #333; font-size:10px; margin-left:6px; color:#9aa0a6; }', '#'+CFG.panelId+' .retail .item{ border-top:1px dashed #2a2a2f; padding-top:8px; margin-top:8px; }', '#'+CFG.panelId+' .retail .item h4{ margin:0 0 4px; font-size:12px; }', '#'+CFG.panelId+' .retail .row{ display:flex; justify-content:space-between; gap:8px; font-size:12px; margin:2px 0; }', '#'+CFG.panelId+' .retail input.cost{ width:110px; background:#0e0e12; color:#e9e9ef; border:1px solid #333; border-radius:6px; padding:2px 6px; font-size:12px; }', '#'+CFG.panelId+' .ad-footer{ padding:8px 12px; border-top:1px solid #222; font-size:11px; color:#a7a7b2; }', '#'+CFG.panelId+' .row{ display:flex; justify-content:space-between; gap:8px; font-size:12px; margin:2px 0; }', '#'+CFG.panelId+' input[type="password"], #'+CFG.panelId+' input[type="number"]{ background:#0e0e12; color:#e9e9ef; border:1px solid #333; border-radius:6px; padding:4px 6px; font-size:12px; }' ].join('\n'); document.head.appendChild(css); } function ensurePanel(){ injectStyles(); var panel = qs('#'+CFG.panelId); if (!panel){ panel = document.createElement('div'); panel.id = CFG.panelId; panel.setAttribute('tabindex','0'); panel.innerHTML=[ '<div class="ad-header">', ' <div class="ad-title">Assistant Director <span class="ad-chip">v0.3.6</span></div>', ' <div class="ad-controls">', ' <button data-ad="refresh">Refresh</button>', ' <button data-ad="collapse">Collapse</button>', ' </div>', '</div>', '<div class="ad-body"></div>', '<div class="ad-footer">Read-only. Tips are heuristics; API key stored locally if you add it.</div>' ].join('\n'); document.body.appendChild(panel); var collapsed = !!store.get(CFG.ls.collapse,false); if (collapsed) panel.classList.add('collapsed'); updateCollapseButtonLabel(panel); panel.querySelector('[data-ad="collapse"]').addEventListener('click', function(){ panel.classList.toggle('collapsed'); store.set(CFG.ls.collapse, panel.classList.contains('collapsed')); updateCollapseButtonLabel(panel); }); panel.querySelector('[data-ad="refresh"]').addEventListener('click', function(){ runAll(); }); panel.addEventListener('input', function(e){ if (e && e.target && e.target.id==='ad-api-key'){ sessionStorage.setItem('ad.apiKey.draft', e.target.value); } }); } return panel; } function updateCollapseButtonLabel(panel){ var btn = qs('[data-ad="collapse"]', panel); if (!btn) return; btn.textContent = panel.classList.contains('collapsed') ? 'Expand' : 'Collapse'; } function setPanelBody(html){ var panel = ensurePanel(); qs('.ad-body', panel).innerHTML = html; } function renderApiControls(){ var draft = sessionStorage.getItem('ad.apiKey.draft'); var saved = localStorage.getItem(CFG.ls.apiKey) || ''; var apiKey = (draft !== null ? draft : saved); var lowDefault = store.get(CFG.ls.lowEffThreshold, CFG.staff.lowEffDefault); var warnDefault = store.get(CFG.ls.warnEffThreshold, CFG.staff.warnEffDefault); return [ '<div class="ad-section">', ' <h3>Settings</h3>', ' <div class="row"><span class="muted">Torn API key (optional)</span><span><input id="ad-api-key" type="password" placeholder="Enter key" value="'+apiKey+'"></span></div>', ' <div class="row"><span class="muted">Low effectiveness threshold</span><span><input id="ad-low-thr" type="number" min="0" max="100" value="'+lowDefault+'">%</span></div>', ' <div class="row"><span class="muted">Warn effectiveness threshold</span><span><input id="ad-warn-thr" type="number" min="0" max="100" value="'+warnDefault+'">%</span></div>', ' <div class="row"><span></span><span><button id="ad-save-settings">Save</button></span></div>', '</div>' ].join('\n'); } function wireSettings(){ var panel = ensurePanel(); var saveBtn = qs('#ad-save-settings', panel); if (!saveBtn) return; saveBtn.addEventListener('click', function(){ var keyEl = qs('#ad-api-key', panel); var key = keyEl ? (keyEl.value||'').trim() : ''; if (key) localStorage.setItem(CFG.ls.apiKey, key); else localStorage.removeItem(CFG.ls.apiKey); sessionStorage.removeItem('ad.apiKey.draft'); var lowEl = qs('#ad-low-thr', panel); var warnEl = qs('#ad-warn-thr', panel); var low = parseNumber(lowEl ? lowEl.value : null); var warn = parseNumber(warnEl ? warnEl.value : null); if (low!=null) store.set(CFG.ls.lowEffThreshold, low); if (warn!=null) store.set(CFG.ls.warnEffThreshold, warn); runAll(); }); } // ---------- Page detectors ---------- function onCompanyPage(){ var url = location.pathname + location.search; return (/\/company\.php|\/companies\.php/i).test(url); } function isStaffTableVisible(){ var tables = qsa('table, .table, .employees, .staff-list, .company-employees'); for (var i=0;i<tables.length;i++){ var tbl=tables[i], tr=qs('tr', tbl); var hdr = getText(tr||tbl).toLowerCase(); if (/(\bemployee\b|name)/.test(hdr) && /(role|position)/.test(hdr) && /(effective|efficiency)/.test(hdr)) return tbl; } return null; } // ---------- DOM scraping (fallback) ---------- function findByLabelNearby(label){ var nodes = qsa('*').filter(function(el){ var t = getText(el).toLowerCase(); return t && t.indexOf(label)!==-1; }); for (var i=0;i<nodes.length;i++){ var el = nodes[i]; var numHere = parseNumber(getText(el)); if (numHere!=null) return numHere; var near = (el.closest && el.closest('tr,li,div')) || el.parentElement; if (near){ var n1 = parseNumber(getText(qs('.value, .stat, .right, .bold, .number, ._value, ._stat, .t-green, .t-red', near))); if (n1!=null) return n1; var txt = getText(near); var p = /(-?\d[\d,]*\.?\d*)\s*%/.exec(txt); if (p) return parseNumber(p[1]); var m = /[$£€]\s*(-?\d[\d,]*\.?\d*)/.exec(txt); if (m) return parseNumber(m[1]); } } return null; } function scrapeCompanySummaryDOM(){ var popularity = findByLabelNearby('popularity'); var environment = findByLabelNearby('environment'); var effectiveness = findByLabelNearby('effectiveness'); var income = coalesce(findByLabelNearby('income'), findByLabelNearby('revenue')); var expenses = coalesce(findByLabelNearby('expenses'), findByLabelNearby('wages')); var profit = null, margin = null; if (income!=null && expenses!=null){ profit = income - expenses; if (income>0) margin = (profit/income)*100; } else { var profitLbl = coalesce(findByLabelNearby('profit'), findByLabelNearby('net')); if (profitLbl!=null) profit = profitLbl; } var vacancies = coalesce(findByLabelNearby('vacancies'), null); return { popularity:popularity, environment:environment, effectiveness:effectiveness, income:income, expenses:expenses, profit:profit, margin:margin, vacancies:vacancies }; } function arrayFindIndex(arr, predicate){ for (var i=0;i<arr.length;i++){ if (predicate(arr[i], i, arr)) return i; } return -1; } function arrayFind(arr, predicate){ for (var i=0;i<arr.length;i++){ if (predicate(arr[i], i, arr)) return arr[i]; } return undefined; } function scrapeEmployeesDOM(){ var tbl = isStaffTableVisible(); if (!tbl) return []; var rows = qsa('tr', tbl).slice(1); var out = []; rows.forEach(function(tr){ var tds = qsa('td', tr); if (!tds.length) return; var name = getText(tds[0]) || 'Unknown'; var role = ''; var eff = null; var lastAction = ''; for (var i=0;i<tds.length;i++){ var td = tds[i]; var t = getText(td).toLowerCase(); if (!role && /(role|position)/.test(t)) role = getText(td); if (eff==null && /%/.test(t)){ var p = /(-?\d[\d,]*\.?\d*)\s*%/.exec(getText(td)); if (p) eff = parseNumber(p[1]); } if (!lastAction && /last action/i.test(getText(td))) lastAction = getText(td); } role = role || (getText(tds[1]) || '').trim(); if (eff==null){ var pctCell = arrayFind(tds, function(td){ return /%/.test(getText(td)); }); eff = parseNumber(getText(pctCell || '')); } out.push({ name:name, role: role||'Unknown', effectiveness: eff, lastAction: lastAction }); }); return out.filter(function(e){ return e.role; }); } function scrapeInventoryDOM(){ var tables = qsa('table, .table, .inventory, .stock-list, .company-products, .items-list'); var rows = []; for (var t=0;t<tables.length;t++){ var tbl = tables[t]; var trs = qsa('tr', tbl); if (trs.length<2) continue; var hdr = getText(trs[0]).toLowerCase(); var looks = /(item|product|name)/.test(hdr) && /price/.test(hdr) && /(stock|qty|quantity)/.test(hdr); if (!looks) continue; for (var i=1;i<trs.length;i++){ var tds = qsa('td', trs[i]); if (tds.length<3) continue; var name = getText(tds[0]); var price = parseNumber(getText(tds[1])); var stock = parseNumber(getText(tds[2])); if (!name || price==null || stock==null) continue; var cellsText = []; for (var c = 0; c < tds.length; c++) { cellsText.push(getText(tds[c]).toLowerCase()); } var idxToday = arrayFindIndex(cellsText, function(x){ return /sold.*today/.test(x); }); var idx7d = arrayFindIndex(cellsText, function(x){ return /(7\s*d|week)/.test(x) || /sold.*7/.test(x); }); var soldToday = idxToday>=0 ? parseNumber(getText(tds[idxToday])) : null; var sold7d = idx7d>=0 ? parseNumber(getText(tds[idx7d])) : null; rows.push({ name:name, price:price, stock:stock, soldToday:soldToday, sold7d:sold7d }); } } return rows; } // ---------- Renderers ---------- function renderCompanySummary(summary){ var popularity=summary.popularity, environment=summary.environment, effectiveness=summary.effectiveness; var income=summary.income, expenses=summary.expenses, profit=summary.profit, margin=summary.margin, vacancies=summary.vacancies; function statClass(v, good, warn){ if (v==null) return ''; if (typeof v==='number' && !isNaN(v)){ if (v>=good) return 'ok'; if (v>=warn) return 'warn'; } return 'bad'; } var popCls=statClass(popularity,80,60); var envCls=statClass(environment,80,60); var effCls=statClass(effectiveness,80,60); var marCls=statClass(margin,20,10); var tips=[]; if (vacancies!=null && vacancies>0) tips.push('You have <b>'+vacancies+'</b> vacancies — hire to lift popularity & effectiveness.'); if (popularity!=null && popularity<70) tips.push('Consider ads/specials to boost <b>Popularity</b>.'); if (environment!=null && environment<70) tips.push('Review upgrades/perks to improve <b>Environment</b>.'); if (effectiveness!=null && effectiveness<75) tips.push('Check role fit & activity to raise <b>Effectiveness</b>.'); if (margin!=null && margin<10) tips.push('Margin is low — review wages/ads/supplies, or nudge prices (see Retail Advisor).'); if (!tips.length) tips.push('Looking solid. Maintain consistency to push for stars.'); return [ '<div class="ad-section">', ' <h3>Company Snapshot</h3>', ' <div class="ad-grid">', ' <div class="ad-kv"><span class="muted">Popularity</span><span class="'+popCls+'">'+(popularity==null?'—':percent(popularity,0))+'</span></div>', ' <div class="ad-kv"><span class="muted">Environment</span><span class="'+envCls+'">'+(environment==null?'—':percent(environment,0))+'</span></div>', ' <div class="ad-kv"><span class="muted">Effectiveness</span><span class="'+effCls+'">'+(effectiveness==null?'—':percent(effectiveness,0))+'</span></div>', ' <div class="ad-kv"><span class="muted">Daily Income</span><span>'+(income==null?'—':money(income))+'</span></div>', ' <div class="ad-kv"><span class="muted">Daily Expenses</span><span>'+(expenses==null?'—':money(expenses))+'</span></div>', ' <div class="ad-kv"><span class="muted">Daily Profit</span><span class="'+((profit!=null && profit<0)?'bad':'')+'">'+(profit==null?'—':money(profit))+'</span></div>', ' <div class="ad-kv"><span class="muted">Margin</span><span class="'+marCls+'">'+(margin==null?'—':percent(margin,1))+'</span></div>', ' <div class="ad-kv"><span class="muted">Vacancies</span><span>'+(vacancies==null?'—':vacancies)+'</span></div>', ' </div>', '</div>', renderApiControls() ].join('\n'); } function loadItemCosts(){ var costs = store.get(CFG.ls.itemCosts, {}); if (!costs || typeof costs!=='object'){ costs={}; store.set(CFG.ls.itemCosts, costs); } return costs; } function saveItemCosts(m){ store.set(CFG.ls.itemCosts, m||{}); } function buildRetailAdvice(items){ var costs = loadItemCosts(); return items.map(function(it){ var cost = costs[it.name]; var margin = (cost!=null) ? ((it.price - cost)/Math.max(1,cost))*100 : null; var dailySold = (it.soldToday!=null) ? it.soldToday : ((it.sold7d!=null) ? it.sold7d/7 : null); var stockDaysLeft = dailySold ? (it.stock/Math.max(0.01,dailySold)) : null; var sellThroughPct = (dailySold && it.stock) ? Math.min(100,(dailySold/it.stock)*100) : null; var suggestion='Hold price', nudgePct=0; if (sellThroughPct!=null){ var target = CFG.retail.targetSellThroughDaily*100; var diff = sellThroughPct - target; if (diff>5){ var stepsUp = Math.ceil(diff/5); nudgePct = clamp(stepsUp*CFG.retail.elasticityStepPctPerStep, 0, CFG.retail.maxNudgePct); suggestion = 'Raise ~'+nudgePct.toFixed(0)+'%'; } else if (diff<-5){ var stepsDn = Math.ceil(Math.abs(diff)/5); nudgePct = -clamp(stepsDn*CFG.retail.elasticityStepPctPerStep, 0, CFG.retail.maxNudgePct); suggestion = 'Lower ~'+Math.abs(nudgePct).toFixed(0)+'%'; } } var warnings=[]; if (margin!=null && margin<CFG.retail.minFloorMarginPct) warnings.push('Low margin ('+percent(margin,0)+')'); if (stockDaysLeft!=null && stockDaysLeft>30) warnings.push('Overstocked (>30 days)'); var newPrice = nudgePct ? Math.max(0, Math.round(it.price*(1+nudgePct/100))) : it.price; return { name:it.name, price:it.price, stock:it.stock, soldToday:it.soldToday, sold7d:it.sold7d, cost:cost, margin:margin, dailySold:dailySold, stockDaysLeft:stockDaysLeft, sellThroughPct:sellThroughPct, suggestion:suggestion, nudgePct:nudgePct, newPrice:newPrice, warnings:warnings }; }); } function renderRetailSection(advice){ if (!advice.length) return ''; return [ '<div class="ad-section retail">', ' <h3>Retail Pricing Advisor <span class="ad-chip">beta</span></h3>', advice.map(function(i){ return [ '<div class="item" data-name="'+i.name+'">', ' <h4>'+i.name+'</h4>', ' <div class="row"><span class="muted">Price</span><span>'+money(i.price)+'</span></div>', ' <div class="row"><span class="muted">Stock</span><span>'+i.stock+'</span></div>', (i.dailySold!=null ? ' <div class="row"><span class="muted">Sold/day</span><span>'+i.dailySold.toFixed(1)+'</span></div>' : ''), (i.sellThroughPct!=null ? ' <div class="row"><span class="muted">Sell-through</span><span>'+percent(i.sellThroughPct,1)+'</span></div>' : ''), (i.stockDaysLeft!=null ? ' <div class="row"><span class="muted">Days of stock</span><span>'+i.stockDaysLeft.toFixed(1)+'</span></div>' : ''), ' <div class="row"><span class="muted">Cost (edit)</span><span><input class="cost" type="number" step="1" min="0" placeholder="optional" value="'+(i.cost!=null?i.cost:'')+'" data-itemcost="'+i.name+'"></span></div>', (i.margin!=null ? ' <div class="row"><span class="muted">Margin</span><span>'+percent(i.margin,1)+'</span></div>' : ''), ' <div class="row"><span class="muted">Advice</span><span>'+i.suggestion+(i.newPrice!==i.price?(' → <b>'+money(i.newPrice)+'</b>'):'')+'</span></div>', (i.warnings.length ? ' <div class="row warn">⚠ '+i.warnings.join(' • ')+'</div>' : ''), '</div>' ].join('\n'); }).join('\n'), ' <div class="muted" style="margin-top:6px;">Tip: set item costs to calculate margins & low-margin warnings.</div>', '</div>' ].join('\n'); } function wireRetailCostInputs(){ var container = qs('#'+CFG.panelId+' .retail'); if (!container) return; var costs = loadItemCosts(); qsa('input.cost[data-itemcost]', container).forEach(function(inp){ inp.addEventListener('change', function(){ var name = inp.getAttribute('data-itemcost'); var val = parseNumber(inp.value); if (val==null){ delete costs[name]; } else { costs[name]=val; } saveItemCosts(costs); runAll(); }); }); } function renderPerRoleTable(employees){ if (!employees.length) return ''; var lowThr = store.get(CFG.ls.lowEffThreshold, CFG.staff.lowEffDefault); var warnThr = store.get(CFG.ls.warnEffThreshold, CFG.staff.warnEffDefault); var byRole = new Map(); employees.forEach(function(e){ var key = e.role || 'Unknown'; if (!byRole.has(key)) byRole.set(key, []); byRole.get(key).push(e); }); var rows=[]; byRole.forEach(function(list, role){ var effs = list.map(function(l){ return (typeof l.effectiveness==='number'?l.effectiveness:null); }).filter(function(v){ return v!=null; }); var avg = effs.length ? (effs.reduce(function(a,b){ return a+b; },0)/effs.length) : null; var lowCount = effs.filter(function(v){ return v<lowThr; }).length; var inactiveCount = list.filter(function(l){ var la = (l.lastAction || '').toLowerCase(); for (var i=0;i<CFG.staff.inactivePhrases.length;i++){ if (la.indexOf(CFG.staff.inactivePhrases[i])!==-1) return true; } return false; }).length; rows.push({ role:role, staff:list.length, avg:avg, low:lowCount, inactive:inactiveCount }); }); rows.sort(function(a,b){ if (a.avg==null) return -1; if (b.avg==null) return 1; return b.avg - a.avg; }); return [ '<div class="ad-section">', ' <h3>Per-role Effectiveness</h3>', rows.map(function(r){ var cls = (r.avg==null) ? '' : (r.avg>=warnThr ? (r.avg>=90?'ok':'warn') : 'bad'); return '<div class="row"><span class="muted">'+r.role+' (Staff '+r.staff+')</span><span class="'+cls+'">'+(r.avg==null?'—':percent(r.avg,0))+' · Low '+r.low+' · Inactive '+r.inactive+'</span></div>'; }).join('\n'), ' <div class="muted" style="margin-top:6px;">Thresholds: Low '+lowThr+'% · Warn '+warnThr+'% (adjust in Settings above).</div>', '</div>' ].join('\n'); } // ---------- Main render ---------- function runAll(){ sleep(150, function(){ var sections=[]; var apiKey = localStorage.getItem(CFG.ls.apiKey) || ''; var summary=null, employees=[], invRows=[]; maybeGetApiData(apiKey).then(function(apiData){ if (apiData){ summary = apiData.summary || null; employees = apiData.employees || []; invRows = apiData.stockRows || []; } if (!summary && onCompanyPage()) summary = scrapeCompanySummaryDOM(); if (!employees.length) employees = scrapeEmployeesDOM(); if (!invRows.length) invRows = scrapeInventoryDOM(); if (summary) sections.push(renderCompanySummary(summary)); if (employees.length) sections.push(renderPerRoleTable(employees)); if (invRows.length){ var advice = buildRetailAdvice(invRows); sections.push(renderRetailSection(advice)); } if (!sections.length){ sections.push([ '<div class="ad-section">', ' <h3>Assistant Director</h3>', ' <div class="muted">Enter your Torn API key (optional) and open Company/Staff/Inventory tabs to see data. The panel also works without API by reading the page.</div>', '</div>' ].join('\n')); } setPanelBody(sections.join('\n')); wireRetailCostInputs(); wireSettings(); updateCollapseButtonLabel(ensurePanel()); }); }); } // ---------- Observer ---------- var observer=null, observerTimer=null; function attachObserver(){ if (observer) observer.disconnect(); observer = new MutationObserver(function(mutations){ var panel = ensurePanel(); if (isTypingInPanel()) return; var relevant=false; for (var i=0;i<mutations.length;i++){ var m = mutations[i]; if (m.type!=='childList') continue; if (!panel){ relevant=true; break; } if (panel.contains(m.target)) continue; var skip=false, n; for (n=0;n<m.addedNodes.length;n++){ if (panel.contains(m.addedNodes[n])) { skip=true; break; } } if (skip) continue; for (n=0;n<m.removedNodes.length;n++){ if (panel.contains(m.removedNodes[n])) { skip=true; break; } } if (skip) continue; relevant=true; break; } if (!relevant) return; clearTimeout(observerTimer); observerTimer = setTimeout(runAll, 400); }); observer.observe(document.body, { childList:true, subtree:true }); } // Boot ensurePanel(); runAll(); attachObserver(); })();