Replaces Shapeshifter symbols with colored boxes containing numbers on the webpage, so it matches with the visuals on the Bakeru application. Also has Copy HTML button for convenience.
// ==UserScript==
// @name Bakeru Web Visualizer for Shapeshifter
// @namespace GreaseMonkey
// @version 1.0.2
// @description Replaces Shapeshifter symbols with colored boxes containing numbers on the webpage, so it matches with the visuals on the Bakeru application. Also has Copy HTML button for convenience.
// @match *://www.neopets.com/medieval/shapeshifter.phtml*
// @grant none
// @license MIT
// ==/UserScript==
/*
// #######\ ##\
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀## __##\ ## |
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡈⢯⡉⠓⠦⣄⡀⠀⠀⠀⠀⠀ ## | ## | ######\ ## | ##\ ######\ ######\ ##\ ##\ ⠀
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣉⠹⠷⠀⠀⠀⠙⢷⡀⠀⠀⠀⠀#######\ | \____##\ ## | ## |## __##\ ## __##\ ## | ## |
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⠀⠀⠀⠀⠀⠀⠀⢿⡇⠀⠀⠀ ## __##\ ####### |###### / ######## |## | \__|## | ## |
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⢈⡇⠀⠀⠀ ## | ## |## __## |## _##< ## ____|## | ## | ## |
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠹⠝⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀ ####### |\####### |## | \##\ \#######\ ## | \###### |
//⠀⠀⠀⠀⠀⠀⠀⣠⠞⠀⣀⣠⣤⣤⠄⠀⠀⢠⡏⠀⠀⠀⠀\_______/ \_______|\__| \__| \_______|\__| \______/
//⠀⠀⠀⠀⠀⠀⠚⠢⠼⠿⠟⢛⣾⠃⠀⠀⠀⢸⡇⠀⠀⠀
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⣻⠃⠀⠀⠀⠀⢸⡉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀.## ##: ### -
//⠀⠀⠀⠀⠀⠀⠀⠀⣰⢻⡷⠁⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀ ##+ ## ### ##% ###########-
//⠀⠀⠀⠀⠀⠀⠀⢰⢽⡟⠁⠀⠀⠀⠀⠀⠀⠀⣇⠀⠀⠀⠀⠀⠀⠀⠀ ##* ## ##- -## ##% =###
//⠀⠀⠀⠀⠀⠀⠀⢾⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡆⠀⠀⠀⠀⠀⠀:###= ## .####. %## ###########: :##%
//⠀⠀⠀⠀⠀⠀⠀⢸⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡀⠀⠀⠀ #####= #####: %## ##% :##########:
//⠀⠀⠀⠀⠀⠀⠀⠘⢧⣳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣷⠀⠀⠀⠀ # %#= ##: %#* # ##% :###%-. +###
//⠀⠀⠀⠀⠀⠀⠀⠀⠈⣷⣱⡀⠀⠀⠀⠀⣸⠀⠀⠀⠈⢻⣦⠀⠀⠀⠀ %#= ## %####: ### ###- :. =##
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣷⡙⣆⠀⠀⣾⠃⠀⠀⠀⠀⠈⢽⡆⠀⠀⠀ %#= ## ## *### ### : ###%###. %##
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡇⢷⡏⠃⢠⠇⠀⠀⣀⠄⠀⠀⠀⣿⡖⠀⠀ %#= ##- ### ### *### ##+ %#####
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⢨⠇⠀⡼⢀⠔⠊⠀⠀⠀⠀⠀⠘⣯⣄⢀ %#= -######## ##. -######=
//⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡇⣼⡀⣰⣷⠁⠀⠀⠀⠀⠀⠀⠀⠀⣇⢻⣧⡄
//⠀⠀⠀⠀⠀⠀⣀⣮⣿⣿⣿⣯⡭⢉⠟⠛⠳⢤⣄⣀⣀⣀⣀⡴⢠⠨⢻⣿ Web Visualizer Version 1.0.2
//⠀ ⢀⣾⣿⣿⣿⣿⢏⠓⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢨⣿
// ⣰⣿⣿⣿⣿⣿⣿⡱⠌⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢭⣾⠏ Script created by willnjohnson
// ⣰⡿⠟⠋⠛⢿⣿⣿⊊⠡⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⢀⣠⣼⡿⠋⠀
// ⠋⠁⠀⠀⠀⠀⠈⠑⠿⢶⣄⣀⣀⣀⣀⣀⣄⣤⡶⠿⠟⠋⠁⠀⠀⠀ (To be used alongside the Bakeru application.)
*/
(function () {
'use strict';
const colorMap = {
0: 'rgb(46, 139, 87)', // SeaGreen
1: 'rgb(178, 34, 34)', // Firebrick
2: 'rgb(255, 140, 0)', // DarkOrange
3: 'rgb(70, 130, 180)', // SteelBlue
4: 'rgb(139, 0, 139)' // DarkMagenta
};
// Helper to normalize filename by removing trailing _number before .gif
function normalizeFilename(filename) {
return filename.replace(/_\d+(?=\.gif$)/, '');
}
// Step 1: Find all symbols from the board (used to assign mappings)
const sourceTables = document.querySelectorAll('table[border="1"][bordercolor="gray"]');
const sourceImages = [];
for (const table of sourceTables) {
sourceImages.push(...Array.from(table.querySelectorAll('img')));
}
const symImages = sourceImages.filter(img => {
const src = img.getAttribute('src');
return src && src.endsWith('.gif') && !src.includes('arrow.gif');
});
let N = symImages.length - 2;
if (N < 0) {
console.log("Not enough symbols to map.");
return;
}
const mapping = {};
let assigned = 0;
for (const img of symImages) {
let filename = img.src.split('/').pop();
filename = normalizeFilename(filename);
if (!(filename in mapping)) {
mapping[filename] = N - assigned;
console.log(`${mapping[filename]}: ${filename}`);
assigned++;
}
}
// Step 2: Extract shape mask from first <table border="0" cellpadding="15" cellspacing="0" width="50" height="50">
function extractShapeMask() {
const shapeTable = document.querySelector('table[border="0"][cellpadding="15"][cellspacing="0"][width="50"][height="50"]');
if (!shapeTable) return [];
const innerTable = shapeTable.querySelector('table');
if (!innerTable) return [];
const mask = [];
for (const row of innerTable.rows) {
const rowPattern = [];
for (const cell of row.cells) {
const hasImg = cell.querySelector('img') !== null;
rowPattern.push(hasImg ? 1 : 0);
}
mask.push(rowPattern);
}
console.log('Extracted shape mask:', mask);
return mask;
}
const shapeMask = extractShapeMask();
const maskHeight = shapeMask.length;
const maskWidth = maskHeight > 0 ? shapeMask[0].length : 0;
// Step 3: Apply mapping of styles across ALL images in both sets of tables
const allTargetTables = [
...document.querySelectorAll('table[border="1"][bordercolor="gray"]'),
...document.querySelectorAll('table[align="center"][cellpadding="0"][cellspacing="0"][border="0"]')
];
// To help revert colors after hover, store original styles per cell
const originalStyles = new WeakMap();
for (const table of allTargetTables) {
const images = table.querySelectorAll('img');
for (const img of images) {
const src = img.getAttribute('src');
if (!src || !src.endsWith('.gif') || src.includes('arrow.gif')) continue;
let filename = src.split('/').pop();
filename = normalizeFilename(filename);
const num = mapping[filename];
if (num === undefined) continue;
// Create replacement box
const div = document.createElement('div');
div.textContent = num;
div.style.width = img.width + 'px';
div.style.height = img.height + 'px';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.style.color = 'white';
div.style.fontWeight = 'bold';
div.style.border = '1px solid gray';
div.style.backgroundColor = colorMap[num % 5];
div.style.fontFamily = 'Arial, sans-serif';
div.style.fontSize = '16px';
div.style.boxSizing = 'border-box';
img.style.display = 'none';
img.parentNode.insertBefore(div, img);
}
}
// Step 4: Hover effect on main board table cells
const boardTable = document.querySelector('table[align="center"][cellpadding="0"][cellspacing="0"][border="0"]');
if (!boardTable) return;
const boardRows = Array.from(boardTable.rows);
const boardHeight = boardRows.length;
const boardWidth = boardHeight > 0 ? boardRows[0].cells.length : 0;
function getDivInCell(row, col) {
if (row < 0 || row >= boardHeight) return null;
const cells = boardRows[row].cells;
if (!cells || col < 0 || col >= cells.length) return null;
return cells[col].querySelector('div');
}
function saveOriginalStyles(div) {
if (!originalStyles.has(div)) {
originalStyles.set(div, {
bg: div.style.backgroundColor,
fg: div.style.color
});
}
}
for (let r = 0; r < boardHeight; r++) {
for (let c = 0; c < boardWidth; c++) {
const cellDiv = getDivInCell(r, c);
if (!cellDiv) continue;
cellDiv.style.cursor = 'pointer';
cellDiv.addEventListener('mouseenter', () => {
if (r + maskHeight > boardHeight || c + maskWidth > boardWidth) return;
for (let dy = 0; dy < maskHeight; dy++) {
for (let dx = 0; dx < maskWidth; dx++) {
if (shapeMask[dy][dx] === 1) {
const targetDiv = getDivInCell(r + dy, c + dx);
if (!targetDiv) continue;
saveOriginalStyles(targetDiv);
targetDiv.style.backgroundColor = 'white';
targetDiv.style.color = 'black';
}
}
}
});
cellDiv.addEventListener('mouseleave', () => {
if (r + maskHeight > boardHeight || c + maskWidth > boardWidth) return;
for (let dy = 0; dy < maskHeight; dy++) {
for (let dx = 0; dx < maskWidth; dx++) {
if (shapeMask[dy][dx] === 1) {
const targetDiv = getDivInCell(r + dy, c + dx);
if (!targetDiv) continue;
const orig = originalStyles.get(targetDiv);
if (orig) {
targetDiv.style.backgroundColor = orig.bg;
targetDiv.style.color = orig.fg;
}
}
}
}
});
}
}
// Display a Copy HTML button (for convenience, so user doesn't have to open console to copy HTML)
const contentTd = document.querySelector('td.content');
if (contentTd) {
if (getComputedStyle(contentTd).position === 'static') {
contentTd.style.position = 'relative';
}
// Copy HTML button
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy HTML';
Object.assign(copyBtn.style, {
position: 'absolute',
top: '10px',
right: '155px',
backgroundColor: '#007FFF',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '8px 16px',
fontWeight: 'bold',
cursor: 'pointer',
zIndex: 10000,
boxShadow: '0 2px 5px rgba(0,0,0,0.3)',
userSelect: 'none',
});
// Bakeru on Github button
const githubBtn = document.createElement('button');
githubBtn.textContent = 'Bakeru (Solver)';
Object.assign(githubBtn.style, {
position: 'absolute',
top: '10px',
right: '10px',
backgroundColor: '#333333',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '8px 16px',
fontWeight: 'bold',
cursor: 'pointer',
zIndex: 10000,
boxShadow: '0 2px 5px rgba(0,0,0,0.3)',
userSelect: 'none',
});
copyBtn.addEventListener('mouseenter', () => Object.assign(copyBtn.style, { backgroundColor: '#003FBF' }));
copyBtn.addEventListener('mouseleave', () => Object.assign(copyBtn.style, { backgroundColor: '#007FFF' }));
githubBtn.addEventListener('mouseenter', () => Object.assign(githubBtn.style, { backgroundColor: '#000000' }));
githubBtn.addEventListener('mouseleave', () => Object.assign(githubBtn.style, { backgroundColor: '#333333' }));
copyBtn.addEventListener('click', () => {
try {
const htmlToCopy = contentTd.innerHTML;
navigator.clipboard.writeText(htmlToCopy).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => (copyBtn.textContent = 'Copy HTML'), 2000);
}, () => {
alert('Failed to copy HTML.');
});
} catch (e) {
alert('Clipboard API not supported.');
}
});
githubBtn.addEventListener('click', () => {
window.open('https://github.com/willnjohnson/Bakeru', '_blank', 'noopener,noreferrer');
});
contentTd.appendChild(copyBtn);
contentTd.appendChild(githubBtn);
}
})();