Fortunate Maps Texture Preview

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. Your texture pack choice is remembered until you reset it.

// ==UserScript==
// @name         Fortunate Maps Texture Preview
// @namespace    http://tampermonkey.net/
// @version      0.7
// @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. Your texture pack choice is remembered until you reset it.
// @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).
    const floorTiles = {
        "d4d4d4": { y: 4, x: 13 },
        "808000": { y: 1, x: 13 },
        "ff0000": { y: 1, x: 14 },
        "0000ff": { y: 1, x: 15 },
        "373737": { y: 0, x: 12 },
        "202020": { y: 0, x: 13 },
        "b90000": { y: 5, x: 14 },
        "190094": { y: 5, x: 15 },
        "dcbaba": { y: 4, x: 14 },
        "bbb8dd": { y: 4, x: 15 },
        "dcdcba": { y: 5, x: 13 },
        "00ff00": { y: 4, x: 12 },
        "b97a57": { y: 6, x: 13 },
        "ff8000": { y: 1, x: 12 },
        "007500_empty": { y: 3, x: 12 },
        "007500_green": { y: 3, x: 13 },
        "007500_red":   { y: 3, x: 14 },
        "007500_blue":  { y: 3, x: 15 },
        "8080ff":       { y: 8, x: 14 },
        "8080ff":       { y: 7, x: 14 },
        "656500":       { y: 6, x: 14 },
    };
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","splats":"/textures/classic/splats.png","popularity":3259028385,"popularityScore":"3259"},{"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","splats":"/textures/sniperpack/splats.png","popularity":1437466820,"popularityScore":"1437"},{"name":"Muscle's Cup Gradients","author":"MuscleCups","url":"musclescupgradients","tiles":"/textures/musclescupgradients/tiles.png","speedpad":"/textures/musclescupgradients/speedpad.png","speedpadRed":"/textures/musclescupgradients/speedpadred.png","speedpadBlue":"/textures/musclescupgradients/speedpadblue.png","portal":"/textures/musclescupgradients/portal.png","portalRed":"/textures/musclescupgradients/portalred.png","portalBlue":"/textures/musclescupgradients/portalblue.png","splats":"/textures/musclescupgradients/splats.png","popularity":1436532153,"popularityScore":"1437"},{"name":"Coral Light","author":"MagicPigeon","url":"corallight","tiles":"/textures/corallight/tiles.png","speedpad":"/textures/corallight/speedpad.png","speedpadRed":"/textures/corallight/speedpadred.png","speedpadBlue":"/textures/corallight/speedpadblue.png","portal":"/textures/corallight/portal.png","portalRed":"/textures/corallight/portalred.png","portalBlue":"/textures/corallight/portalblue.png","splats":"/textures/corallight/splats.png","popularity":1161028272,"popularityScore":"1161"},{"name":"Muscle's Cup OG","author":"MuscleCups","url":"musclescupog","tiles":"/textures/musclescupog/tiles.png","speedpad":"/textures/musclescupog/speedpad.png","speedpadRed":"/textures/musclescupog/speedpadred.png","speedpadBlue":"/textures/musclescupog/speedpadblue.png","portal":"/textures/musclescupog/portal.png","portalRed":"/textures/musclescupog/portalred.png","portalBlue":"/textures/musclescupog/portalblue.png","splats":"/textures/musclescupog/splats.png","popularity":687443389,"popularityScore":"687"},{"name":"Coral","author":"MagicPigeon","url":"coral","tiles":"/textures/coral/tiles.png","speedpad":"/textures/coral/speedpad.png","speedpadRed":"/textures/coral/speedpadred.png","speedpadBlue":"/textures/coral/speedpadblue.png","portal":"/textures/coral/portal.png","portalRed":"/textures/coral/portalred.png","portalBlue":"/textures/coral/portalblue.png","splats":"/textures/coral/splats.png","popularity":457526831,"popularityScore":"458"},{"name":"MTBad","author":"mtbkr24","url":"mtbad","tiles":"/textures/mtbad/tiles.png","speedpad":"/textures/mtbad/speedpad.png","speedpadRed":"/textures/mtbad/speedpadred.png","speedpadBlue":"/textures/mtbad/speedpadblue.png","portal":"/textures/mtbad/portal.png","portalRed":"/textures/mtbad/portalred.png","portalBlue":"/textures/mtbad/portalblue.png","splats":"/textures/mtbad/splats.png","popularity":412084222,"popularityScore":"412"},{"name":"Flat","author":"why","url":"flat","tiles":"/textures/flat/tiles.png","speedpad":"/textures/flat/speedpad.png","speedpadRed":"/textures/flat/speedpadred.png","speedpadBlue":"/textures/flat/speedpadblue.png","portal":"/textures/flat/portal.png","portalRed":"/textures/flat/portalred.png","portalBlue":"/textures/flat/portalblue.png","splats":"/textures/flat/splats.png","popularity":401656196,"popularityScore":"402"},{"name":"MLTP Live","author":"Ron Spawnson","url":"mltplive","tiles":"/textures/mltplive/tiles.png","speedpad":"/textures/mltplive/speedpad.png","speedpadRed":"/textures/mltplive/speedpadred.png","speedpadBlue":"/textures/mltplive/speedpadblue.png","portal":"/textures/mltplive/portal.png","portalRed":"/textures/mltplive/portalred.png","portalBlue":"/textures/mltplive/portalblue.png","splats":"/textures/mltplive/splats.png","popularity":263983209,"popularityScore":"264"},{"name":"24K","author":"MagicPigeon","url":"24k","tiles":"/textures/24k/tiles.png","speedpad":"/textures/24k/speedpad.png","speedpadRed":"/textures/24k/speedpadred.png","speedpadBlue":"/textures/24k/speedpadblue.png","portal":"/textures/24k/portal.png","portalRed":"/textures/24k/portalred.png","portalBlue":"/textures/24k/portalblue.png","splats":"/textures/24k/splats.png","popularity":233605766,"popularityScore":"234"},{"name":"Precision Dark","author":"Peach Fuzz","url":"precisiondark","tiles":"/textures/precisiondark/tiles.png","speedpad":"/textures/precisiondark/speedpad.png","speedpadRed":"/textures/precisiondark/speedpadred.png","speedpadBlue":"/textures/precisiondark/speedpadblue.png","portal":"/textures/precisiondark/portal.png","portalRed":"/textures/precisiondark/portalred.png","portalBlue":"/textures/precisiondark/portalblue.png","splats":"/textures/precisiondark/splats.png","popularity":213202866,"popularityScore":"213"},{"name":"Plumb","author":"SuperTed","url":"plumb","tiles":"/textures/plumb/tiles.png","speedpad":"/textures/plumb/speedpad.png","speedpadRed":"/textures/plumb/speedpadred.png","speedpadBlue":"/textures/plumb/speedpadblue.png","portal":"/textures/plumb/portal.png","portalRed":"/textures/plumb/portalred.png","portalBlue":"/textures/plumb/portalblue.png","splats":"/textures/plumb/splats.png","popularity":192741223,"popularityScore":"193"},{"name":"Plique","author":"Despair","url":"plique","tiles":"/textures/plique/tiles.png","speedpad":"/textures/plique/speedpad.png","speedpadRed":"/textures/plique/speedpadred.png","speedpadBlue":"/textures/plique/speedpadblue.png","portal":"/textures/plique/portal.png","portalRed":"/textures/plique/portalred.png","portalBlue":"/textures/plique/portalblue.png","splats":"/textures/plique/splats.png","popularity":183995905,"popularityScore":"184"},{"name":"Isometric","author":"mtbkr24","url":"isometric","tiles":"/textures/isometric/tiles.png","speedpad":"/textures/isometric/speedpad.png","speedpadRed":"/textures/isometric/speedpadred.png","speedpadBlue":"/textures/isometric/speedpadblue.png","portal":"/textures/isometric/portal.png","portalRed":"/textures/isometric/portalred.png","portalBlue":"/textures/isometric/portalblue.png","splats":"/textures/isometric/splats.png","popularity":167044212,"popularityScore":"167"},{"name":"Sparkle","author":"MagicPigeon","url":"sparkle","tiles":"/textures/sparkle/tiles.png","speedpad":"/textures/sparkle/speedpad.png","speedpadRed":"/textures/sparkle/speedpadred.png","speedpadBlue":"/textures/sparkle/speedpadblue.png","portal":"/textures/sparkle/portal.png","portalRed":"/textures/sparkle/portalred.png","portalBlue":"/textures/sparkle/portalblue.png","splats":"/textures/sparkle/splats.png","popularity":166286098,"popularityScore":"166"},{"name":"CamsPP Dark","author":"Cam","url":"camsppdark","tiles":"/textures/camsppdark/tiles.png","speedpad":"/textures/camsppdark/speedpad.png","speedpadRed":"/textures/camsppdark/speedpadred.png","speedpadBlue":"/textures/camsppdark/speedpadblue.png","portal":"/textures/camsppdark/portal.png","portalRed":"/textures/camsppdark/portalred.png","portalBlue":"/textures/camsppdark/portalblue.png","splats":"/textures/camsppdark/splats.png","popularity":159743715,"popularityScore":"160"},{"name":"CamsPP Old","author":"Cam","url":"camsppold","tiles":"/textures/camsppold/tiles.png","speedpad":"/textures/camsppold/speedpad.png","speedpadRed":"/textures/camsppold/speedpadred.png","speedpadBlue":"/textures/camsppold/speedpadblue.png","portal":"/textures/camsppold/portal.png","portalRed":"/textures/camsppold/portalred.png","portalBlue":"/textures/camsppold/portalblue.png","splats":"/textures/camsppold/splats.png","popularity":157841341,"popularityScore":"158"},{"name":"CMYK","author":"MagicPigeon","url":"cmyk","tiles":"/textures/cmyk/tiles.png","speedpad":"/textures/cmyk/speedpad.png","speedpadRed":"/textures/cmyk/speedpadred.png","speedpadBlue":"/textures/cmyk/speedpadblue.png","portal":"/textures/cmyk/portal.png","portalRed":"/textures/cmyk/portalred.png","portalBlue":"/textures/cmyk/portalblue.png","splats":"/textures/cmyk/splats.png","popularity":144688179,"popularityScore":"145"},{"name":"CamsPP Light","author":"Cam","url":"camspplight","tiles":"/textures/camspplight/tiles.png","speedpad":"/textures/camspplight/speedpad.png","speedpadRed":"/textures/camspplight/speedpadred.png","speedpadBlue":"/textures/camspplight/speedpadblue.png","portal":"/textures/camspplight/portal.png","portalRed":"/textures/camspplight/portalred.png","portalBlue":"/textures/camspplight/portalblue.png","splats":"/textures/camspplight/splats.png","popularity":135009946,"popularityScore":"135"},{"name":"Electric","author":"Bug","url":"electric","tiles":"/textures/electric/tiles.png","speedpad":"/textures/electric/speedpad.png","speedpadRed":"/textures/electric/speedpadred.png","speedpadBlue":"/textures/electric/speedpadblue.png","portal":"/textures/electric/portal.png","portalRed":"/textures/electric/portalred.png","portalBlue":"/textures/electric/portalblue.png","splats":"/textures/electric/splats.png","popularity":127175831,"popularityScore":"127"},{"name":"Sketch+","author":"MagicPigeon","url":"sketch","tiles":"/textures/sketch/tiles.png","speedpad":"/textures/sketch/speedpad.png","speedpadRed":"/textures/sketch/speedpadred.png","speedpadBlue":"/textures/sketch/speedpadblue.png","portal":"/textures/sketch/portal.png","portalRed":"/textures/sketch/portalred.png","portalBlue":"/textures/sketch/portalblue.png","splats":"/textures/sketch/splats.png","popularity":126489052,"popularityScore":"126"},{"name":"Sharp","author":"MagicPigeon","url":"sharp","tiles":"/textures/sharp/tiles.png","speedpad":"/textures/sharp/speedpad.png","speedpadRed":"/textures/sharp/speedpadred.png","speedpadBlue":"/textures/sharp/speedpadblue.png","portal":"/textures/sharp/portal.png","portalRed":"/textures/sharp/portalred.png","portalBlue":"/textures/sharp/portalblue.png","splats":"/textures/sharp/splats.png","popularity":103226439,"popularityScore":"103"},{"name":"PastelPro","author":"SuperTed","url":"pastelpro","tiles":"/textures/pastelpro/tiles.png","speedpad":"/textures/pastelpro/speedpad.png","speedpadRed":"/textures/pastelpro/speedpadred.png","speedpadBlue":"/textures/pastelpro/speedpadblue.png","portal":"/textures/pastelpro/portal.png","portalRed":"/textures/pastelpro/portalred.png","portalBlue":"/textures/pastelpro/portalblue.png","splats":"/textures/pastelpro/splats.png","popularity":99507002,"popularityScore":"100"},{"name":"Element+","author":"MagicPigeon","url":"element","tiles":"/textures/element/tiles.png","speedpad":"/textures/element/speedpad.png","speedpadRed":"/textures/element/speedpadred.png","speedpadBlue":"/textures/element/speedpadblue.png","portal":"/textures/element/portal.png","portalRed":"/textures/element/portalred.png","portalBlue":"/textures/element/portalblue.png","splats":"/textures/element/splats.png","popularity":98019109,"popularityScore":"98"},{"name":"Mural","author":"DaEvil1","url":"mural","tiles":"/textures/mural/tiles.png","speedpad":"/textures/mural/speedpad.png","speedpadRed":"/textures/mural/speedpadred.png","speedpadBlue":"/textures/mural/speedpadblue.png","portal":"/textures/mural/portal.png","portalRed":"/textures/mural/portalred.png","portalBlue":"/textures/mural/portalblue.png","splats":"/textures/mural/splats.png","popularity":80387834,"popularityScore":"80"},{"name":"Supreme","author":"bicycle","url":"supreme","tiles":"/textures/supreme/tiles.png","speedpad":"/textures/supreme/speedpad.png","speedpadRed":"/textures/supreme/speedpadred.png","speedpadBlue":"/textures/supreme/speedpadblue.png","portal":"/textures/supreme/portal.png","portalRed":"/textures/supreme/portalred.png","portalBlue":"/textures/supreme/portalblue.png","splats":"/textures/supreme/splats.png","popularity":69068153,"popularityScore":"69"},{"name":"Circlejerk","author":"Bizkut and Ion","url":"circlejerk","tiles":"/textures/circlejerk/tiles.png","speedpad":"/textures/circlejerk/speedpad.png","speedpadRed":"/textures/circlejerk/speedpadred.png","speedpadBlue":"/textures/circlejerk/speedpadblue.png","portal":"/textures/circlejerk/portal.png","portalRed":"/textures/circlejerk/portalred.png","portalBlue":"/textures/circlejerk/portalblue.png","splats":"/textures/circlejerk/splats.png","popularity":68828720,"popularityScore":"69"},{"name":"nom","author":"anom","url":"nom","tiles":"/textures/nom/tiles.png","speedpad":"/textures/nom/speedpad.png","speedpadRed":"/textures/nom/speedpadred.png","speedpadBlue":"/textures/nom/speedpadblue.png","portal":"/textures/nom/portal.png","portalRed":"/textures/nom/portalred.png","portalBlue":"/textures/nom/portalblue.png","splats":"/textures/nom/splats.png","popularity":58222444,"popularityScore":"58"},{"name":"Starlight","author":"MagicPigeon","url":"starlight","tiles":"/textures/starlight/tiles.png","speedpad":"/textures/starlight/speedpad.png","speedpadRed":"/textures/starlight/speedpadred.png","speedpadBlue":"/textures/starlight/speedpadblue.png","portal":"/textures/starlight/portal.png","portalRed":"/textures/starlight/portalred.png","portalBlue":"/textures/starlight/portalblue.png","splats":"/textures/starlight/splats.png","popularity":54456391,"popularityScore":"54"},{"name":"TerminalPX","author":"pooppants","url":"terminalpx","tiles":"/textures/terminalpx/tiles.png","speedpad":"/textures/terminalpx/speedpad.png","speedpadRed":"/textures/terminalpx/speedpadred.png","speedpadBlue":"/textures/terminalpx/speedpadblue.png","portal":"/textures/terminalpx/portal.png","portalRed":"/textures/terminalpx/portalred.png","portalBlue":"/textures/terminalpx/portalblue.png","splats":"/textures/terminalpx/splats.png","popularity":49589262,"popularityScore":"50"},{"name":"Brioche Light","author":"Brioche","url":"briochelight","tiles":"/textures/briochelight/tiles.png","speedpad":"/textures/briochelight/speedpad.png","speedpadRed":"/textures/briochelight/speedpadred.png","speedpadBlue":"/textures/briochelight/speedpadblue.png","portal":"/textures/briochelight/portal.png","portalRed":"/textures/briochelight/portalred.png","portalBlue":"/textures/briochelight/portalblue.png","splats":"/textures/briochelight/splats.png","popularity":46374492,"popularityScore":"46"},{"name":"Crystal","author":"MagicPigeon","url":"crystal","tiles":"/textures/crystal/tiles.png","speedpad":"/textures/crystal/speedpad.png","speedpadRed":"/textures/crystal/speedpadred.png","speedpadBlue":"/textures/crystal/speedpadblue.png","portal":"/textures/crystal/portal.png","portalRed":"/textures/crystal/portalred.png","portalBlue":"/textures/crystal/portalblue.png","splats":"/textures/crystal/splats.png","popularity":41009434,"popularityScore":"41"},{"name":"Bold","author":"MagicPigeon","url":"bold","tiles":"/textures/bold/tiles.png","speedpad":"/textures/bold/speedpad.png","speedpadRed":"/textures/bold/speedpadred.png","speedpadBlue":"/textures/bold/speedpadblue.png","portal":"/textures/bold/portal.png","portalRed":"/textures/bold/portalred.png","portalBlue":"/textures/bold/portalblue.png","splats":"/textures/bold/splats.png","popularity":35898990,"popularityScore":"36"},{"name":"Turbo","author":"Ooops","url":"turbo","tiles":"/textures/turbo/tiles.png","speedpad":"/textures/turbo/speedpad.png","speedpadRed":"/textures/turbo/speedpadred.png","speedpadBlue":"/textures/turbo/speedpadblue.png","portal":"/textures/turbo/portal.png","portalRed":"/textures/turbo/portalred.png","portalBlue":"/textures/turbo/portalblue.png","splats":"/textures/turbo/splats.png","popularity":35092795,"popularityScore":"35"},{"name":"Granata","author":"_Coffee_","url":"granata","tiles":"/textures/granata/tiles.png","speedpad":"/textures/granata/speedpad.png","speedpadRed":"/textures/granata/speedpadred.png","speedpadBlue":"/textures/granata/speedpadblue.png","portal":"/textures/granata/portal.png","portalRed":"/textures/granata/portalred.png","portalBlue":"/textures/granata/portalblue.png","splats":"/textures/granata/splats.png","popularity":32450816,"popularityScore":"32"},{"name":"Celeste","author":"MagicPigeon","url":"celeste","tiles":"/textures/celeste/tiles.png","speedpad":"/textures/celeste/speedpad.png","speedpadRed":"/textures/celeste/speedpadred.png","speedpadBlue":"/textures/celeste/speedpadblue.png","portal":"/textures/celeste/portal.png","portalRed":"/textures/celeste/portalred.png","portalBlue":"/textures/celeste/portalblue.png","splats":"/textures/celeste/splats.png","popularity":30547603,"popularityScore":"31"},{"name":"Brioche Deluxe","author":"Bumballbee and Brioche","url":"briochedeluxe","tiles":"/textures/briochedeluxe/tiles.png","speedpad":"/textures/briochedeluxe/speedpad.png","speedpadRed":"/textures/briochedeluxe/speedpadred.png","speedpadBlue":"/textures/briochedeluxe/speedpadblue.png","portal":"/textures/briochedeluxe/portal.png","portalRed":"/textures/briochedeluxe/portalred.png","portalBlue":"/textures/briochedeluxe/portalblue.png","splats":"/textures/briochedeluxe/splats.png","popularity":29724638,"popularityScore":"30"},{"name":"Brioche Extra Light","author":"Brioche","url":"briocheextralight","tiles":"/textures/briocheextralight/tiles.png","speedpad":"/textures/briocheextralight/speedpad.png","speedpadRed":"/textures/briocheextralight/speedpadred.png","speedpadBlue":"/textures/briocheextralight/speedpadblue.png","portal":"/textures/briocheextralight/portal.png","portalRed":"/textures/briocheextralight/portalred.png","portalBlue":"/textures/briocheextralight/portalblue.png","splats":"/textures/briocheextralight/splats.png","popularity":28923330,"popularityScore":"29"},{"name":"TagPro Next Classic","author":"KoalaBeast, MagicPigeon, Ronding","url":"tagpronextclassic","tiles":"/textures/tagpronextclassic/tiles.png","speedpad":"/textures/tagpronextclassic/speedpad.png","speedpadRed":"/textures/tagpronextclassic/speedpadred.png","speedpadBlue":"/textures/tagpronextclassic/speedpadblue.png","portal":"/textures/tagpronextclassic/portal.png","portalRed":"/textures/tagpronextclassic/portalred.png","portalBlue":"/textures/tagpronextclassic/portalblue.png","splats":"/textures/tagpronextclassic/splats.png","popularity":27984534,"popularityScore":"28"},{"name":"Flat (Bug)","author":"Bug","url":"flatbug","tiles":"/textures/flatbug/tiles.png","speedpad":"/textures/flatbug/speedpad.png","speedpadRed":"/textures/flatbug/speedpadred.png","speedpadBlue":"/textures/flatbug/speedpadblue.png","portal":"/textures/flatbug/portal.png","portalRed":"/textures/flatbug/portalred.png","portalBlue":"/textures/flatbug/portalblue.png","splats":"/textures/flatbug/splats.png","popularity":24945237,"popularityScore":"25"},{"name":"Maxima","author":"MagicPigeon","url":"maxima","tiles":"/textures/maxima/tiles.png","speedpad":"/textures/maxima/speedpad.png","speedpadRed":"/textures/maxima/speedpadred.png","speedpadBlue":"/textures/maxima/speedpadblue.png","portal":"/textures/maxima/portal.png","portalRed":"/textures/maxima/portalred.png","portalBlue":"/textures/maxima/portalblue.png","splats":"/textures/maxima/splats.png","popularity":24314189,"popularityScore":"24"},{"name":"Jello","author":"MC Ride","url":"jello","tiles":"/textures/jello/tiles.png","speedpad":"/textures/jello/speedpad.png","speedpadRed":"/textures/jello/speedpadred.png","speedpadBlue":"/textures/jello/speedpadblue.png","portal":"/textures/jello/portal.png","portalRed":"/textures/jello/portalred.png","portalBlue":"/textures/jello/portalblue.png","splats":"/textures/jello/splats.png","popularity":19824774,"popularityScore":"20"},{"name":"Chip","author":"anom","url":"chip","tiles":"/textures/chip/tiles.png","speedpad":"/textures/chip/speedpad.png","speedpadRed":"/textures/chip/speedpadred.png","speedpadBlue":"/textures/chip/speedpadblue.png","portal":"/textures/chip/portal.png","portalRed":"/textures/chip/portalred.png","portalBlue":"/textures/chip/portalblue.png","splats":"/textures/chip/splats.png","popularity":17452655,"popularityScore":"17"},{"name":"Wave","author":"Waterwheel","url":"wave","tiles":"/textures/wave/tiles.png","speedpad":"/textures/wave/speedpad.png","speedpadRed":"/textures/wave/speedpadred.png","speedpadBlue":"/textures/wave/speedpadblue.png","portal":"/textures/wave/portal.png","portalRed":"/textures/wave/portalred.png","portalBlue":"/textures/wave/portalblue.png","splats":"/textures/wave/splats.png","popularity":16795172,"popularityScore":"17"},{"name":"Mumbo","author":"MagicPigeon","url":"mumbo","tiles":"/textures/mumbo/tiles.png","speedpad":"/textures/mumbo/speedpad.png","speedpadRed":"/textures/mumbo/speedpadred.png","speedpadBlue":"/textures/mumbo/speedpadblue.png","portal":"/textures/mumbo/portal.png","portalRed":"/textures/mumbo/portalred.png","portalBlue":"/textures/mumbo/portalblue.png","splats":"/textures/mumbo/splats.png","popularity":14947784,"popularityScore":"15"},{"name":"Funko","author":"MC Ride","url":"funko","tiles":"/textures/funko/tiles.png","speedpad":"/textures/funko/speedpad.png","speedpadRed":"/textures/funko/speedpadred.png","speedpadBlue":"/textures/funko/speedpadblue.png","portal":"/textures/funko/portal.png","portalRed":"/textures/funko/portalred.png","portalBlue":"/textures/funko/portalblue.png","splats":"/textures/funko/splats.png","popularity":7612965,"popularityScore":"8"},{"name":"Ancient Gems","author":"Flaccid Trip ft. jazzz","url":"ancientgems","tiles":"/textures/ancientgems/tiles.png","speedpad":"/textures/ancientgems/speedpad.png","speedpadRed":"/textures/ancientgems/speedpadred.png","speedpadBlue":"/textures/ancientgems/speedpadblue.png","portal":"/textures/ancientgems/portal.png","portalRed":"/textures/ancientgems/portalred.png","portalBlue":"/textures/ancientgems/portalblue.png","splats":"/textures/ancientgems/splats.png","popularity":5546128,"popularityScore":"6"},{"name":"Yin & Yang","author":"_Coffee_","url":"yinyang","tiles":"/textures/yinyang/tiles.png","speedpad":"/textures/yinyang/speedpad.png","speedpadRed":"/textures/yinyang/speedpadred.png","speedpadBlue":"/textures/yinyang/speedpadblue.png","portal":"/textures/yinyang/portal.png","portalRed":"/textures/yinyang/portalred.png","portalBlue":"/textures/yinyang/portalblue.png","splats":"/textures/yinyang/splats.png","popularity":5255750,"popularityScore":"5"},{"name":"Spongepack","author":"_Coffee_","url":"spongepack","tiles":"/textures/spongepack/tiles.png","speedpad":"/textures/spongepack/speedpad.png","speedpadRed":"/textures/spongepack/speedpadred.png","speedpadBlue":"/textures/spongepack/speedpadblue.png","portal":"/textures/spongepack/portal.png","portalRed":"/textures/spongepack/portalred.png","portalBlue":"/textures/spongepack/portalblue.png","splats":"/textures/spongepack/splats.png","popularity":5125995,"popularityScore":"5"},{"name":"Xmas","author":"jazzz","url":"xmas","tiles":"/textures/xmas/tiles.png","speedpad":"/textures/xmas/speedpad.png","speedpadRed":"/textures/xmas/speedpadred.png","speedpadBlue":"/textures/xmas/speedpadblue.png","portal":"/textures/xmas/portal.png","portalRed":"/textures/xmas/portalred.png","portalBlue":"/textures/xmas/portalblue.png","splats":"/textures/xmas/splats.png","popularity":3897718,"popularityScore":"4"},{"name":"Afloat","author":"Pingu","url":"afloat","tiles":"/textures/afloat/tiles.png","speedpad":"/textures/afloat/speedpad.png","speedpadRed":"/textures/afloat/speedpadred.png","speedpadBlue":"/textures/afloat/speedpadblue.png","portal":"/textures/afloat/portal.png","portalRed":"/textures/afloat/portalred.png","portalBlue":"/textures/afloat/portalblue.png","splats":"/textures/afloat/splats.png","popularity":2873024,"popularityScore":"3"},{"name":"Twine","author":"Flaccid Trip","url":"twine","tiles":"/textures/twine/tiles.png","speedpad":"/textures/twine/speedpad.png","speedpadRed":"/textures/twine/speedpadred.png","speedpadBlue":"/textures/twine/speedpadblue.png","portal":"/textures/twine/portal.png","portalRed":"/textures/twine/portalred.png","portalBlue":"/textures/twine/portalblue.png","splats":"/textures/twine/splats.png","popularity":2340488,"popularityScore":"2"},{"name":"Bowling","author":"_Coffee_","url":"bowling","tiles":"/textures/bowling/tiles.png","speedpad":"/textures/bowling/speedpad.png","speedpadRed":"/textures/bowling/speedpadred.png","speedpadBlue":"/textures/bowling/speedpadblue.png","portal":"/textures/bowling/portal.png","portalRed":"/textures/bowling/portalred.png","portalBlue":"/textures/bowling/portalblue.png","splats":"/textures/bowling/splats.png","popularity":913939,"popularityScore":"1"},{"name":"Supreme Shine","author":"Flaccid Trip","url":"supremeshine","tiles":"/textures/supremeshine/tiles.png","speedpad":"/textures/supremeshine/speedpad.png","speedpadRed":"/textures/supremeshine/speedpadred.png","speedpadBlue":"/textures/supremeshine/speedpadblue.png","portal":"/textures/supremeshine/portal.png","portalRed":"/textures/supremeshine/portalred.png","portalBlue":"/textures/supremeshine/portalblue.png","splats":"/textures/supremeshine/splats.png","popularity":696670,"popularityScore":"1"},{"name":"Softpaint","author":"Xcissors ft. anom","url":"softpaint","tiles":"/textures/softpaint/tiles.png","speedpad":"/textures/softpaint/speedpad.png","speedpadRed":"/textures/softpaint/speedpadred.png","speedpadBlue":"/textures/softpaint/speedpadblue.png","portal":"/textures/softpaint/portal.png","portalRed":"/textures/softpaint/portalred.png","portalBlue":"/textures/softpaint/portalblue.png","splats":"/textures/softpaint/splats.png","popularity":618115,"popularityScore":"1"},{"name":"Return","author":"Flaccid Trip","url":"return","tiles":"/textures/return/tiles.png","speedpad":"/textures/return/speedpad.png","speedpadRed":"/textures/return/speedpadred.png","speedpadBlue":"/textures/return/speedpadblue.png","portal":"/textures/return/portal.png","portalRed":"/textures/return/portalred.png","portalBlue":"/textures/return/portalblue.png","splats":"/textures/return/splats.png","popularity":149148,"popularityScore":"0"}]}
    const wallTypes = {
        "787878": { wallSolids: 0xff },
        "804070": { wallSolids: 0x2d },
        "408050": { wallSolids: 0xd2 },
        "405080": { wallSolids: 0x4b },
        "807040": { wallSolids: 0xb4 }
    };

    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, 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
    // ----------------------------
    // (See description above)
    // ----------------------------
    let originalPNGImage = null;
    let mapJSONData = null;
    let wallMap = [];
    // Set currentTexturePack from localStorage if available.
    let currentTexturePack = textures.texturePacks[0];
    const savedTexture = localStorage.getItem('fm_selected_texturePack');
    if (savedTexture) {
        const found = textures.texturePacks.find(tp => tp.url === savedTexture);
        if (found) {
            currentTexturePack = found;
            console.log("[FM] Loaded saved texture pack:", currentTexturePack.name);
        }
    }

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

    // loadImage prepends baseUrl if needed.
    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;
        });
    }

    function getFloorTile(key) {
        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 };
        } 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 };
        } else if (floorTiles.hasOwnProperty(key)) {
            return { image: images.tile, x: floorTiles[key].x, y: floorTiles[key].y };
        } else {
            return null;
        }
    }

    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);
        }
    }

    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...");

        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");

        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;

        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();
                wallMap[y][x] = (a === 0) ? 0 : (wallTypes.hasOwnProperty(hex) ? wallTypes[hex].wallSolids : 0);
            }
        }
        console.log("[FM] wallMap built:", wallMap);

        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");

        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);
                }
            }
        }

        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();
                if (wallTypes.hasOwnProperty(hex)) continue;
                let tileSource = null;
                if (hex === "007500") {
                    const fieldKey = `${x},${y}`;
                    let state = "empty"; // Default state remains "empty"
                    if (mapJSONData && mapJSONData.fields && mapJSONData.fields[fieldKey]) {
                        let ds = mapJSONData.fields[fieldKey].defaultState;
                        if (ds === "on") {
                            state = "green";
                        } else if (ds === "off") {
                            state = "empty"; // Change "off" to "empty"
                        } else {
                            state = 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 {
                    finalCtx.fillStyle = "#000000";
                    finalCtx.fillRect(x * config.tileSize, y * config.tileSize, config.tileSize, config.tileSize);
                }
            }
        }
        console.log("[FM] Floor and gate tiles drawn");


        if (mapJSONData && mapJSONData.portals) {
            Object.entries(mapJSONData.portals).forEach(([key, portalData]) => {
                if (!portalData.hasOwnProperty("destination")) {
                    const [x, y] = key.split(",").map(Number);
                    const sx = 4 * config.tileSize; // source x: tile column 4
                    const sy = 0 * config.tileSize; // source y: tile row 0
                    finalCtx.drawImage(images.portal, sx, sy, config.tileSize, config.tileSize,
                        x * config.tileSize, y * config.tileSize, config.tileSize, config.tileSize);
                    console.log(`[FM] Exit portal drawn at ${x},${y}`);
                }
            });
        }
        // ------------------------------------

        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");

        // Marsball rendering:
        // For each marsball in the JSON, draw an 80x80 image from the tiles texture (source: 12*40, 9*40)
        // Now centered over the tile instead of having its top-left corner at the tile's top-left.
        if (mapJSONData && mapJSONData.marsballs) {
            mapJSONData.marsballs.forEach(mars => {
                // Calculate destination so the marsball is centered over the tile.
                const destX = mars.x * config.tileSize - config.tileSize/2;
                const destY = mars.y * config.tileSize - config.tileSize/2;
                finalCtx.drawImage(
                    images.tile,
                    12 * config.tileSize, 9 * config.tileSize, 2 * config.tileSize, 2 * config.tileSize,
                    destX, destY, 2 * config.tileSize, 2 * config.tileSize
                );
            });
            console.log("[FM] Marsballs drawn");
        }

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

    // ----------------------------
    // UI: Texture Pack Dropdown with Search & Scrollbar
    // ----------------------------
    function addTextureDropdown() {
        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;
        }
        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">
                        ${currentTexturePack.name}
                    </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>
        `;
        referenceDiv.insertAdjacentHTML('afterend', textureContainerHTML);
        console.log("[FM] Texture pack dropdown container inserted directly below the reference dropdown");

        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";
            });
        }

        const searchInput = document.getElementById('fm-dropdown-search');
        searchInput.addEventListener('input', filterItems);

        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;
                    localStorage.setItem('fm_selected_texturePack', selectedTexture.url);
                    console.log("[FM] Texture pack changed to:", selectedTexture.name);
                    processMap();
                    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();
    }
})();