您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
统计画廊形状,点击形状可以自动移动,增加全自动按钮
// ==UserScript== // @name gooboo画廊形状移动脚本 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 统计画廊形状,点击形状可以自动移动,增加全自动按钮 // @author zding // @match *://*/gooboo/ // @match https://gooboo.g8hh.com.cn/* // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; const MODES = [ { name: 'LS', sortAsc: true, stopLow: true, description: '最少优先, 低资源停止' }, { name: 'LC', sortAsc: true, stopLow: false, description: '最少优先, 低资源继续' }, { name: 'MS', sortAsc: false, stopLow: true, description: '最多优先, 低资源停止' }, { name: 'MC', sortAsc: false, stopLow: false, description: '最多优先, 低资源继续' } ]; const MODE_STORAGE_KEY = 'gooboo_gallery_sort_mode_v1'; let currentModeIndex = 0; const CONFIG = { MOVE_DELAY_MS: 10, DOM_UPDATE_WAIT_MS: 20, ENABLE_STATE_SYNC_CHECK: false, AUTO_SORT_INTERVAL_MS: 350, MIN_SHAPE_COUNT_FOR_AUTO_SORT: 5,//最低整理数 REROLL_COST_THRESHOLD: 36 }; const MDI_PREFIX = "mdi-"; const TEXT_COLOR_SUFFIX = "--text"; const GREY_COLOR = 'grey'; const SHAPE_BG_SELECTOR = ".shape-bg"; const CELL_SELECTOR = "td[id^='galleryShape_']"; const TABLE_LG_SELECTOR = ".mx-auto.shape-table-lg"; const TABLE_SM_SELECTOR = ".mx-auto.shape-table-sm"; const ALL_CELLS_SELECTOR = '.shape-cell'; const CELL_ID_REGEX = /galleryShape_(\d+)_(\d+)/; const ALLOWED_SHAPES = ['circle', 'rectangle', 'triangle', 'star', 'ellipse', 'heart', 'square', 'octagon', 'pentagon', 'hexagon']; const GALLERY_MOTIVATION_KEY = 'gallery_motivation'; const AUTO_SORT_BUTTON_ID = 'auto-sort-button'; const STATS_CONTAINER_CLASS = 'shape-stats'; const SHAPE_CHIP_CLASS = 'shape-chip'; let isAutoSorting = false; let autoSortIntervalId = null; const coordsToString = ({ x, y }) => `${x},${y}`; const stringToCoords = (str) => { const [x, y] = str.split(',').map(Number); return { x, y }; }; const coordsToId = ({ x, y }) => `galleryShape_${x}_${y}`; const getAdjacentCoords = ({ x, y }) => [{ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 }]; const manhattanDistance = (coords1, coords2) => Math.abs(coords1.x - coords2.x) + Math.abs(coords1.y - coords2.y); function applyModeSettings(modeIndex) { const mode = MODES[modeIndex]; if (mode) { CONFIG.SORT_ASCENDING = mode.sortAsc; CONFIG.STOP_ON_CURRENCY_LOW = mode.stopLow; console.log(`应用模式: ${mode.name} (${mode.description})`); } else { console.warn(`无效的模式索引: ${modeIndex}, 使用默认模式 LS`); applyModeSettings(MODES.findIndex(m => m.name === 'LS')); } } let savedModeIndex = GM_getValue(MODE_STORAGE_KEY, MODES.findIndex(m => m.name === 'MC')); if (typeof savedModeIndex !== 'number' || savedModeIndex < 0 || savedModeIndex >= MODES.length) { console.warn(`存储的模式索引无效 (${savedModeIndex}), 重置为默认模式 LS`); savedModeIndex = MODES.findIndex(m => m.name === 'LS'); GM_setValue(MODE_STORAGE_KEY, savedModeIndex); } currentModeIndex = savedModeIndex; applyModeSettings(currentModeIndex); let modeButtonElement = null; function updateModeButtonDisplay() { if (modeButtonElement) { const currentMode = MODES[currentModeIndex]; modeButtonElement.textContent = `${currentMode.name}`; let tooltipTitle = `点击切换模式. 当前: ${currentMode.name}\n\n可用模式:\n`; MODES.forEach((mode, index) => { const prefix = (index === currentModeIndex) ? '-> ' : ' '; tooltipTitle += `${prefix}${mode.name}: ${mode.description}\n`; }); modeButtonElement.title = tooltipTitle.trim(); } } function switchMode() { currentModeIndex = (currentModeIndex + 1) % MODES.length; applyModeSettings(currentModeIndex); GM_setValue(MODE_STORAGE_KEY, currentModeIndex); updateModeButtonDisplay(); } function createModeButton() { const button = document.createElement('button'); button.className = 'v-btn v-btn--is-elevated v-btn--has-bg theme--light v-size--default'; button.style.margin = '5px'; button.id = 'mode-switch-button'; button.style.backgroundColor = '#ef6c00'; button.style.color = 'white'; button.addEventListener('click', switchMode); modeButtonElement = button; updateModeButtonDisplay(); return button; } function checkAndUpdateStats() { const tableElement = document.querySelector(TABLE_LG_SELECTOR) || document.querySelector(TABLE_SM_SELECTOR); if (!tableElement) return; const tableParent = tableElement.parentNode; let statsContainer = tableParent.querySelector(`.${STATS_CONTAINER_CLASS}`); if (!statsContainer) { statsContainer = document.createElement("div"); statsContainer.className = `d-flex flex-wrap justify-center align-center ma-1 ${STATS_CONTAINER_CLASS}`; const autoButton = createAutoButton(); statsContainer.appendChild(autoButton); const modeButton = createModeButton(); statsContainer.appendChild(modeButton); tableParent.insertBefore(statsContainer, tableElement.nextSibling); } const shapeCounts = {}; const cells = tableElement.querySelectorAll(CELL_SELECTOR); cells.forEach(cell => { const hasShapeBackground = cell.querySelector(SHAPE_BG_SELECTOR) !== null; const iconElement = cell.querySelector("i.mdi"); if (iconElement) { const classList = iconElement.classList; let color = null; let shape = null; let skipThisIcon = false; for (const className of classList) { if (className.startsWith(MDI_PREFIX)) { shape = className.substring(MDI_PREFIX.length); if (!ALLOWED_SHAPES.includes(shape)) { skipThisIcon = true; break; } } else if (className.endsWith(TEXT_COLOR_SUFFIX)) { color = className.slice(0, -TEXT_COLOR_SUFFIX.length).replace(/^light-/, ''); } } if (skipThisIcon) return; if (!hasShapeBackground) { color = GREY_COLOR; } if (color && shape) { const key = `${color}-${shape}`; shapeCounts[key] = (shapeCounts[key] || 0) + 1; } } }); const existingChips = Array.from(statsContainer.querySelectorAll(`.${SHAPE_CHIP_CLASS}`)); const processedKeys = new Set(); existingChips.forEach(chip => { const shape = chip.dataset.shape; const color = chip.dataset.color; const key = `${color}-${shape}`; const count = shapeCounts[key] || 0; const countSpan = chip.querySelector("span"); if (countSpan) { countSpan.textContent = count; } if (count > 0) { processedKeys.add(key); } else { // Optionally remove the chip if count is 0 // chip.parentNode.remove(); // This removes the badge wrapper } }); for (const key in shapeCounts) { if (Object.hasOwnProperty.call(shapeCounts, key) && !processedKeys.has(key)) { const [color, shape] = key.split("-"); const count = shapeCounts[key]; // Merged createStatElement logic const badge = document.createElement("span"); badge.className = "v-badge v-badge--bordered v-badge--dot v-badge--overlap theme--dark"; const chip = document.createElement("div"); const isGrey = color === GREY_COLOR; chip.className = `v-chip v-chip--label v-size--small px-2 balloon-text-dynamic theme--dark darken-3 ${color} ma-1 ${SHAPE_CHIP_CLASS}`; chip.setAttribute("aria-haspopup", "true"); chip.setAttribute("aria-expanded", "false"); chip.dataset.shape = shape; chip.dataset.color = color; const icon = document.createElement("i"); icon.className = `v-icon notranslate mr-2 mdi ${MDI_PREFIX}${shape} theme--dark ${isGrey ? 'shape-icon-disabled' : color + TEXT_COLOR_SUFFIX}`; icon.setAttribute("aria-hidden", "true"); icon.setAttribute("aria-label", shape); icon.style.fontSize = "16px"; const countSpan = document.createElement("span"); countSpan.textContent = count; chip.append(icon, countSpan); if (!isGrey) { chip.addEventListener("click", () => triggerSort(chip.dataset.shape)); } badge.appendChild(chip); statsContainer.appendChild(badge); } } } function checkGameBoardExists() { return document.querySelector(ALL_CELLS_SELECTOR) !== null; } const simulateDragDrop = async (sourceId, targetId) => { if (!checkGameBoardExists()) { stopAutoSort(); return false; } if (getCurrencyValue(GALLERY_MOTIVATION_KEY) < 1) { if (CONFIG.STOP_ON_CURRENCY_LOW) { stopAutoSort(); } return false; } const sourceElement = document.getElementById(sourceId); const targetElement = document.getElementById(targetId); if (!sourceElement || !targetElement) { return false; } try { const dataTransfer = new DataTransfer(); sourceElement.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dataTransfer })); targetElement.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dataTransfer })); targetElement.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dataTransfer })); sourceElement.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dataTransfer })); return true; } catch (error) { return false; } }; const getShapeCells = (shapeName) => { const iconClass = `${MDI_PREFIX}${shapeName}`; return Array.from(document.querySelectorAll(`td${ALL_CELLS_SELECTOR}`)) .filter(cell => cell.querySelector(`i.${iconClass}`) && cell.querySelector(SHAPE_BG_SELECTOR)) .map(cell => { const match = cell.id.match(CELL_ID_REGEX); return match ? { id: cell.id, x: parseInt(match[1]), y: parseInt(match[2]) } : null; }) .filter(Boolean); }; const getAllCellCoordsSet = () => { return new Set( Array.from(document.querySelectorAll(ALL_CELLS_SELECTOR)) .map(cell => { const match = cell.id.match(CELL_ID_REGEX); return match ? coordsToString({ x: parseInt(match[1]), y: parseInt(match[2]) }) : null; }) .filter(Boolean) ); }; const isConnected = (coordSet) => { if (coordSet.size <= 1) return true; const startNode = coordSet.values().next().value; const visited = new Set([startNode]); const queue = [startNode]; while (queue.length > 0) { const currentCoordString = queue.shift(); const currentCoords = stringToCoords(currentCoordString); for (const adjacentCoords of getAdjacentCoords(currentCoords)) { const adjacentCoordString = coordsToString(adjacentCoords); if (coordSet.has(adjacentCoordString) && !visited.has(adjacentCoordString)) { visited.add(adjacentCoordString); queue.push(adjacentCoordString); } } } return visited.size === coordSet.size; }; const calculateCenterOfMass = (coordSet) => { if (coordSet.size === 0) return null; let sumX = 0, sumY = 0; coordSet.forEach(coordString => { const { x, y } = stringToCoords(coordString); sumX += x; sumY += y; }); return { x: Math.round(sumX / coordSet.size), y: Math.round(sumY / coordSet.size) }; }; const findComponents = (coordSet) => { const components = []; const visited = new Set(); coordSet.forEach(startNode => { if (!visited.has(startNode)) { const currentComponent = new Set(); const queue = [startNode]; visited.add(startNode); currentComponent.add(startNode); while (queue.length > 0) { const currentCoordString = queue.shift(); const currentCoords = stringToCoords(currentCoordString); for (const adjacentCoords of getAdjacentCoords(currentCoords)) { const adjacentCoordString = coordsToString(adjacentCoords); if (coordSet.has(adjacentCoordString) && !visited.has(adjacentCoordString)) { visited.add(adjacentCoordString); currentComponent.add(adjacentCoordString); queue.push(adjacentCoordString); } } } components.push(currentComponent); } }); return components; }; function findBestMove(targetShapeCoordsSet, allCellCoordsSet, centerCoords, componentMap) { let bestMove = null; let neutralMove = null; let maxScore = -Infinity; const CONNECTIVITY_BONUS = 1000; // Defined locally or pass as argument if needed elsewhere for (const sourceCoordString of targetShapeCoordsSet) { const sourceCoords = stringToCoords(sourceCoordString); const currentDistance = manhattanDistance(sourceCoords, centerCoords); const sourceComponentId = componentMap.get(sourceCoordString); for (const adjacentCoords of getAdjacentCoords(sourceCoords)) { const adjacentCoordString = coordsToString(adjacentCoords); if (allCellCoordsSet.has(adjacentCoordString) && !targetShapeCoordsSet.has(adjacentCoordString)) { const targetCoords = adjacentCoords; const newDistance = manhattanDistance(targetCoords, centerCoords); const distanceReduction = currentDistance - newDistance; let connectivityBonus = 0; for (const neighborCoords of getAdjacentCoords(targetCoords)) { const neighborCoordString = coordsToString(neighborCoords); if (targetShapeCoordsSet.has(neighborCoordString) && neighborCoordString !== sourceCoordString && componentMap.get(neighborCoordString) !== sourceComponentId) { connectivityBonus = CONNECTIVITY_BONUS; break; } } const totalScore = distanceReduction + connectivityBonus; if (totalScore > maxScore) { maxScore = totalScore; bestMove = { srcId: coordsToId(sourceCoords), tgtId: coordsToId(targetCoords), srcStr: sourceCoordString, tgtStr: adjacentCoordString, score: totalScore, bonus: connectivityBonus > 0 }; } else if (totalScore === 0 && !neutralMove) { neutralMove = { srcId: coordsToId(sourceCoords), tgtId: coordsToId(targetCoords), srcStr: sourceCoordString, tgtStr: adjacentCoordString, score: 0, bonus: false }; } } } } return (bestMove && maxScore > 0) ? bestMove : neutralMove; } async function triggerSort(shapeName) { try { const initialShapeCells = getShapeCells(shapeName); if (!initialShapeCells || initialShapeCells.length === 0) return; const allCellCoordsSet = getAllCellCoordsSet(); if (allCellCoordsSet.size === 0) return; let targetShapeCoordsSet = new Set(initialShapeCells.map(cell => coordsToString({ x: cell.x, y: cell.y }))); if (isConnected(targetShapeCoordsSet)) return; const centerCoords = calculateCenterOfMass(targetShapeCoordsSet); if (!centerCoords) return; const history = []; let iterations = 0; const MAX_ITERATIONS_FACTOR = 80; // Defined locally const maxIterations = targetShapeCoordsSet.size * MAX_ITERATIONS_FACTOR; while (!isConnected(targetShapeCoordsSet)) { iterations++; if (iterations > maxIterations) return; if (getCurrencyValue(GALLERY_MOTIVATION_KEY) < 1) { if (CONFIG.STOP_ON_CURRENCY_LOW) stopAutoSort(); return; } const components = findComponents(targetShapeCoordsSet); const componentMap = new Map(components.flatMap((componentSet, index) => [...componentSet].map(coordStr => [coordStr, index]) )); const move = findBestMove(targetShapeCoordsSet, allCellCoordsSet, centerCoords, componentMap); if (!move) { if (isConnected(targetShapeCoordsSet)) break; return; } const success = await simulateDragDrop(move.srcId, move.tgtId); if (!success) return; if (CONFIG.DOM_UPDATE_WAIT_MS > 0) { await new Promise(resolve => setTimeout(resolve, CONFIG.DOM_UPDATE_WAIT_MS)); } targetShapeCoordsSet.delete(move.srcStr); targetShapeCoordsSet.add(move.tgtStr); history.push({ sourceId: move.srcId, targetId: move.tgtId }); if (CONFIG.ENABLE_STATE_SYNC_CHECK) { const currentDomShapeCells = getShapeCells(shapeName); const domCoordsSet = new Set(currentDomShapeCells.map(c => coordsToString({ x: c.x, y: c.y }))); if (domCoordsSet.size !== targetShapeCoordsSet.size || ![...domCoordsSet].every(c => targetShapeCoordsSet.has(c))) { targetShapeCoordsSet = domCoordsSet; if (isConnected(targetShapeCoordsSet)) break; } } if (CONFIG.MOVE_DELAY_MS > 0) { await new Promise(resolve => setTimeout(resolve, CONFIG.MOVE_DELAY_MS)); } } } catch (error) { console.error(`Sort error for ${shapeName}:`, error); } } function createAutoButton() { const button = document.createElement('button'); button.className = 'v-btn v-btn--is-elevated v-btn--has-bg theme--light v-size--default primary'; button.style.margin = '5px'; button.textContent = '自动'; button.id = AUTO_SORT_BUTTON_ID; button.addEventListener('click', toggleAutoSort); return button; } function toggleAutoSort() { if (isAutoSorting) { stopAutoSort(); } else { startAutoSort(); } } function startAutoSort() { if (isAutoSorting) return; isAutoSorting = true; const autoButton = document.getElementById(AUTO_SORT_BUTTON_ID); if (autoButton) autoButton.textContent = '停止'; let isCurrentlySorting = false; const autoSortIteration = async () => { if (!isAutoSorting) return; if (!checkGameBoardExists()) { stopAutoSort(); return; } const currentMotivation = getCurrencyValue(GALLERY_MOTIVATION_KEY); if (currentMotivation < 1 && CONFIG.STOP_ON_CURRENCY_LOW) { stopAutoSort(); return; } if (isCurrentlySorting) return; isCurrentlySorting = true; try { // Merged calculateMovableShapeStats logic const gridStats = {}; const tableElement = document.querySelector(TABLE_LG_SELECTOR) || document.querySelector(TABLE_SM_SELECTOR); if (tableElement) { const cells = tableElement.querySelectorAll(CELL_SELECTOR); cells.forEach(cell => { const iconElement = cell.querySelector("i.mdi"); const hasShapeBackground = cell.querySelector(SHAPE_BG_SELECTOR) !== null; if (iconElement && hasShapeBackground) { const shape = Array.from(iconElement.classList) .find(c => c.startsWith(MDI_PREFIX)) ?.substring(MDI_PREFIX.length); if (shape && ALLOWED_SHAPES.includes(shape)) { gridStats[shape] = (gridStats[shape] || 0) + 1; } } }); } // End of merged logic const eligibleShapes = Object.entries(gridStats) .filter(([, count]) => count >= CONFIG.MIN_SHAPE_COUNT_FOR_AUTO_SORT); if (CONFIG.SORT_ASCENDING) { eligibleShapes.sort((a, b) => { if (a[1] !== b[1]) return a[1] - b[1]; return ALLOWED_SHAPES.indexOf(a[0]) - ALLOWED_SHAPES.indexOf(b[0]); }); } else { eligibleShapes.sort((a, b) => { if (a[1] !== b[1]) return b[1] - a[1]; return ALLOWED_SHAPES.indexOf(a[0]) - ALLOWED_SHAPES.indexOf(b[0]); }); } if (eligibleShapes.length > 0) { const bestShapeToArrange = eligibleShapes[0][0]; await triggerSort(bestShapeToArrange); await new Promise(resolve => setTimeout(resolve, 100)); if (getCurrencyValue(GALLERY_MOTIVATION_KEY) > 1) { clickShapeOnBoard(bestShapeToArrange); } await new Promise(resolve => setTimeout(resolve, 300)); } else { if (canAffordReroll()) { buyShapeReroll(); await new Promise(resolve => setTimeout(resolve, 300)); } else { if (CONFIG.STOP_ON_CURRENCY_LOW) { stopAutoSort(); } } } } catch (error) { console.error("Auto sort iteration error:", error); } finally { isCurrentlySorting = false; } }; autoSortIteration(); // Run once immediately autoSortIntervalId = setInterval(autoSortIteration, CONFIG.AUTO_SORT_INTERVAL_MS); } function stopAutoSort() { if (!isAutoSorting) return; clearInterval(autoSortIntervalId); autoSortIntervalId = null; isAutoSorting = false; const autoButton = document.getElementById(AUTO_SORT_BUTTON_ID); if (autoButton) autoButton.textContent = '自动'; } function clickShapeOnBoard(shapeName) { const tableElement = document.querySelector(TABLE_LG_SELECTOR) || document.querySelector(TABLE_SM_SELECTOR); if (!tableElement) return; const cells = tableElement.querySelectorAll(CELL_SELECTOR); for (const cell of cells) { const iconElement = cell.querySelector(`i.${MDI_PREFIX}${shapeName}`); const hasShapeBackground = cell.querySelector(SHAPE_BG_SELECTOR) !== null; if (iconElement && hasShapeBackground) { cell.click(); return; } } } function buyShapeReroll() { const buttons = document.querySelectorAll('button.v-btn'); for (const button of buttons) { if (button.querySelector('.mdi-cached')) { if (button.disabled || button.classList.contains('v-btn--disabled')) { return; } button.click(); return; } } } function canAffordReroll() { return getCurrencyValue(GALLERY_MOTIVATION_KEY) >= CONFIG.REROLL_COST_THRESHOLD; } function getCurrencyValue(currencyKey = GALLERY_MOTIVATION_KEY) { try { const vueInstanceElement = document.querySelector('.v-application') || document.body; const vueInstance = vueInstanceElement ? vueInstanceElement.__vue__ : null; if (vueInstance && vueInstance.$store && vueInstance.$store.state && vueInstance.$store.state.currency && vueInstance.$store.state.currency[currencyKey]) { return vueInstance.$store.state.currency[currencyKey].value || 0; } return 0; } catch (error) { return 0; } } const statsUpdateIntervalId = setInterval(checkAndUpdateStats, 500); console.log("Gooboo 画廊形状移动脚本已加载。"); })();