// ==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 画廊形状移动脚本已加载。");
})();