Greasy Fork is available in English.
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);
}
})();