FortunaMaps Enhanced Preview – PNG Walls, Portals & Boosts

Fetches PNG/JSON map data, draws floor tiles using different images for boosts and portals, computes walls from PNG hex codes, overlays gate tiles using JSON fields, and updates the preview image. The texture pack dropdown is inserted directly below a specific dropdown and now includes a search bar with a scrollbar.

目前為 2025-02-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name         FortunaMaps Enhanced Preview – PNG Walls, Portals & Boosts
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Fetches PNG/JSON map data, draws floor tiles using different images for boosts and portals, computes walls from PNG hex codes, overlays gate tiles using JSON fields, and updates the preview image. The texture pack dropdown is inserted directly below a specific dropdown and now includes a search bar with a scrollbar.
// @match        https://fortunatemaps.herokuapp.com/map/*
// @run-at       document-end
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    console.log("[FM] Script started");

    // Base URL for texture images.
    const baseUrl = "https://static.koalabeast.com";

    // ----------------------------
    // Configuration & Global Data
    // ----------------------------
    const config = {
        tileSize: 40,         // Final tile is 40x40px.
        quadSize: 20          // Each wall quadrant is 20x20px.
    };

    // Mapping for floor/feature tiles (except boosts/portals).
    // Keys are hex codes (without '#').
    const floorTiles = {
        "d4d4d4": { y: 4, x: 13 }, // floor
        "808000": { y: 1, x: 13 }, // yellow flag
        "ff0000": { y: 1, x: 14 }, // red flag
        "0000ff": { y: 1, x: 15 }, // blue flag
        "373737": { y: 0, x: 12 }, // spike
        "202020": { y: 0, x: 13 }, // blackhole
        "b90000": { y: 5, x: 14 }, // redgoal
        "190094": { y: 5, x: 15 }, // bluegoal
        "dcbaba": { y: 4, x: 14 }, // redtt
        "bbb8dd": { y: 4, x: 15 }, // bluett
        "dcdcba": { y: 5, x: 13 }, // yellowtt
        "00ff00": { y: 4, x: 12 }, // pup
        "b97a57": { y: 6, x: 13 }, // button (tiles.png)
        "ff8000": { y: 1, x: 12 }, // bomb (tiles.png)
        // Gates – these keys will be formed as "007500_empty", etc.
        "007500_empty": { y: 3, x: 12 },
        "007500_green": { y: 3, x: 13 },
        "007500_red":   { y: 3, x: 14 },
        "007500_blue":  { y: 3, x: 15 }
    };

    // The boost and portal colors are handled separately.
    // For boosts:
    // "ffff00" -> boost, "ff7373" -> red boost, "7373ff" -> blue boost.
    // For portals:
    // "cac000" -> portal, "cc3300" -> red portal, "0066cc" -> blue portal.

    // Texture packs definition using relative paths.
    const textures = {
        texturePacks: [
            {
                name: "Classic",
                author: "LuckySpammer",
                url: "classic",
                tiles: "/textures/classic/tiles.png",
                speedpad: "/textures/classic/speedpad.png",
                speedpadRed: "/textures/classic/speedpadred.png",
                speedpadBlue: "/textures/classic/speedpadblue.png",
                portal: "/textures/classic/portal.png",
                portalRed: "/textures/classic/portalred.png",
                portalBlue: "/textures/classic/portalblue.png"
            },
            {
                name: "Sniper Pack",
                author: "DOKE",
                url: "sniperpack",
                tiles: "/textures/sniperpack/tiles.png",
                speedpad: "/textures/sniperpack/speedpad.png",
                speedpadRed: "/textures/sniperpack/speedpadred.png",
                speedpadBlue: "/textures/sniperpack/speedpadblue.png",
                portal: "/textures/sniperpack/portal.png",
                portalRed: "/textures/sniperpack/portalred.png",
                portalBlue: "/textures/sniperpack/portalblue.png"
            }
        ]
    };

    // Wall types: keys are hex codes (from PNG) and values hold a wallSolids bitmask.
    const wallTypes = {
        "787878": { wallSolids: 0xff },
        "804070": { wallSolids: 0x2d },
        "408050": { wallSolids: 0xd2 },
        "405080": { wallSolids: 0x4b },
        "807040": { wallSolids: 0xb4 }
    };

    // Wall quadrant texture coordinates (multiplied by 40 when drawn).
    const quadrantCoords = {
      "132": [10.5, 7.5],
      "232": [11, 7.5],
      "332": [11, 8],
      "032": [10.5, 8],
      "132d": [0.5, 3.5],
      "232d": [1, 3.5],
      "032d": [0.5, 4],
      "143": [4.5, 9.5],
      "243": [5, 9.5],
      "343": [5, 10],
      "043": [4.5, 10],
      "143d": [1.5, 2.5],
      "243d": [2, 2.5],
      "043d": [1.5, 3],
      "154": [6.5, 9.5],
      "254": [7, 9.5],
      "354": [7, 10],
      "054": [6.5, 10],
      "154d": [9.5, 2.5],
      "254d": [10, 2.5],
      "354d": [10, 3],
      "165": [0.5, 7.5],
      "265": [1, 7.5],
      "365": [1, 8],
      "065": [0.5, 8],
      "165d": [10.5, 3.5],
      "265d": [11, 3.5],
      "365d": [11, 4],
      "176": [1.5, 6.5],
      "276": [2, 6.5],
      "376": [2, 7],
      "076": [1.5, 7],
      "276d": [9, 1.5],
      "376d": [9, 2],
      "076d": [8.5, 2],
      "107": [6.5, 8.5],
      "207": [7, 8.5],
      "307": [7, 9],
      "007": [6.5, 9],
      "207d": [11, 1.5],
      "307d": [11, 2],
      "007d": [10.5, 2],
      "110": [4.5, 8.5],
      "210": [5, 8.5],
      "310": [5, 9],
      "010": [4.5, 9],
      "110d": [0.5, 1.5],
      "310d": [1, 2],
      "010d": [0.5, 2],
      "121": [9.5, 6.5],
      "221": [10, 6.5],
      "321": [10, 7],
      "021": [9.5, 7],
      "121d": [2.5, 1.5],
      "321d": [3, 2],
      "021d": [2.5, 2],
      "142": [1.5, 7.5],
      "242": [2, 7.5],
      "042": [1.5, 8],
      "142d": [10.5, 0.5],
      "242d": [11, 0.5],
      "042d": [10.5, 1],
      "153": [5.5, 6.5],
      "253": [6, 6.5],
      "353": [6, 7],
      "053": [5.5, 7],
      "153d": [5.5, 0.5],
      "253d": [6, 0.5],
      "164": [9.5, 7.5],
      "264": [10, 7.5],
      "364": [10, 8],
      "164d": [0.5, 0.5],
      "264d": [1, 0.5],
      "364d": [1, 1],
      "175": [4.5, 5.5],
      "275": [5, 5.5],
      "375": [5, 6],
      "075": [4.5, 6],
      "275d": [7, 1.5],
      "375d": [7, 2],
      "206": [4.5, 9.5],
      "306": [4.5, 10],
      "006": [3.5, 10],
      "206d": [2, 3.5],
      "306d": [2, 4],
      "006d": [1.5, 4],
      "117": [5.5, 2.5],
      "217": [6, 2.5],
      "317": [6, 4],
      "017": [5.5, 4],
      "317d": [6, 3],
      "017d": [5.5, 3],
      "120": [7.5, 9.5],
      "320": [8, 10],
      "020": [7.5, 10],
      "120d": [9.5, 3.5],
      "320d": [10, 4],
      "020d": [9.5, 4],
      "131": [6.5, 5.5],
      "231": [7, 5.5],
      "331": [7, 6],
      "031": [6.5, 6],
      "131d": [4.5, 1.5],
      "031d": [4.5, 2],
      "141": [7.5, 8.5],
      "241": [8, 8.5],
      "323": [4, 5],
      "041": [7.5, 9],
      "141d": [8.5, 3.5],
      "041d": [8.5, 4],
      "152": [8.5, 7.5],
      "252": [9, 7.5],
      "334": [2, 0],
      "052": [8.5, 8],
      "152d": [3.5, 0.5],
      "252d": [4, 0.5],
      "163": [2.5, 7.5],
      "263": [3, 7.5],
      "363": [3, 8],
      "045": [9.5, 0],
      "163d": [7.5, 0.5],
      "263d": [8, 0.5],
      "174": [3.5, 8.5],
      "274": [4, 8.5],
      "374": [4, 9],
      "056": [7.5, 5],
      "274d": [3, 3.5],
      "374d": [3, 4],
      "167": [7.5, 6.5],
      "205": [10, 8.5],
      "305": [10, 9],
      "005": [9.5, 9],
      "205d": [2, 0.5],
      "305d": [2, 1],
      "170": [6.5, 7.5],
      "216": [9, 9.5],
      "316": [9, 10],
      "016": [8.5, 10],
      "316d": [10, 5],
      "016d": [9.5, 5],
      "127": [2.5, 9.5],
      "201": [5, 7.5],
      "327": [3, 10],
      "027": [2.5, 10],
      "327d": [2, 5],
      "027d": [1.5, 5],
      "130": [1.5, 8.5],
      "212": [4, 6.5],
      "330": [2, 9],
      "030": [1.5, 9],
      "130d": [9.5, 0.5],
      "030d": [9.5, 1],
      "151": [10.5, 9.5],
      "251": [11, 9.5],
      "324": [0, 7],
      "051": [10.5, 10],
      "151d": [10.5, 4.5],
      "324d": [0, 0],
      "162": [8.5, 10.5],
      "262": [9, 10.5],
      "335": [6, 8],
      "035": [5.5, 8],
      "162d": [3.5, 2.5],
      "262d": [8, 2.5],
      "173": [0.5, 9.5],
      "273": [1, 9.5],
      "373": [1, 10],
      "046": [11.5, 7],
      "046d": [11.5, 0],
      "273d": [1, 4.5],
      "157": [11.5, 8.5],
      "204": [0, 5.5],
      "304": [0, 5],
      "057": [11.5, 9],
      "204d": [0, 4.5],
      "304d": [0, 6],
      "160": [11.5, 7.5],
      "215": [8, 6.5],
      "315": [8, 7],
      "015": [7.5, 7],
      "160d": [2.5, 4.5],
      "315d": [9, 3],
      "171": [5.5, 10.5],
      "271": [6, 10.5],
      "326": [6, 5],
      "026": [5.5, 5],
      "326d": [7, 5],
      "026d": [4.5, 5],
      "137": [3.5, 6.5],
      "202": [0, 7.5],
      "337": [4, 7],
      "037": [3.5, 7],
      "202d": [9, 4.5],
      "037d": [2.5, 3],
      "140": [11.5, 5.5],
      "213": [0, 8.5],
      "313": [0, 9],
      "040": [11.5, 5],
      "140d": [11.5, 4.5],
      "040d": [11.5, 6],
      "161": [9.5, 10.5],
      "261": [10, 10.5],
      "325": [9, 6],
      "025": [8.5, 6],
      "161d": [3.5, 1.5],
      "325d": [4, 1],
      "172": [1.5, 10.5],
      "272": [2, 10.5],
      "336": [3, 6],
      "036": [2.5, 6],
      "036d": [7.5, 1],
      "272d": [8, 1.5],
      "147": [4.5, 7.5],
      "203": [4, 3.5],
      "303": [4, 4],
      "047": [4.5, 8],
      "047d": [8.5, 5],
      "203d": [8, 4.5],
      "150": [7.5, 3.5],
      "214": [7, 7.5],
      "314": [7, 8],
      "050": [7.5, 4],
      "150d": [3.5, 4.5],
      "314d": [3, 5],
      "100": [5.5, 5.5],
      "200": [6, 5.5],
      "300": [6, 6],
      "000": [5.5, 6],
      "100d": [5.5, 8.5],
      "200d": [6, 8.5],
      "300d": [6, 10],
      "000d": [5.5, 10]
    };

    // ----------------------------
    // Global Image Variables
    // ----------------------------
    let images = {
        tile: null,
        speedpad: null,
        speedpadRed: null,
        speedpadBlue: null,
        portal: null,
        portalRed: null,
        portalBlue: null
    };

    // ----------------------------
    // JSON Gate Data Explanation
    // ----------------------------
    // Expected JSON structure:
    // {
    //   "info": {},
    //   "switches": {},
    //   "fields": {
    //     "8,6": { "defaultState": "on" },
    //     "8,7": { "defaultState": "red" },
    //     "8,8": { "defaultState": "blue" }
    //   },
    //   "portals": {},
    //   "marsballs": [ { "y": 7, "x": 12 } ]
    // }
    // For any cell with a gate placeholder ("007500"), if its "x,y" is not in fields, treat it as "empty".

    // ----------------------------
    // Global Variables
    // ----------------------------
    let originalPNGImage = null;
    let mapJSONData = null;
    let wallMap = [];     // 2D array of wall bitmasks.
    let currentTexturePack = textures.texturePacks[0];

    // ----------------------------
    // Utility: waitForElement
    // ----------------------------
    function waitForElement(selector, callback, timeout = 10000) {
        const start = Date.now();
        (function check() {
            const el = document.querySelector(selector);
            if (el) {
                console.log(`[FM] Found element for selector "${selector}"`);
                callback(el);
            } else if (Date.now() - start > timeout) {
                console.warn(`[FM] Timeout waiting for element: ${selector}`);
            } else {
                setTimeout(check, 200);
            }
        })();
    }

    // ----------------------------
    // Utility Functions
    // ----------------------------
    function rgbToHex(r, g, b) {
        return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    }

    // Updated loadImage: If the URL starts with "/", prepend baseUrl.
    function loadImage(url) {
        if (url.startsWith("/")) {
            url = baseUrl + url;
        }
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = "anonymous";
            img.onload = () => {
                console.log(`[FM] Loaded image: ${url}`);
                resolve(img);
            };
            img.onerror = (e) => {
                console.error(`[FM] Error loading image: ${url}`, e);
                reject(e);
            };
            img.src = url;
        });
    }

    // ----------------------------
    // getFloorTile: Selects proper image and coordinates based on the hex key.
    // ----------------------------
    function getFloorTile(key) {
        // Boosts:
        if (key === "ffff00") {
            return { image: images.speedpad, y: 0, x: 0 };
        } else if (key === "ff7373") {
            return { image: images.speedpadRed, y: 0, x: 0 };
        } else if (key === "7373ff") {
            return { image: images.speedpadBlue, y: 0, x: 0 };
        }
        // Portals:
        else if (key === "cac000") {
            return { image: images.portal, x: 0, y: 0 };
        } else if (key === "cc3300") {
            return { image: images.portalRed, x: 0, y: 0 };
        } else if (key === "0066cc") {
            return { image: images.portalBlue, x: 0, y: 0 };
        }
        // For all others, use floorTiles mapping and the tile image.
        else if (floorTiles.hasOwnProperty(key)) {
            return { image: images.tile, x: floorTiles[key].x, y: floorTiles[key].y };
        } else {
            return null;
        }
    }

    // ----------------------------
    // Wall Drawing Functions
    // ----------------------------
    function wallSolidsAt(col, row) {
        if (col < 0 || row < 0 || row >= wallMap.length || col >= wallMap[0].length) return 0;
        return wallMap[row][col];
    }

    function drawWallTile(ctx, col, row) {
        const solids = wallMap[row][col];
        if (!solids) return;
        for (let q = 0; q < 4; q++) {
            const mask = (solids >> (q << 1)) & 3;
            if (mask === 0) continue;
            const cornerX = col + ((q & 2) === 0 ? 1 : 0);
            const cornerY = row + (((q + 1) & 2) === 0 ? 0 : 1);
            let aroundCorner =
                (wallSolidsAt(cornerX, cornerY) & 0xc0) |
                (wallSolidsAt(cornerX - 1, cornerY) & 0x03) |
                (wallSolidsAt(cornerX - 1, cornerY - 1) & 0x0c) |
                (wallSolidsAt(cornerX, cornerY - 1) & 0x30);
            aroundCorner |= (aroundCorner << 8);
            const startDirection = q * 2 + 1;
            let cwSteps = 0;
            while (cwSteps < 8 && (aroundCorner & (1 << (startDirection + cwSteps)))) { cwSteps++; }
            let ccwSteps = 0;
            while (ccwSteps < 8 && (aroundCorner & (1 << (startDirection + 7 - ccwSteps)))) { ccwSteps++; }
            const hasChip = (mask === 3 && (((solids | (solids << 8)) >> ((q + 2) << 1)) & 3) === 0);
            let solidStart, solidEnd;
            if (cwSteps === 8) {
                solidStart = solidEnd = 0;
            } else {
                solidEnd = (startDirection + cwSteps + 4) % 8;
                solidStart = (startDirection - ccwSteps + 12) % 8;
            }
            const key = `${q}${solidStart}${solidEnd}${hasChip ? "d" : ""}`;
            const coords = quadrantCoords[key] || [5.5, 5.5];
            let destX = col * config.tileSize;
            let destY = row * config.tileSize;
            if (q === 0) destX += config.quadSize;
            else if (q === 1) { destX += config.quadSize; destY += config.quadSize; }
            else if (q === 2) destY += config.quadSize;
            const srcX = coords[0] * 40;
            const srcY = coords[1] * 40;
            ctx.drawImage(images.tile, srcX, srcY, config.quadSize, config.quadSize,
                          destX, destY, config.quadSize, config.quadSize);
        }
    }

    // ----------------------------
    // Map Data Fetching & Processing
    // ----------------------------
    function fetchMapData(mapCode) {
        const pngURL = `https://fortunatemaps.herokuapp.com/png/${mapCode}.png`;
        const jsonURL = `https://fortunatemaps.herokuapp.com/json/${mapCode}.json`;
        console.log(`[FM] Fetching map data for code: ${mapCode}`);
        return Promise.all([
            fetch(pngURL)
                .then(res => res.blob())
                .then(blob => {
                    console.log("[FM] PNG fetched successfully");
                    return URL.createObjectURL(blob);
                }),
            fetch(jsonURL)
                .then(res => res.json())
                .then(json => {
                    console.log("[FM] JSON fetched successfully:", json);
                    return json;
                })
        ]);
    }

    async function processMap() {
        if (!originalPNGImage) return;
        console.log("[FM] Starting map processing...");

        // Load all images from the current texture pack.
        try {
            images.tile = await loadImage(currentTexturePack.tiles);
            images.speedpad = await loadImage(currentTexturePack.speedpad);
            images.speedpadRed = await loadImage(currentTexturePack.speedpadRed);
            images.speedpadBlue = await loadImage(currentTexturePack.speedpadBlue);
            images.portal = await loadImage(currentTexturePack.portal);
            images.portalRed = await loadImage(currentTexturePack.portalRed);
            images.portalBlue = await loadImage(currentTexturePack.portalBlue);
        } catch(e) {
            console.error("[FM] Error loading texture images", e);
            return;
        }
        console.log("[FM] All texture images loaded");

        // Create an offscreen canvas to read the 1px-per-tile PNG.
        const offCanvas = document.createElement('canvas');
        offCanvas.width = originalPNGImage.width;
        offCanvas.height = originalPNGImage.height;
        const offCtx = offCanvas.getContext('2d');
        offCtx.drawImage(originalPNGImage, 0, 0);
        console.log("[FM] Offscreen canvas drawn");
        const imageData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height);
        const data = imageData.data;

        // Build wallMap: for each pixel, if its hex is in wallTypes, store the bitmask.
        wallMap = [];
        for (let y = 0; y < offCanvas.height; y++) {
            wallMap[y] = [];
            for (let x = 0; x < offCanvas.width; x++) {
                const idx = (y * offCanvas.width + x) * 4;
                const r = data[idx], g = data[idx + 1], b = data[idx + 2], a = data[idx + 3];
                const hex = rgbToHex(r, g, b).toLowerCase();
                if (a === 0) {
                    wallMap[y][x] = 0;
                } else if (wallTypes.hasOwnProperty(hex)) {
                    wallMap[y][x] = wallTypes[hex].wallSolids;
                } else {
                    wallMap[y][x] = 0;
                }
            }
        }
        console.log("[FM] wallMap built:", wallMap);

        // Create final canvas.
        const finalCanvas = document.createElement('canvas');
        finalCanvas.width = offCanvas.width * config.tileSize;
        finalCanvas.height = offCanvas.height * config.tileSize;
        const finalCtx = finalCanvas.getContext('2d');
        finalCtx.imageSmoothingEnabled = false;
        console.log("[FM] Final canvas created");

        // *** Draw floor background for every cell ***
        // This ensures transparent edges on other tiles show the floor underneath.
        const defaultFloorTile = getFloorTile("d4d4d4");
        if (defaultFloorTile) {
            const sx = defaultFloorTile.x * config.tileSize;
            const sy = defaultFloorTile.y * config.tileSize;
            for (let y = 0; y < offCanvas.height; y++) {
                for (let x = 0; x < offCanvas.width; x++) {
                    finalCtx.drawImage(defaultFloorTile.image, sx, sy, config.tileSize, config.tileSize,
                                       x * config.tileSize, y * config.tileSize, config.tileSize, config.tileSize);
                }
            }
        }

        // Draw floor and gate tiles.
        for (let y = 0; y < offCanvas.height; y++) {
            for (let x = 0; x < offCanvas.width; x++) {
                const idx = (y * offCanvas.width + x) * 4;
                const r = data[idx], g = data[idx + 1], b = data[idx + 2], a = data[idx + 3];
                if (a === 0) continue;
                const hex = rgbToHex(r, g, b).toLowerCase();
                // Skip if this pixel is a wall.
                if (wallTypes.hasOwnProperty(hex)) continue;
                let tileSource = null;
                if (hex === "007500") {
                    // Gate tile: use JSON fields to decide state.
                    const fieldKey = `${x},${y}`;
                    let state = "empty";
                    if (mapJSONData && mapJSONData.fields && mapJSONData.fields[fieldKey]) {
                        let ds = mapJSONData.fields[fieldKey].defaultState;
                        state = (ds === "on") ? "green" : ds;
                    }
                    const tileKey = "007500_" + state;
                    tileSource = getFloorTile(tileKey);
                } else {
                    tileSource = getFloorTile(hex);
                }
                if (tileSource) {
                    const sx = tileSource.x * config.tileSize;
                    const sy = tileSource.y * config.tileSize;
                    finalCtx.drawImage(tileSource.image, sx, sy, config.tileSize, config.tileSize,
                                       x * config.tileSize, y * config.tileSize, config.tileSize, config.tileSize);
                } else {
                    // Fill with black for any tile that isn't recognized.
                    finalCtx.fillStyle = "#000000";
                    finalCtx.fillRect(x * config.tileSize, y * config.tileSize, config.tileSize, config.tileSize);
                }
            }
        }
        console.log("[FM] Floor and gate tiles drawn");

        // Overlay wall tiles.
        for (let y = 0; y < wallMap.length; y++) {
            for (let x = 0; x < wallMap[0].length; x++) {
                if (wallMap[y][x] !== 0) {
                    drawWallTile(finalCtx, x, y);
                }
            }
        }
        console.log("[FM] Wall tiles overlaid");

        // Update the preview image.
        waitForElement('img.card-img-top', function(previewImg) {
            previewImg.src = finalCanvas.toDataURL();
            console.log("[FM] Preview image updated");
        });
    }

    // ----------------------------
    // UI: Texture Pack Dropdown
    // ----------------------------
    function addTextureDropdown() {
        // Locate the reference element: the existing dropdown with classes "dropdown w-100 mt-2"
        const referenceDiv = document.querySelector('main.container div.row.mb-2 div.col-md-4 div.card.stats-card div.card-body div.dropdown.w-100.mt-2');
        if (!referenceDiv) {
            console.error("[FM] Reference dropdown not found");
            return;
        }

        // Create a new container div for the texture dropdown.
        // The dropdown menu includes an input search bar at the top.
        const textureContainerHTML = `
            <div id="fm-texture-container">
                <div class="dropdown w-100 mt-2" id="fm-texture-dropdown">
                    <button class="btn btn-primary w-100 dropdown-toggle" type="button" id="fm-dropdown-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        Texture Pack
                    </button>
                    <div class="dropdown-menu" aria-labelledby="fm-dropdown-button" style="max-height: 200px; overflow-y: auto; padding: 0.5rem;">
                        <div class="px-2">
                            <input type="text" id="fm-dropdown-search" class="form-control form-control-sm" placeholder="Search Texture Packs...">
                        </div>
                        <div id="fm-dropdown-items">
                            ${textures.texturePacks.map(tp => `<a class="dropdown-item" href="#" data-value="${tp.url}">${tp.name}</a>`).join('')}
                        </div>
                    </div>
                </div>
            </div>
        `;
        // Insert the new container directly after the reference div.
        referenceDiv.insertAdjacentHTML('afterend', textureContainerHTML);
        console.log("[FM] Texture pack dropdown container inserted directly below the reference dropdown");

        // Function to filter dropdown items based on search input.
        function filterItems() {
            const query = document.getElementById('fm-dropdown-search').value.toLowerCase();
            const items = document.querySelectorAll('#fm-dropdown-items .dropdown-item');
            items.forEach(item => {
                const text = item.textContent.toLowerCase();
                item.style.display = text.indexOf(query) > -1 ? "" : "none";
            });
        }

        // Add event listener for the search input.
        const searchInput = document.getElementById('fm-dropdown-search');
        searchInput.addEventListener('input', filterItems);

        // Add click event listeners for each texture pack option.
        const items = document.querySelectorAll('#fm-dropdown-items .dropdown-item');
        items.forEach(item => {
            item.addEventListener('click', function(e) {
                e.preventDefault();
                const selectedUrl = this.getAttribute('data-value');
                const selectedTexture = textures.texturePacks.find(tp => tp.url === selectedUrl);
                if (selectedTexture && selectedTexture.url !== currentTexturePack.url) {
                    currentTexturePack = selectedTexture;
                    console.log("[FM] Texture pack changed to:", selectedTexture.name);
                    processMap();
                    // Update the button text.
                    document.getElementById('fm-dropdown-button').textContent = selectedTexture.name;
                }
            });
        });
    }

    // ----------------------------
    // Main Execution
    // ----------------------------
    function main() {
        console.log("[FM] Main execution started");
        const mapCodeMatch = window.location.href.match(/\/map\/(\d+)/);
        if (!mapCodeMatch) {
            console.error("[FM] No map code found in URL");
            return;
        }
        const mapCode = mapCodeMatch[1];
        console.log("[FM] Map code:", mapCode);

        fetchMapData(mapCode)
            .then(([pngObjectUrl, jsonData]) => {
                mapJSONData = jsonData;
                console.log("[FM] Map JSON data stored");
                return loadImage(pngObjectUrl);
            })
            .then(img => {
                originalPNGImage = img;
                console.log("[FM] Original PNG loaded");
                processMap();
            })
            .catch(err => console.error("[FM] Error fetching or processing map data:", err));

        addTextureDropdown();
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", main);
    } else {
        main();
    }
})();