您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Distribute money or change balances for multiple faction members (Not supported on mobile)
// ==UserScript== // @name Batch Balance // @namespace https://github.com/tobytorn // @description Distribute money or change balances for multiple faction members (Not supported on mobile) // @author tobytorn [1617955] // @match https://www.torn.com/factions.php?step=your* // @version 2.0.2 // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @supportURL https://github.com/tobytorn/batch-balance // @license MIT // @require https://unpkg.com/[email protected]/dist/jquery.min.js // ==/UserScript== // Usage: // Add the following parameters to the URL of the control page (https://www.torn.com/factions.php?step=your#/tab=controls) to enable this script // batbal_uids Comma-separated user IDs // batbal_amounts Comma-separated amounts // batbal_action [Optional] "add" for adding to balance (default), or "give" for giving money // batbal_asset [Optional] "money" (default) or "points" // // Example: The following URL will add 120 to Leslie, subtract 250 from tobytorn, and add 1.5k to Duke // https://www.torn.com/factions.php?step=your#/tab=controls&batbal_uids=15,1617955,4&batbal_amounts=120,-250,1500 'use strict'; function batchBalanceWrapper() { console.log('Batch Balance starts'); const ACTION_INTERVAL_MS = 1000; const GM_VALUE_KEY = 'batbal-action'; const PROFILE_HREF_PREFIX = 'profiles.php?XID='; const ACTION_SPECS = { give: { summary: 'Give', text: 'Give', waitingText: 'Giving', bodyParam: 'giveMoney', }, add: { summary: 'Add to balance', text: 'Add', waitingText: 'Adding', bodyParam: 'addToBalance', }, }; const $ = window.jQuery; const LOCAL_STORAGE_PREFIX = 'BATCH_BALANCE_'; function getLocalStorage(key, defaultValue) { const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key); try { return JSON.parse(value) ?? defaultValue; } catch (err) { return defaultValue; } } function setLocalStorage(key, value) { window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value)); } const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda'); const [getValue, setValue] = isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function' ? [getLocalStorage, setLocalStorage] : [window.GM_getValue, window.GM_setValue]; const STYLE = ` .batbal-overlay { position: relative; } .batbal-overlay:after { content: ''; position: absolute; background: repeating-linear-gradient(135deg, #2228, #2228 70px, #0008 70px, #0008 80px); top: 0; left: 0; width: 100%; height: 100%; z-index: 900000; } #batbal-ctrl { margin: 10px 0; padding: 10px; border-radius: 5px; background-color: var(--default-bg-panel-color); text-align: center; line-height: 16px; } #batbal-ctrl-detail > :not(:first-child), #batbal-ctrl > :not(:first-child) { margin-top: 10px; } #batbal-ctrl-title { font-size: large; font-weight: bold; } #batbal-ctrl-status { font-weight: bold; } #batbal-ctrl button { margin: 0 4px; } #batbal-ctrl table { margin: 0 auto; } #batbal-ctrl th { font-weight: bold; } #batbal-ctrl th, #batbal-ctrl td { color: inherit; padding: 5px; border: 1px solid #ccc; } #batbal-ctrl td:last-child { text-align: right; } #batbal-ctrl-detail tr.batbal-done:after { content: '\u2713'; color: green; padding-left: 6px; } `; const CONTROLLER_HTML = ` <div id="batbal-ctrl"> <div id="batbal-ctrl-title">Batch Balance</div> <div> <table> <thead> <tr> <th colspan="2">Summary</th> </tr> </thead> <tbody> <tr> <th>Action</th> <td id="batbal-ctrl-summary-action-type">-</td> </tr> <tr> <th>Asset Type</th> <td id="batbal-ctrl-summary-asset-type">-</td> </tr> <tr> <th>Player Count</th> <td id="batbal-ctrl-summary-player-count">-</td> </tr> <tr> <th>Player Not in Faction</th> <td><span id="batbal-ctrl-summary-player-not-in-faction">-</span></td> </tr> <tr> <th>Total Amount</th> <td><span id="batbal-ctrl-summary-total-amount">-</span></td> </tr> </tbody> </table> </div> <div> <button id="batbal-ctrl-start" class="torn-btn" disabled>Start</button> <button id="batbal-ctrl-show-detail" class="torn-btn">Show details</button> <button id="batbal-ctrl-hide-detail" class="torn-btn" style="display: none">Hide details</button> <button id="batbal-ctrl-clear-data" class="torn-btn" disabled>Clear data</button> </div> <button id="batbal-ctrl-submit" class="torn-btn" style="display: none" disabled></button> <div>Status: <span id="batbal-ctrl-status"></span></div> <div id="batbal-ctrl-detail" style="display: none"> <table> <thead> <tr> <th>ID</th> <th>Name</th> <th>Amount</th> <th>Note</th> </tr> </thead> <tbody></tbody> </table> </div> </div>`; function formatAmount(v) { return (v >= 0 ? '+' : '') + v.toString().replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => s + ','); } async function sleep(t) { await new Promise((r) => setTimeout(r, t)); } // Copied from https://stackoverflow.com/a/25490531 function getCookie(name) { return document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; } function getParams() { const params = new URLSearchParams(location.hash.slice(1)); return params.get('/tab') === 'controls' ? params : null; } function storeAction(action) { setValue(GM_VALUE_KEY, action); } function parseAction() { const params = getParams(); if (!params) { return null; } const paramUids = params.get('batbal_uids'); const paramAmounts = params.get('batbal_amounts'); if (paramUids === null || paramAmounts === null) { return null; } const uids = paramUids.split(','); const amounts = paramAmounts.split(','); if (amounts.length !== uids.length) { return { error: 'Param "batbal_uids" and "batbal_amounts" have different lengths' }; } if (uids.length === 0 || uids.some((x) => !x.match(/^\d+$/))) { return { error: 'Param "batbal_uids" is invalid' }; } if (amounts.length === 0 || amounts.some((x) => !x.match(/^[+-]?\d{1,11}$/))) { return { error: 'Param "batbal_amounts" is invalid' }; } const actionType = params.get('batbal_action') ?? 'add'; if (!['give', 'add'].includes(actionType)) { return { error: 'Param "batbal_action" is invalid' }; } const assetType = params.get('batbal_asset') ?? 'money'; if (!['money', 'points'].includes(assetType)) { return { error: 'Param "batbal_asset" is invalid' }; } return { uidAmounts: uids.map((uid, i) => [uid, parseInt(amounts[i])]).filter(([, amount]) => amount !== 0), next: 0, actionType, assetType, }; } function checkAction(parsedAction, storedAction) { if (parsedAction && storedAction) { if ( JSON.stringify(parsedAction.uidAmounts) !== JSON.stringify(storedAction.uidAmounts) || parsedAction.actionType !== storedAction.actionType || parsedAction.assetType !== storedAction.assetType ) { throw new Error( "An unfinished Batch Balance operation was found that doesn't match the URL parameters. " + 'Click "Show details" to view the pending operation. ' + 'To resume it, clear the URL parameters and refresh the page.' + 'To discard it, click "Clear data" and refresh the page.', ); } } return storedAction ?? parsedAction; } /** @returns Promise<Record<string, { name: string, isInFaction: boolean }>> */ async function getUidMap() { return new Promise((resolve) => { const interval = setInterval(function () { const $depositors = $('.money___aACfM .userListWrap___voEX8 .userInfoWrap___rjWOK'); if ($depositors.length === 0) { return; } const map = {}; $depositors.each(function () { const $name = $(this).find(`a[href*="${PROFILE_HREF_PREFIX}"]`).first(); if ($name.length > 0) { const uid = ($name.attr('href') || '').split(PROFILE_HREF_PREFIX)[1]; map[uid] = { name: $name.text().trim(), isInFaction: !$(this).hasClass('inactive___Hd0EQ'), }; } }); clearInterval(interval); resolve(map); }, 200); }); } function renderController() { GM_addStyle(STYLE); const $controlsWrap = $('.faction-controls-wrap'); $controlsWrap.addClass('batbal-overlay'); $controlsWrap.before(CONTROLLER_HTML); $('#batbal-ctrl-show-detail').on('click', function () { $('#batbal-ctrl-detail').show(); $('#batbal-ctrl-hide-detail').show(); $(this).hide(); }); $('#batbal-ctrl-hide-detail').on('click', function () { $('#batbal-ctrl-detail').hide(); $('#batbal-ctrl-show-detail').show(); $(this).hide(); }); $('#batbal-ctrl-clear-data').on('click', function () { if ( confirm( 'Are you sure you want to delete the saved Batch Balance data? ' + 'This will remove any unfinished operations and cannot be undone.', ) ) { storeAction(null); $('#batbal-ctrl').hide(); alert('Saved data has been deleted, please refresh the page'); } }); } function updateStatus(s) { $('#batbal-ctrl-status').text(String(s)); if (s instanceof Error) { $('#batbal-ctrl-status').css('color', 'red'); } } function renderDetails(action, uidMap) { const $tbody = $('#batbal-ctrl-detail tbody'); $tbody.empty(); let outsideCount = 0; action.uidAmounts.forEach(([uid, amount], i) => { const amountClass = amount >= 0 ? 't-green' : 't-red'; const trClass = i < action.next ? 'batbal-done' : ''; const uidInfo = uidMap[uid] || {}; const name = uidInfo.name || ''; const isInFaction = uidInfo.isInFaction || false; if (!isInFaction) { outsideCount++; } $tbody.append(`<tr class="${trClass}"> <td>${uid}</td> <td>${name}</td> <td><span class="${amountClass}">${formatAmount(amount)}</span></td> <td><span class="${!isInFaction ? 't-red' : ''}">${!isInFaction ? 'Not in faction' : ''}</span></td> </tr>`); }); const total = action.uidAmounts.reduce((v, [, amount]) => v + amount, 0); const totalClass = total >= 0 ? 't-green' : 't-red'; const actionSpec = ACTION_SPECS[action.actionType]; $('#batbal-ctrl-summary-action-type').text(actionSpec.summary); $('#batbal-ctrl-summary-asset-type').text(action.assetType); $('#batbal-ctrl-summary-player-count').text(action.uidAmounts.length); $('#batbal-ctrl-summary-player-not-in-faction') .text(outsideCount) .toggleClass('t-red', outsideCount > 0); $('#batbal-ctrl-summary-total-amount').text(formatAmount(total)).addClass(totalClass); updateStatus(`Progress: ${action.next} / ${action.uidAmounts.length} done`); } async function addMoney({ uid, name, amount, actionType, assetType }) { const $submit = $('#batbal-ctrl-submit'); $submit.show(); const actionSpec = ACTION_SPECS[actionType]; const textSuffix = ` ${assetType}: ${name} [${uid}] ${formatAmount(amount)}`; const queryParam = { money: 'factionsGiveMoney', points: 'factionsGivePoints', }[assetType]; $submit.text(`${actionSpec.text} ${textSuffix}`); $submit.prop('disabled', false); return new Promise((resolve, reject) => { $submit.on('click', async () => { try { $submit.off('click'); $submit.text(`${actionSpec.waitingText} ${textSuffix}`); $submit.prop('disabled', true); const rfcv = getCookie('rfc_v'); const rsp = await fetch(`/page.php?sid=${queryParam}&rfcv=${rfcv}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-requested-with': 'XMLHttpRequest', }, body: JSON.stringify({ option: actionSpec.bodyParam, receiver: parseInt(uid), amount, }), }); const rawData = await rsp.text(); if (!rsp.ok) { throw new Error(`Network error: ${rsp.status} ${rawData}`); } const data = JSON.parse(rawData); if (data.success === true) { resolve(); } else { reject(new Error(`Unexpected server response: ${rawData}`)); } } catch (err) { reject(err); } }); }); } async function start(action, uidMap) { storeAction(action); $('#batbal-ctrl-start').prop('disabled', true); $('#batbal-ctrl-clear-data').prop('disabled', true); try { while (action.next < action.uidAmounts.length) { updateStatus(`Current progress: ${action.next} / ${action.uidAmounts.length} done`); const now = Date.now(); const [uid, amount] = action.uidAmounts[action.next]; const uidInfo = uidMap[uid] || {}; const name = uidInfo.name || 'Unknown player'; await addMoney({ uid, name, amount, actionType: action.actionType, assetType: action.assetType }); action.next++; storeAction(action); renderDetails(action, uidMap); const elapsed = Date.now() - now; if (elapsed < ACTION_INTERVAL_MS) { await sleep(ACTION_INTERVAL_MS - elapsed); } } storeAction(null); updateStatus('All done!'); } catch (err) { updateStatus(err); } } async function main() { try { const parsedAction = parseAction(); const storedAction = getValue(GM_VALUE_KEY, null); if (storedAction === null && parsedAction === null) { return; } const uidMap = await getUidMap(); renderController(); if (storedAction) { renderDetails(storedAction, uidMap); $('#batbal-ctrl-clear-data').prop('disabled', false); } if (parsedAction.error) { throw new Error(parsedAction.error); } const action = checkAction(parsedAction, storedAction); if (!storedAction) { renderDetails(action, uidMap); } if (action.actionType === 'give') { if (action.uidAmounts.some(([uid]) => !uidMap[uid]?.isInFaction)) { throw new Error('Some players are not in the faction'); } if (action.uidAmounts.some(([, amount]) => amount <= 0)) { throw new Error('Amounts to give must be positive'); } } $('#batbal-ctrl-start').prop('disabled', false); $('#batbal-ctrl-start').on('click', () => start(action, uidMap)); } catch (err) { updateStatus(err); console.log('Unhandled exception from Batch Balance:', err); } } main(); console.log('Batch Balance ends'); } if (document.readyState === 'loading') { document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive') { batchBalanceWrapper(); } }); } else { batchBalanceWrapper(); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址