Bakeru Web Visualizer for Shapeshifter

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.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
    }
})();