/*:
 * @target MZ
 * @plugindesc Live Map Editor - v1.12.01
 *
 * <pluginName:CycloneMapEditor>
 * @author Hudell
 * @url https://makerdevs.com/plugin/cyclone-map-editor
 *
 * @help
 * ===========================================================================
 *                                    88
 *                                    88
 *                                    88
 *   ,adPPYba, 8b       d8  ,adPPYba, 88  ,adPPYba,  8b,dPPYba,   ,adPPYba,
 *  a8"     "" `8b     d8' a8"     "" 88 a8"     "8a 88P'   `"8a a8P_____88
 *  8b          `8b   d8'  8b         88 8b       d8 88       88 8PP"""""""
 *  "8a,   ,aa   `8b,d8'   "8a,   ,aa 88 "8a,   ,a8" 88       88 "8b,   ,aa
 *   `"Ybbd8"'     Y88'     `"Ybbd8"' 88  `"YbbdP"'  88       88  `"Ybbd8"'
 *                 d8'
 *                d8'
 * Live Map Editor                                                   by Hudell
 * ===========================================================================
 * Terms of Use
 * ===========================================================================
 * 1. For support, feature requests or bug reports, you may contact me through
 *  any of the following channels (in order of preference):
 *
 *   1.a. Opening an issue on the plugin's GitHub repository:
 *      https://github.com/Hudell/cyclone-engine
 *   1.b. Tagging me on threads on Rpg Maker related Forums, such as:
 *      rpgmakerweb.com (English)
 *      centrorpg.com (Portuguese)
 *      condadobraveheart.com (Portuguese)
 *   1.c. Opening threads on the plugin's itch.io page
 *   1.d. Tagging my user on Rpg Maker related sub-reddits, such as r/rpgmaker
 *
 * 2. Do not send me Direct Messages asking for support or bug reports.
 * You may only send me direct messages when none of the above platforms are
 * appropiate for it, or when you want to share pictures of cute dogs.
 *
 * 3. A special exception is created for patreon users who get access to my
 * priority support discord server.
 *
 * 4. Sending plugin related questions on channels related to any of my other
 * projects (such as my game's Discord server) may result in an immediate ban
 * from such platforms and I may also choose to ignore your future requests.
 *
 * 5. This plugin is released under the Apache License 2.0 (Apache-2.0).
 *
 * 6. You can send me your own changes to this plugin if you wish to see them
 * included in an update, by registering a Pull Request on the plugin's GitHub
 * repository.
 *
 * 7. This plugin is provided as is. While I'll often read feedback and offer
 * updates to my plugins, I am in no obligation to do so.
 *
 * 8. I'm not responsible for anything created with this plugin.
 * ===========================================================================
 * Did you know?
 * Early map makers used to include fake towns on their maps to identify
 * copies of their work.
 * ===========================================================================
 * Change Log
 * ===========================================================================
 * 2021-01-27 - Version 1.12.00
 *   * Added option to generate 48x48 tilesets when using other sizes.
 * 2020-11-05 - Version 1.11.00
 *   * General bug fixes
 * 2020-10-10 - Version 1.10.00
 *   * Added tileset tabs
 *   * Moved layer list to left side of the screen
 *   * General bug fixes
 *
 * 2020-10-09 - Version 1.09.00
 *   * General bug fixes
 *   * Added support to Cyclone Magic v1.1
 *
 * 2020-10-07 - Version 1.08.00
 *   * Added support to Cyclone Extra Tilesets v1.1
 *
 * 2020-09-22 - Version 1.07.00
 *   * Several quality-of-life updates
 *   * Added support to Cyclone Magic
 *   * Added support to Cyclone Extra Tilesets
 *
 * 2020-09-15 - Version 1.06.00
 *   * New option to view tile properties such as tags, passability, bush,
 *   ladder and so on.
 *
 * 2020-09-14 - Version 1.05.01
 *   * Fixed small delay on integration between movement and map editor.
 *
 * 2020-09-14 - Version 1.05.00
 *   * Added new collision options;
 *   * Changed data compression algorithm;
 *
 * 2020-09-10 - Version 1.04.00
 *   * Added option to customize the collision in blocks of 1/4 of a tile.
 *   * Added options to export the collision map to an image
 *   * Changed the editor to force a larger screen when the game uses a small
 *   resolution.
 *   * Added option to toggle event visibility.
 *
 * 2020-08-30 - Version 1.03.00
 *   * Added options to export the map as images
 *
 * 2020-08-29 - Version 1.02.00
 *   * Created a version of this plugin for Rpg Maker MV
 *   * Changed the way shadows are displayed on the tile list
 *   * Copying an area while holding shift will now skip invisible layers.
 *
 * 2020-08-29 - Version 1.01.00
 *   * Web browser support
 *   * Eraser will now only erase one layer at a time on the auto layer
 *   * Keep changed data in memory even if you leave the map.
 *
 *
 * 2020-08-28 - Version 1.00.00
 * ===========================================================================
 *
 * @param regionIcons
 * @text Region Icons
 * @type struct<RegionIcon>[]
 * @desc Configure certain regions to display an icon instead of the number
 *
 * @param Status Bar
 *
 * @param showMapId
 * @text Show Map Id
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the Map Id be visible in the status bar?
 *
 * @param showTilesetId
 * @text Show Tileset Id
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the Tileset Id be visible in the status bar?
 *
 * @param showPosition
 * @text Show Position
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the X and Y position in the status bar?
 *
 * @param showCellTiles
 * @text Show Cell Tiles
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the id of the current position tiles be displayed in the status bar?
 *
 * @param showRegionId
 * @text Show Region Id
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the region id of the current position be displayed in the status bar?
 *
 * @param showTag
 * @text Show Terrain Tag
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the terrain tag of the current position be displayed in the status bar?
 *
 * @param showCollision
 * @text Show Collision
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the collision of the current position be displayed in the status bar?
 *
 * @param showLadder
 * @text Show Ladder
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should status bar indicate if the current position is a ladder?
 *
 * @param showBush
 * @text Show Bush
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the status bar indicate if the current position is a bush?
 *
 * @param showCounter
 * @text Show Counter
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the status bar indicate if the current position is a counter?
 *
 * @param showDamageFloor
 * @text Show Damage Floor
 * @parent Status Bar
 * @type boolean
 * @default true
 * @desc Should the status bar indicate if the current position is a damage floor tile?
 *
 * @param collisionStepCount
 * @text Default Collision Blocks
 * @type select
 * @default 1
 * @desc How many collision blocks per tile should the editor show?
 * @option 4
 * @option 2
 * @option 1
 *
 **/

/*~struct~RegionIcon:
 * @param regionId
 * @text Region Id
 * @type number
 * @desc The regionId to display an icon on
 *
 * @param icon
 * @text Icon
 * @type string
 * @desc Right click to select icon index
 */
(function () {
'use strict';

globalThis.CyclonePatcher=class{static initialize(t){this.pluginName=t,this.superClasses=new Map;}static _descriptorIsProperty(t){return t.get||t.set||!t.value||"function"!=typeof t.value}static _getAllClassDescriptors(t,e=!1){if(t===Object)return {};const r=Object.getOwnPropertyDescriptors(e?t.prototype:t);let s={};if(t.prototype){const r=Object.getPrototypeOf(t.prototype).constructor;r!==Object&&(s=this._getAllClassDescriptors(r,e));}return Object.assign({},s,r)}static _assignDescriptor(t,e,r,s,a=!1){if(this._descriptorIsProperty(r))r.get||r.set?Object.defineProperty(t,s,{get:r.get,set:r.set,enumerable:r.enumerable,configurable:r.configurable}):Object.defineProperty(t,s,{value:r.value,enumerable:r.enumerable,configurable:r.configurable});else {let r=s;if(a)for(;r in t;)r=`_${r}`;t[r]=e[s];}}static _applyPatch(t,e,r,s,a=!1){const n=this._getAllClassDescriptors(t,a),i=a?t.prototype:t,o=a?e.prototype:e,l=Object.getOwnPropertyDescriptors(o);let u=!1;for(const t in l){if(s.includes(t))continue;if(t in n){u=!0;const e=n[t];this._assignDescriptor(r,i,e,t,!0);}const e=l[t];this._assignDescriptor(i,o,e,t);}return u}static patchClass(t,e){const r=this.superClasses&&this.superClasses[t.name]||{},s={},a={},n=e(a,s);if("function"!=typeof n)throw new Error(`Invalid class patch for ${t.name}`);const i=Object.getOwnPropertyNames(class{}),o=Object.getOwnPropertyNames(class{}.prototype),l=this._applyPatch(t,n,r,i),u=this._applyPatch(t,n,s,o,!0);if(l){const t=Object.getOwnPropertyDescriptors(r);for(const e in t)this._assignDescriptor(a,r,t[e],e);u&&(a.$prototype=s);}else Object.assign(a,s);this.superClasses&&(this.superClasses[t.name]=a);}};const t=Object.freeze(["TRUE","ON","1","YES","T","V"]);class e extends CyclonePatcher{static initialize(t){super.initialize(t),this.fileName=void 0,this.params={},this.structs=new Map,this.eventListeners=new Map,this.structs.set("Dictionary",{name:{type:"string",defaultValue:""},value:{type:"string",defaultValue:""}});}static register(t={}){const e=this.loadAllParams();this.params=this.loadParamMap(t,e);}static loadAllParams(){for(const t of globalThis.$plugins){if(!t||!t.status)continue;if(!t.description||!t.description.includes(`<pluginName:${this.pluginName}`))continue;this.fileName=t.name;const e=new Map;for(const r in t.parameters)r&&!r.startsWith("-")&&e.set(r,t.parameters[r]);return e}}static loadParamMap(t,e){const r={};for(const s in t)if(t.hasOwnProperty(s))try{r[s]=this.parseParam(s,t,e);}catch(t){console.error(`CycloneEngine crashed while trying to parse a parameter value (${s}). Please report the following error to Hudell:`),console.log(t);}return r}static registerEvent(t,e){this.eventListeners.has(t)||this.eventListeners.set(t,new Set);this.eventListeners.get(t).add(e);}static removeEventListener(t,e){if(!this.eventListeners.has(t))return;this.eventListeners.get(t).delete(e);}static shouldReturnCallbackResult(t,{abortOnTrue:e,abortOnFalse:r,returnOnValue:s}){return !(!1!==t||!r)||(!(!0!==t||!e)||!(void 0===t||!s))}static runEvent(t,{abortOnTrue:e=!1,abortOnFalse:r=!1,returnOnValue:s=!1}={},...a){if(!this.eventListeners.has(t))return;const n=this.eventListeners.get(t);for(const t of n){if("number"==typeof t){this.runCommonEvent(t);continue}if("function"!=typeof t){console.error("CycloneEngine: Invalid callback type:"),console.log(t);continue}const n=t(...a);if(this.shouldReturnCallbackResult(n,{abortOnTrue:e,abortOnFalse:r,returnOnValue:s}))return n}}static runCommonEvent(t){const e=globalThis.$dataCommonEvents[t];if(!e)return;const r=new Game_Interpreter(1);if(r.setup(e.list,0),!this._interpreters){this._interpreters=new Set;const t=SceneManager.updateMain;SceneManager.updateMain=()=>{t.call(SceneManager),this.update();};}this._interpreters.add(r);}static update(){if(this._interpreters)for(const t of this._interpreters)t.update(),t.isRunning()||this._interpreters.delete(t);}static getPluginFileName(){return this.fileName??this.pluginName}static isTrue(e){return "string"!=typeof e?Boolean(e):t.includes(e.toUpperCase())}static isFalse(t){return !this.isTrue(t)}static getIntParam({value:t,defaultValue:e}){try{const r=parseInt(t);return isNaN(r)?e:r}catch(r){return ""!==t&&console.error(`Cyclone Engine plugin ${this.pluginName}: Param is expected to be an integer number, but the received value was '${t}'.`),e}}static getFloatParam({value:t,defaultValue:e}){try{const r=parseFloat(t.replace(",","."));return isNaN(r)?e:r}catch(r){return ""!==t&&console.error(`Cyclone Engine plugin ${this.pluginName}: Param is expected to be a number, but the received value was '${t}'.`),e}}static getIntListParam({value:t}){return this.parseArray((t??"").trim(),(t=>{try{return parseInt(t.trim())}catch(e){return ""!==t&&console.error(`Cyclone Engine plugin ${this.pluginName}: Param is expected to be a list of integer numbers, but one of the items was '${t}'.`),0}}))}static parseStructArrayParam({data:t,type:e}){const r=[];for(const s of t){const t=this.parseStructParam({value:s,defaultValue:"",type:e});t&&r.push(t);}return r}static getFloatListParam({value:t}){return this.parseArray((t||"").trim(),(t=>{try{return parseFloat(t.trim())}catch(e){return ""!==t&&console.error(`Cyclone Engine plugin ${this.pluginName}: Param ${name} is expected to be a list of numbers, but one of the items was '${t}'.`),0}}))}static getParam({key:t,value:e,defaultValue:r,type:s}){try{if(s.endsWith("[]"))return this.parseArrayParam({key:t,value:e,type:s});if(s.startsWith("struct<"))return this.parseStructParam({key:t,value:e,defaultValue:r,type:s});if(void 0===e)return r;switch(s){case"int":return this.getIntParam({value:e,defaultValue:r});case"float":return this.getFloatParam({value:e,defaultValue:r});case"boolean":return "boolean"==typeof e?e:this.isTrue(String(e).trim());default:return e}}catch(e){throw t&&console.error(t),e}}static getPluginParam(t){return this.params.get(t)}static defaultValueForType(t){switch(t){case"int":case"float":return 0;case"boolean":return !1}return ""}static parseParam(t,e,r){let s=e[t];s&&"string"==typeof s&&(s={type:s,defaultValue:this.defaultValueForType(s)});const{name:a=t,type:n="string",defaultValue:i=""}=s;let o;if(r)o=r.get(a)??i;else {o=(this.getPluginParam(a)||{}).value??i;}return this.getParam({key:t,value:o,defaultValue:i,type:n})}static parseArrayParam({value:t,type:e}){const r=this.parseArray(t);if(!r||!r.length)return r;const s=e.substr(0,e.length-2),a=[];for(const t of r){const e=this.defaultValueForType(s);a.push(this.getParam({value:t,type:s,defaultValue:e}));}return a}static getRegexMatch(t,e,r){const s=t.match(e);if(s)return s[r]}static parseStructData(t,e){for(const r in t){if(!t.hasOwnProperty(r))continue;let s=t[r];"string"==typeof s&&(s={type:s,defaultValue:this.defaultValueForType(s)}),e[r]=this.getParam({key:r,value:e[r],defaultValue:s.defaultValue,type:s.type});}return e}static parseStructParam({key:t,value:e,defaultValue:r,type:s}){let a;if(e)try{a=JSON.parse(e);}catch(r){console.error("Cyclone Engine failed to parse param structure: ",t,e),console.error(r);}if(!a)try{a=JSON.parse(r);}catch(e){throw console.error("Cyclone Engine failed to parse default value: ",t,r),e}const n=this.getRegexMatch(s,/struct<(.*)>/i,1);if(!n)return console.error(`Unknown plugin param type: ${s} (${t||""})`),a;const i=this.structs.get(n);return i?this.parseStructData(i,a):(console.error(`Unknown param structure type: ${n} (${t||""})`),a)}static parseList(t,e,r=","){let s=t;s.startsWith("[")&&(s=s.substr(1)),s.endsWith("]")&&(s=s.substr(0,s.length-1));const a=s.split(r||",");return e?a.map((t=>e(t))):a}static parseArray(t,e){let r;try{r=JSON.parse(t);}catch(t){return []}return r&&r.length?e?r.map((t=>e(t))):r:[]}static registerCommand(t,e,r,s=[]){return "function"==typeof e?PluginManager.registerCommand(this.getPluginFileName(),t,e):PluginManager.registerCommand(this.getPluginFileName(),t,(t=>{const s=new Map;for(const e in t)t.hasOwnProperty(e)&&s.set(e,t[e]);const a=this.loadParamMap(e,s);return Object.assign(t,a),r(t)}))}}globalThis.CyclonePlugin=e;

const Layers = {
  shadows: 4,
  regions: 5,
  events: 6,
  auto: 7,
  collisions: 8,
  tags: 9,
  blend: 10,
};

const Tools = {
  eraser: 'eraser',
  pencil: 'pencil',
  rectangle: 'rectangle',
  fill: 'fill',
  passage: 'passage',
  passage4: 'passage4',
  ladder: 'ladder',
  bush: 'bush',
  counter: 'counter',
  damage: 'damage',
  terrain: 'terrain',
};

const tilePropTools = [
  Tools.passage,
  Tools.passage4,
  Tools.ladder,
  Tools.bush,
  Tools.counter,
  Tools.damage,
  Tools.terrain,
];

const TilePassageType = {
  free: 0,
  blocked: 1,
  star: 2,
};

class MapshotTileMap extends Bitmap {
  constructor() {
    const tileWidth = $gameMap.tileWidth();
    const tileHeight = $gameMap.tileHeight();
    const width = $gameMap.width() * tileWidth;
    const height = $gameMap.height() * tileHeight;

    super(width, height);
    this.flags = $gameMap.tileset().flags;
  }

  drawSingleLayer(layerIndex) {
    const width = $gameMap.width();
    const height = $gameMap.height();

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        this.drawLayerSpot(x, y, layerIndex);
      }
    }
  }

  drawLayerSpot(x, y, z, filterFn = undefined) {
    const index = CycloneMapEditor.tileIndex(x, y, z);
    const tileId = $dataMap.data[index] ?? 0;

    if (filterFn && !filterFn(tileId)) {
      return;
    }

    const drawX = x * $gameMap.tileWidth();
    const drawY = y * $gameMap.tileHeight();

    this.drawTile(tileId, drawX, drawY);
  }

  isHigherTile(tileId) {
    return this.flags[tileId] & 0x10;
  }

  drawLowerTiles() {
    const width = $gameMap.width();
    const height = $gameMap.height();

    const filterFn = (tileId) => !this.isHigherTile(tileId);

    for (let z = 0; z <= 3; z++) {
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          this.drawLayerSpot(x, y, z, filterFn);
        }
      }
    }
  }

  drawUpperTiles() {
    const width = $gameMap.width();
    const height = $gameMap.height();

    const filterFn = (tileId) => this.isHigherTile(tileId);

    for (let z = 0; z <= 3; z++) {
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          this.drawLayerSpot(x, y, z, filterFn);
        }
      }
    }
  }

  drawEvents(priority = undefined) {
    const events = SceneManager._scene._spriteset._tilemap.children.filter(child => child instanceof Sprite_Character);
    for (const sprite of events) {
      if (sprite._character !== null) {
        if (sprite._character instanceof Game_Player || sprite._character instanceof Game_Follower || sprite._character instanceof Game_Vehicle) {
          continue;
        }
      }

      sprite.update();
      if (sprite._characterName === '' && sprite._tileId === 0) {
        continue;
      }

      if (priority !== undefined && sprite._character._priorityType !== priority) {
        continue;
      }

      const x = sprite.x - sprite._frame.width / 2 + $gameMap._displayX * $gameMap.tileWidth();
      const y = sprite.y - sprite._frame.height + $gameMap._displayY * $gameMap.tileHeight();

      this.blt(sprite.bitmap, sprite._frame.x, sprite._frame.y, sprite._frame.width, sprite._frame.height, x, y, sprite._frame.width, sprite._frame.height);
    }
  }

  drawDefaultCollision() {
    const width = $gameMap.width();
    const height = $gameMap.height();

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const drawWidth = CycloneMapEditor.tileWidth;
        const drawHeight = CycloneMapEditor.tileHeight;
        const drawX = x * drawWidth;
        const drawY = y * drawHeight;

        const downBlocked = !$gameMap.isPassable(x, y, 2);
        const upBlocked = !$gameMap.isPassable(x, y, 8);
        const leftBlocked = !$gameMap.isPassable(x, y, 4);
        const rightBlocked = !$gameMap.isPassable(x, y, 6);

        if (downBlocked && upBlocked && leftBlocked && rightBlocked) {
          this.fillRect(drawX, drawY, drawWidth, drawHeight, '#FF0000');
          continue;
        }

        const pieceHeight = Math.floor(drawHeight / 4);
        const pieceWidth = Math.floor(drawWidth / 4);

        if (downBlocked) {
          this.fillRect(drawX, drawY + drawHeight - pieceHeight, drawWidth, pieceHeight, '#FF00FF');
        }
        if (upBlocked) {
          this.fillRect(drawX, drawY, drawWidth, pieceHeight, '#FF00FF');
        }
        if (leftBlocked) {
          this.fillRect(drawX, drawY, pieceWidth, drawHeight, '#FF00FF');
        }
        if (rightBlocked) {
          this.fillRect(drawX + drawWidth - pieceWidth, drawY, pieceWidth, drawHeight, '#FF00FF');
        }
      }
    }
  }

  drawCustomCollision() {
    const customCollisionTable = CycloneMapEditor.customCollisionTable;
    const height = $gameMap.height();
    const width = $gameMap.width();
    const tileWidth = CycloneMapEditor.tileWidth;
    const tileHeight = CycloneMapEditor.tileHeight;
    const drawWidth = tileWidth / 4;
    const drawHeight = tileHeight / 4;
    const colors = ['#00FF00', '#FF0000'];
    const collisionHeight = height * 4;
    const collisionWidth = width * 4;

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        for (let cellX = 0; cellX < 4; cellX++) {
          for (let cellY = 0; cellY < 4; cellY++) {

            const intX = Math.floor(x * 4) + cellX;
            const intY = Math.floor(y * 4) + cellY;
            const index = (intY % collisionHeight) * collisionWidth + (intX % collisionWidth);

            // eslint-disable-next-line max-depth
            if (customCollisionTable[index]) {
              const drawX = intX * drawWidth;
              const drawY = intY * drawHeight;

              this.clearRect(drawX, drawY, drawWidth, drawHeight);

              const colorIndex = customCollisionTable[index] - 1;
              const color = colors[colorIndex % colors.length];
              this.fillRect(drawX, drawY, drawWidth, drawHeight, color);
            }
          }
        }
      }
    }
  }
}

var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n;}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return "";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256;}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o));}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return "";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else {if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}else {for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a];}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u);}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}else {for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a];}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++);}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++;}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return ""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return "";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else {if(l!==d)return null;v=s+s.charAt(0);}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++);}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);

function throttle(fn, delay) {
  let timeout;
  let latestArgs;
  let needsCalling = false;

  const call = () => {
    timeout = setTimeout(() => {
      if (needsCalling) {
        call();
      } else {
        timeout = false;
      }
      needsCalling = false;
    }, delay);

    fn.call(this, ...latestArgs);
  };

  const debounced = function(...args) {
    latestArgs = args;
    if (timeout) {
      needsCalling = true;
      return;
    }

    call();
  };

  return debounced;
}

function debounce(fn, delay) {
  let clearTimer;
  return function(...args) {
    const context = this;
    clearTimeout(clearTimer);
    clearTimer = setTimeout(() => fn.call(context, ...args), delay);
  };
}

class DirectionHelper {
  static goesLeft(d) {
    return d && d % 3 === 1;
  }

  static goesRight(d) {
    return d && d % 3 === 0;
  }

  static goesUp(d) {
    return d >= 7 && d <= 9;
  }

  static goesDown(d) {
    return d >= 1 && d <= 3;
  }

  static isDiagonal(d) {
    return this.isVertical(d) && this.isHorizontal(d);
  }

  static isVertical(d) {
    return this.goesDown(d) || this.goesUp(d);
  }

  static isHorizontal(d) {
    return this.goesLeft(d) || this.goesRight(d);
  }

  static shareADirection(dir1, dir2) {
    if (this.goesDown(dir1) && this.goesDown(dir2)) {
      return true;
    }

    if (this.goesLeft(dir1) && this.goesLeft(dir2)) {
      return true;
    }

    if (this.goesRight(dir1) && this.goesRight(dir2)) {
      return true;
    }

    if (this.goesUp(dir1) && this.goesUp(dir2)) {
      return true;
    }

    return false;
  }

  static getFirstDirection(diagonalDirection) {
    if (!diagonalDirection) {
      return diagonalDirection;
    }

    if (diagonalDirection > 6) {
      return 8;
    }
    if (diagonalDirection < 4) {
      return 2;
    }
    return diagonalDirection;
  }

  static getAlternativeDirection(direction, diagonalDirection) {
    if (direction === diagonalDirection) {
      return direction;
    }

    switch (diagonalDirection) {
      case 7:
        return direction == 8 ? 4 : 8;
      case 9:
        return direction == 8 ? 6 : 8;
      case 1:
        return direction == 2 ? 4 : 2;
      case 3:
        return direction == 2 ? 6 : 2;
    }

    return direction;
  }
}

function logImage(canvas, text) {
  const url = canvas.toDataURL();

  const scale = 1;
  const img = new Image();

  function getBox(width, height) {
    return {
      string: '+',
      style: `font-size: 1px; padding: ${ Math.floor(height / 2) }px ${ Math.floor(width / 2) }px; line-height: ${ height }px;`,
    };
  }

  img.onload = function() {
    const dim = getBox(this.width * scale, this.height * scale);
    if (text) {
      console.log(text);
    }
    console.log(`%c${ dim.string }`, `${ dim.style }background: url('${ url }'); background-size: ${ this.width * scale }px ${ this.height * scale }px; color: transparent;`);
  };

  img.src = url;
}

function getTilesetIndex(tileId) {
  if (tileId >= (Tilemap.TILE_ID_A5 + 256) && tileId < Tilemap.TILE_ID_A1) {
    return 11;
  }

  if (Tilemap.isTileA1(tileId)) {
    return 0;
  }

  if (Tilemap.isTileA2(tileId)) {
    return 1;
  }

  if (Tilemap.isTileA3(tileId)) {
    return 2;
  }

  if (Tilemap.isTileA4(tileId)) {
    return 3;
  }

  if (Tilemap.isTileA5(tileId)) {
    return 4;
  }

  return 5 + Math.floor(tileId / 256);
}

const layerVisibility = [true, true, true, true, true, false, true, false, false, false, false];
let editorActive = true;
let windowWidth = 216;
const mapCaches = {};
let customCollisionTable = {};
let tileBlendingTable = {};

let currentLayer = 7;
let currentTab = 'A';
let currentTileId = undefined;
let tileCols = 1;
let tileRows = 1;
let selectedTileList = [];
let multiLayerSelection = [];
let tileWidth = 48;
let tileHeight = 48;
let currentTool = 'pencil';
let lastDrawingTool = 'pencil';
let wasPressing = false;
let isRightButtonDown = false;
let wasRightButtonDown = false;
let rectangleStartMouseX = 0;
let rectangleStartMouseY = 0;
let rectangleStartX = 0;
let rectangleStartY = 0;
let rectangleWidth = 0;
let rectangleHeight = 0;
let rectangleBackWidth = 0;
let rectangleBackHeight = 0;
const tabs = ['A', 'B', 'C', 'D', 'E'];
let changeHistory = [];
let undoHistory = [];
let currentChange = false;
let previewChanges = {};
let messySelection = false;
let showGrid = true;
let statusTileId = 0;
let statusMapX = 0;
let statusMapY = 0;
let statusTile1 = 0;
let statusTile2 = 0;
let statusTile3 = 0;
let statusTile4 = 0;
let statusRegion = 0;
let statusTag = 0;
let statusCollision = 0;
let statusBush = false;
let statusCounter = false;
let statusDamage = false;
let statusLadder = false;
let currentZoom = 1;
let puzzleMode = false;

let circleData = false;
let smallCircleData = false;

const _ = '';
const o = true;
const x = false;
const autoTileShapeTable = [
  // o means there's a compatible tile on that position,
  // x means there is no compatible tile on that position
  // _ means that position doesn't matter
  [o, o, o, o, o, o, o, o], // 0
  [x, o, o, o, o, o, o, o], // 1
  [o, o, x, o, o, o, o, o], // 2
  [x, o, x, o, o, o, o, o], // 3
  [o, o, o, o, o, o, o, x], // 4
  [x, o, o, o, o, o, o, x], // 5
  [o, o, x, o, o, o, o, x], // 6
  [x, o, x, o, o, o, o, x], // 7
  [o, o, o, o, o, x, o, o], // 8
  [x, o, o, o, o, x, o, o], // 9
  [o, o, x, o, o, x, o, o], //10
  [x, o, x, o, o, x, o, o], //11
  [o, o, o, o, o, x, o, x], //12
  [x, o, o, o, o, x, o, x], //13
  [o, o, x, o, o, x, o, x], //14
  [x, o, x, o, o, x, o, x], //15
  [_, o, o, x, o, _, o, o], //16
  [_, o, x, x, o, _, o, o], //17
  [_, o, o, x, o, _, o, x], //18
  [_, o, x, x, o, _, o, x], //19
  [_, x, _, o, o, o, o, o], //20
  [_, x, _, o, o, o, o, x], //21
  [_, x, _, o, o, x, o, o], //22
  [_, x, _, o, o, x, o, x], //23
  [o, o, _, o, x, o, o, _], //24
  [o, o, _, o, x, x, o, _], //25
  [x, o, _, o, x, o, o, _], //26
  [x, o, _, o, x, x, o, _], //27
  [o, o, o, o, o, _, x, _], //28
  [x, o, o, o, o, _, x, _], //29
  [o, o, x, o, o, _, x, _], //30
  [x, o, x, o, o, _, x, _], //31
  [_, o, _, x, x, _, o, _], //32
  [_, x, _, o, o, _, x, _], //33
  [_, x, _, x, o, _, o, o], //34
  [_, x, _, x, o, _, o, x], //35
  [_, x, _, o, x, o, o, _], //36
  [_, x, _, o, x, x, o, _], //37
  [o, o, _, o, x, _, x, _], //38
  [x, o, _, o, x, _, x, _], //39
  [_, o, o, x, o, _, x, _], //40
  [_, o, x, x, o, _, x, _], //41
  [_, x, _, x, x, _, o, _], //42
  [_, x, _, x, o, _, x, _], //43
  [_, o, _, x, x, _, x, _], //44
  [_, x, _, o, x, _, x, _], //45
  [_, x, _, x, x, _, x, _]  //46
];

const highLayerAutotiles = [
  1,
  2,
  3,
  20,
  21,
  22,
  23,
  28,
  29,
  30,
  31,
  36,
  37,
  38,
  39,
  44,
  45,
  46,
  47,
];

const refreshGrid = throttle(() => {
  SceneManager._scene._mapEditorGrid.refresh();
}, 50);

const refreshTilemap = throttle(() => {
  SceneManager._scene._spriteset._tilemap.refresh();
}, 200);

const refreshCollision = throttle(() => {
  if (TouchInput.isPressed()) {
    setTimeout(() => {
      refreshCollision();
    }, 1);
    return;
  }
  if (window.CycloneMovement) {
    window.CycloneMovement.setupCollision();
  }
}, 200);

const refreshMagic = throttle(() => {
  if (TouchInput.isPressed()) {
    setTimeout(() => {
      refreshMagic();
    }, 1);
    return;
  }

  if (window.CycloneMagic) {
    window.CycloneMagic.loadMagic();
    forceBlenderRefresh(true);
  }
});

const forceBlenderRefresh = throttle((...args) => {
  CycloneMapEditor$1.forceBlenderRefresh(...args);
}, 50);

const saveExtraData = throttle((refreshCollisionToo = false) => {
  if (TouchInput.isPressed()) {
    setTimeout(() => {
      saveExtraData(refreshCollisionToo);
    }, 1);
    return;
  }

  CycloneMapEditor$1.saveExtraData();
  if (refreshCollisionToo) {
    refreshCollision();
    refreshMagic();
  }
}, 200);

const fullRefresh = debounce(() => {
  saveExtraData(true);
}, 500);

class CycloneMapEditor$1 extends CyclonePlugin {
  static get currentTab() {
    return currentTab;
  }
  static get active() {
    return editorActive;
  }
  static set active(value) {
    editorActive = value;
  }
  static get tileWidth() {
    return tileWidth;
  }
  static set tileWidth(value) {
    tileWidth = value;
  }
  static get tileDrawWidth() {
    if (Graphics.width < 1280) {
      if (tileWidth > 32) {
        return Math.floor(tileWidth / 2);
      }

      if (tileWidth <= 16) {
        return tileWidth * 2;
      }
    } else {
      if (tileWidth < 32) {
        return tileWidth * 2;
      }
    }

    return tileWidth;
  }
  static get tileHeight() {
    return tileHeight;
  }
  static set tileHeight(value) {
    tileHeight = value;
  }
  static get tileDrawHeight() {
    if (Graphics.width < 1280) {
      if (tileHeight > 32) {
        return Math.floor(tileHeight / 2);
      }

      if (tileHeight <= 16) {
        return tileHeight * 2;
      }
    } else {
      if (tileHeight < 32) {
        return tileHeight * 2;
      }
    }

    return tileHeight;
  }
  static get windowWidth() {
    return windowWidth;
  }
  static set windowWidth(value) {
    windowWidth = value;
  }

  static get isRightButtonDown() {
    return isRightButtonDown;
  }
  static set isRightButtonDown(value) {
    isRightButtonDown = value;
  }

  // the size of the rectangle tool when the user stretches it right
  static get rectangleWidth() { return rectangleWidth; }
  static set rectangleWidth(value) { rectangleWidth = value; }

  // The size of the rectangle tool when the user stretches it down
  static get rectangleHeight() { return rectangleHeight; }
  static set rectangleHeight(value) { rectangleHeight = value; }

  // the size of the rectangle tool when the user stretches it left
  static get rectangleBackWidth() { return rectangleBackWidth; }
  static set rectangleBackWidth(value) { rectangleBackWidth = value; }

  // The size of the rectangle tool when the user stretches it up
  static get rectangleBackHeight() { return rectangleBackHeight; }
  static set rectangleBackHeight(value) { rectangleBackHeight = value; }

  // The X tile where the rectangle started
  static get rectangleStartX() { return rectangleStartX; }
  static set rectangleStartX(value) { rectangleStartX = value; }

  // The Y tile where the rectangle started
  static get rectangleStartY() { return rectangleStartY; }
  static set rectangleStartY(value) { rectangleStartY = value; }

  static get tileCols() { return tileCols; }
  static set tileCols(value) { tileCols = value; }
  static get tileRows() { return tileRows; }
  static set tileRows(value) { tileRows = value; }

  // The Mouse X position where the rectangle started
  static get rectangleStartMouseX() { return rectangleStartMouseX; }
  static set rectangleStartMouseX(value) { rectangleStartMouseX = value; }

  // The Mouse Y position where the rectangle started
  static get rectangleStartMouseY() { return rectangleStartMouseY; }
  static set rectangleStartMouseY(value) { rectangleStartMouseY = value; }

  static get messySelection() { return messySelection; }
  static set messySelection(value) { messySelection = value; }

  static get changeHistory() { return changeHistory; }
  static get undoHistory() { return undoHistory; }
  static get layerVisibility() { return layerVisibility; }

  static get wasRightButtonDown() { return wasRightButtonDown; }
  static set wasRightButtonDown(value) { wasRightButtonDown = value; }
  static get wasPressing() { return wasPressing; }
  static set wasPressing(value) { wasPressing = value; }

  static get currentTool() { return currentTool; }
  static get currentLayer() { return currentLayer; }
  static get showGrid() { return showGrid; }
  static get previewChanges() { return previewChanges; }
  static get puzzleMode() { return puzzleMode; }

  static get currentTileId() { return currentTileId; }
  static set currentTileId(value) { currentTileId = value; }
  static get selectedTileList() { return selectedTileList; }
  static set selectedTileList(value) { selectedTileList = value; }
  static get multiLayerSelection() { return multiLayerSelection; }
  static set multiLayerSelection(value) { multiLayerSelection = value; }

  static get statusTileId() { return statusTileId; }
  static set statusTileId(value) { statusTileId = value; }
  static get statusMapX() { return statusMapX; }
  static set statusMapX(value) { statusMapX = value; }
  static get statusMapY() { return statusMapY; }
  static set statusMapY(value) { statusMapY = value; }
  static get statusTile1() { return statusTile1; }
  static set statusTile1(value) { statusTile1 = value; }
  static get statusTile2() { return statusTile2; }
  static set statusTile2(value) { statusTile2 = value; }
  static get statusTile3() { return statusTile3; }
  static set statusTile3(value) { statusTile3 = value; }
  static get statusTile4() { return statusTile4; }
  static set statusTile4(value) { statusTile4 = value; }
  static get statusRegion() { return statusRegion; }
  static get statusTag() { return statusTag; }
  static get statusCollision() { return statusCollision; }
  static get statusBush() { return statusBush; }
  static get statusCounter() { return statusCounter; }
  static get statusDamage() { return statusDamage; }
  static get statusLadder() { return statusLadder; }
  static get customCollisionTable() { return customCollisionTable; }
  static get tileBlendingTable() { return tileBlendingTable; }

  static get mapCaches() { return mapCaches; }

  static get currentZoom() { return currentZoom; }
  static set currentZoom(value) {
    currentZoom = value;
    $gameScreen._zoomScale = value;

    if (this.isMapEditorScene()) {
      $gameMap.zoom = new Point(value, value);
      SceneManager._scene._mapEditorGrid.refresh();
      SceneManager._scene._spriteset.updatePosition();
    }

    // if (Utils.isNwjs()) {
    //   this.zoom100Menu.checked = value === 1;
    //   this.zoom150Menu.checked = value === 1.5;
    //   this.zoom200Menu.checked = value === 2;
    //   this.zoom400Menu.checked = value === 4;
    // }
  }

  static get changingTileProps() {
    return tilePropTools.includes(currentTool);
  }

  static register() {
    super.initialize('CycloneMapEditor');

    this.structs.set('CycloneRegionIcon', {
      regionId: 'int',
      icon: 'int',
    });

    super.register({
      regionIcons: {
        type: 'struct<CycloneRegionIcon>[]',
        defaultValue: '[]',
      },
      showMapId: {
        type: 'boolean',
        defaultValue: true,
      },
      showTilesetId: {
        type: 'boolean',
        defaultValue: true,
      },
      showPosition: {
        type: 'boolean',
        defaultValue: true,
      },
      showCellTiles: {
        type: 'boolean',
        defaultValue: true,
      },
      showRegionId: {
        type: 'boolean',
        defaultValue: true,
      },
      showTag: {
        type: 'boolean',
        defaultValue: true,
      },
      showCollision: {
        type: 'boolean',
        defaultValue: true,
      },
      showLadder: {
        type: 'boolean',
        defaultValue: true,
      },
      showBush: {
        type: 'boolean',
        defaultValue: true,
      },
      showCounter: {
        type: 'boolean',
        defaultValue: true,
      },
      showDamageFloor: {
        type: 'boolean',
        defaultValue: true,
      },
      collisionStepCount: {
        type: 'int',
        defaultValue: 1,
      }
    });

    document.addEventListener('keydown', (...args) => {
      this.onKeyDown(...args);
    });
    document.addEventListener('keypress', (...args) => {
      this.onKeyPress(...args);
    });
    document.addEventListener('keyup', (...args) => {
      this.onKeyUp(...args);
    });

    const regionIcons = this.params.regionIcons;
    this.regionIcons = new Map();
    if (regionIcons) {
      for (const { regionId, icon } of regionIcons) {
        if (regionId && icon) {
          this.regionIcons.set(regionId, icon);
        }
      }
    }
  }

  static mapEditorScene() {
    return Scene_Map;
  }

  static isMapEditorScene() {
    return SceneManager._scene instanceof (this.mapEditorScene());
  }

  static makeMenuEvent(fn) {
    return () => {
      if (TouchInput.isPressed()) {
        return;
      }

      fn();
    };
  }

  static addFileMenu(menu) {
    const fileMenu = new nw.Menu();
    fileMenu.append(new nw.MenuItem( {
      label: 'Save Current Map',
      key: 's',
      modifiers: 'ctrl',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.saveButton();
      })
    }));
    fileMenu.append(new nw.MenuItem( {
      label: 'Reload Current Map',
      key: 'r',
      modifiers: 'ctrl',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.reloadButton();
      })
    }));

    fileMenu.append(new nw.MenuItem( {type: 'separator'}));
    fileMenu.append(new nw.MenuItem( {label: 'Exit', click: this.makeMenuEvent(() => {
      window.close();
    })}));

    menu.append(new nw.MenuItem({
      label: 'File',
      submenu: fileMenu,
    }));
  }

  static addEditMenu(menu) {
    const editMenu = new nw.Menu();
    editMenu.append(new nw.MenuItem( {
      label: 'Undo',
      key: 'z',
      modifiers: 'ctrl',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.undoButton();
      })
    }));
    editMenu.append(new nw.MenuItem( {
      label: 'Redo',
      key: 'y',
      modifiers: 'ctrl',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.redoButton();
      })
    }));
    editMenu.append(new nw.MenuItem( {type: 'separator'}));
    this.showGridMenu = new nw.MenuItem( {
      label: 'Show Grid',
      type: 'checkbox',
      checked: showGrid,
      key: 'g',
      modifiers: 'ctrl',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.showGridButton();
      })
    });
    editMenu.append(this.showGridMenu);

    // const zoomMenu = new nw.Menu();
    // this.zoom100Menu = new nw.MenuItem({
    //   label: '100%',
    //   type: 'checkbox',
    //   checked: currentZoom === 1,
    //   click: this.makeMenuEvent(() => {
    //     this.currentZoom = 1;

    //   }),
    // });
    // zoomMenu.append(this.zoom100Menu);
    // this.zoom150Menu = new nw.MenuItem({
    //   label: '150%',
    //   type: 'checkbox',
    //   checked: currentZoom === 1.5,
    //   click: this.makeMenuEvent(() => {
    //     this.currentZoom = 1.5;

    //   }),
    // });
    // zoomMenu.append(this.zoom150Menu);
    // this.zoom200Menu = new nw.MenuItem({
    //   label: '200%',
    //   type: 'checkbox',
    //   checked: currentZoom === 2,
    //   click: this.makeMenuEvent(() => {
    //     this.currentZoom = 2;

    //   }),
    // });
    // zoomMenu.append(this.zoom200Menu);

    // this.zoom400Menu = new nw.MenuItem({
    //   label: '400%',
    //   type: 'checkbox',
    //   checked: currentZoom === 4,
    //   click: this.makeMenuEvent(() => {
    //     this.currentZoom = 4;
    //   }),
    // });
    // zoomMenu.append(this.zoom400Menu);

    // editMenu.append(new nw.MenuItem({
    //   label: 'Zoom',
    //   submenu: zoomMenu,
    // }));

    menu.append(new nw.MenuItem({
      label: 'Edit',
      submenu: editMenu,
    }));
  }

  static addMapMenu(menu) {
    const mapMenu = new nw.Menu();
    mapMenu.append(new nw.MenuItem({
      label: 'Scroll Up',
      key: 'w',
      click: () => {
        $gameMap.scrollUp(3);
      },
    }));
    mapMenu.append(new nw.MenuItem({
      label: 'Scroll Left',
      key: 'a',
      click: () => {
        $gameMap.scrollLeft(3);
      },
    }));
    mapMenu.append(new nw.MenuItem({
      label: 'Scroll Down',
      key: 's',
      click: () => {
        $gameMap.scrollDown(3);
      },
    }));
    mapMenu.append(new nw.MenuItem({
      label: 'Scroll Right',
      key: 'd',
      click: () => {
        $gameMap.scrollRight(3);
      },
    }));

    menu.append(new nw.MenuItem({
      label: 'Map',
      submenu: mapMenu,
    }));
  }

  static addModeMenu(menu) {
    const modeMenu = new nw.Menu();
    this.pencilMenu = new nw.MenuItem( {
      label: 'Pencil',
      type: 'checkbox',
      checked: currentTool === 'pencil',
      key: 'p',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.pencilButton();
      })
    });
    modeMenu.append(this.pencilMenu);
    this.rectangleMenu = new nw.MenuItem( {
      label: 'Rectangle',
      type: 'checkbox',
      checked: currentTool === 'rectangle',
      key: 'r',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.rectangleButton();
      })
    });
    modeMenu.append(this.rectangleMenu);
    this.fillMenu = new nw.MenuItem( {
      label: 'Flood Fill',
      type: 'checkbox',
      checked: currentTool === 'fill',
      key: 'f',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.fillButton();
      })
    });
    modeMenu.append(this.fillMenu);
    modeMenu.append(new nw.MenuItem( {type: 'separator'}));
    this.puzzleMenu = new nw.MenuItem( {
      label: 'Magic Mode',
      type: 'checkbox',
      checked: puzzleMode,
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.puzzleButton();
      })
    });
    modeMenu.append(this.puzzleMenu);
    modeMenu.append(new nw.MenuItem( {type: 'separator'}));

    this.eraserMenu = new nw.MenuItem( {
      label: 'Eraser',
      type: 'checkbox',
      checked: currentTool === 'eraser',
      key: 'e',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.eraserButton();
      })
    });
    modeMenu.append(this.eraserMenu);
    modeMenu.append(new nw.MenuItem( {type: 'separator'}));

    const tilesetPropsMenu = new nw.Menu();
    this.tilePassageMenu = new nw.MenuItem({
      label: 'Passage',
      type: 'checkbox',
      checked: currentTool === Tools.passage,
      key: 'p',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.passage);
      }),
    });
    tilesetPropsMenu.append(this.tilePassageMenu);

    this.tilePassage4Menu = new nw.MenuItem({
      label: 'Passage (4 dir)',
      type: 'checkbox',
      checked: currentTool === Tools.passage4,
      key: 'o',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.passage4);
      }),
    });
    tilesetPropsMenu.append(this.tilePassage4Menu);

    this.tileLadderMenu = new nw.MenuItem({
      label: 'Ladder',
      type: 'checkbox',
      checked: currentTool === Tools.ladder,
      key: 'l',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.ladder);
      }),
    });
    tilesetPropsMenu.append(this.tileLadderMenu);

    this.tileBushMenu = new nw.MenuItem({
      label: 'Bush',
      type: 'checkbox',
      checked: currentTool === Tools.bush,
      key: 'b',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.bush);
      }),
    });
    tilesetPropsMenu.append(this.tileBushMenu);

    this.tileCounterMenu = new nw.MenuItem({
      label: 'Counter',
      type: 'checkbox',
      checked: currentTool === Tools.counter,
      key: 'c',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.counter);
      }),
    });
    tilesetPropsMenu.append(this.tileCounterMenu);

    this.tileDamageMenu = new nw.MenuItem({
      label: 'Damage',
      type: 'checkbox',
      checked: currentTool === Tools.damage,
      key: 'd',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.damage);
      }),
    });
    tilesetPropsMenu.append(this.tileDamageMenu);

    this.tileTerrainMenu = new nw.MenuItem({
      label: 'Terrain Tag',
      type: 'checkbox',
      checked: currentTool === Tools.terrain,
      key: 't',
      modifiers: 'shift',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.toolButton(Tools.terrain);
      }),
    });
    tilesetPropsMenu.append(this.tileTerrainMenu);

    modeMenu.append(new nw.MenuItem({
      label: 'Tile Properties',
      submenu: tilesetPropsMenu,
    }));

    menu.append(new nw.MenuItem({
      label: 'Mode',
      submenu: modeMenu,
    }));
  }

  static addLayerMenu(menu) {
    const layerMenu = new nw.Menu();
    this.autoLayerButton = new nw.MenuItem( {
      label: 'Automatic',
      type: 'checkbox',
      checked: currentLayer === 7,
      key: '0',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(7);
      })
    });
    this.layer1Button = new nw.MenuItem( {
      label: 'Layer 1',
      type: 'checkbox',
      checked: currentLayer === 0,
      key: '1',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(0);
      })
    });
    this.layer2Button = new nw.MenuItem( {
      label: 'Layer 2',
      type: 'checkbox',
      checked: currentLayer === 1,
      key: '2',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(1);
      })
    });
    this.layer3Button = new nw.MenuItem( {
      label: 'Layer 3',
      type: 'checkbox',
      checked: currentLayer === 2,
      key: '3',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(2);
      })
    });
    this.layer4Button = new nw.MenuItem( {
      label: 'Layer 4',
      type: 'checkbox',
      checked: currentLayer === 3,
      key: '4',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(3);
      })
    });
    layerMenu.append(this.autoLayerButton);
    layerMenu.append(this.layer1Button);
    layerMenu.append(this.layer2Button);
    layerMenu.append(this.layer3Button);
    layerMenu.append(this.layer4Button);

    layerMenu.append(new nw.MenuItem( {type: 'separator'}));
    this.shadowsButton = new nw.MenuItem( {
      label: 'Shadows',
      type: 'checkbox',
      checked: currentLayer === 4,
      key: '5',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(4);
      })
    });
    layerMenu.append(this.shadowsButton);
    this.regionsButton = new nw.MenuItem( {
      label: 'Regions',
      type: 'checkbox',
      checked: currentLayer === 5,
      key: '6',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(5);
      })
    });
    layerMenu.append(this.regionsButton);
    this.eventsButton = new nw.MenuItem( {
      label: 'Events',
      type: 'checkbox',
      checked: currentLayer === 6,
      key: '7',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(6);
      })
    });
    layerMenu.append(this.eventsButton);
    layerMenu.append(new nw.MenuItem( {type: 'separator'}));
    this.collisionsButton = new nw.MenuItem( {
      label: 'Collisions',
      type: 'checkbox',
      checked: currentLayer === 8,
      key: '8',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(8);
      })
    });
    layerMenu.append(this.collisionsButton);
    this.tagsButton = new nw.MenuItem( {
      label: 'Tags',
      type: 'checkbox',
      checked: currentLayer === 9,
      key: '9',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(9);
      })
    });
    layerMenu.append(this.tagsButton);

    menu.append(new nw.MenuItem({
      label: 'Layer',
      submenu: layerMenu,
    }));
  }

  static addBlendMenu(menu) {
    const blendMenu = new nw.Menu();

    this.blendButton = new nw.MenuItem( {
      label: 'Blend Layer',
      type: 'checkbox',
      checked: currentLayer === 10,
      key: 'B',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.changeCurrentLayer(10);
        CycloneMapEditor$1.updateCurrentTool();
      })
    });
    blendMenu.append(this.blendButton);

    blendMenu.append(new nw.MenuItem( {type: 'separator'}));
    blendMenu.append(new nw.MenuItem( {
      label: 'Remove blend effect',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.removeAllBlendsButton();
      })
    }));
    blendMenu.append(new nw.MenuItem( {
      label: 'Optimize blend effect',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.optimizeBlendsButton();
      })
    }));


    menu.append(new nw.MenuItem({
      label: 'Blend',
      submenu: blendMenu,
    }));
  }

  static addTilesetMenu(menu) {
    const tilesetMenu = new nw.Menu();

    this._mainTilesetMenu = new nw.MenuItem({
      label: 'Main Tileset',
      enabled: false,
    });

    tilesetMenu.append(this._mainTilesetMenu);
    this._extraTilesetMenu = new nw.Menu();

    for (const tileset of $dataTilesets) {
      if (!tileset) {
        continue;
      }

      const tilesetNames = tileset.tilesetNames;
      if (!tilesetNames[5] && !tilesetNames[6]) {
        continue;
      }

      const menuItem = new nw.MenuItem({
        label: `${ tileset.id.padZero(4) } ${ tileset.name }`,
        enabled: true,
        type: 'checkbox',
        click: this.makeMenuEvent(() => {
          this.toggleTileset(tileset.id);
        }),
      });

      this._extraTilesetMenu.append(menuItem);
    }

    this._extraTilesetMenuItem = new nw.MenuItem({
      label: 'Extra Tileset',
      submenu: this._extraTilesetMenu,
    });

    tilesetMenu.append(this._extraTilesetMenuItem);

    menu.append(new nw.MenuItem({
      label: 'Tilesets',
      submenu: tilesetMenu,
    }));
  }

  static addJumpMenu(menu) {
    const a1 = Tilemap.TILE_ID_A1;
    const a2 = Tilemap.TILE_ID_A2;
    const a3 = Tilemap.TILE_ID_A3;
    const a4 = Tilemap.TILE_ID_A4;
    const b = Tilemap.TILE_ID_B;
    const c = Tilemap.TILE_ID_C;
    const d = Tilemap.TILE_ID_D;
    const e = Tilemap.TILE_ID_E;
    const f = Tilemap.TILE_ID_E + 256;
    const g = Tilemap.TILE_ID_E + 512;
    const a5 = Tilemap.TILE_ID_A5;
    const h = Tilemap.TILE_ID_A5 + 256;

    const jumpToTabMenu = new nw.Menu();
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'AutoTiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([a1, a2, a3, a4]);
      }),
    }));
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'A5 Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToTile(a5);
      }),
    }));
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'B Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([b, c, d, e, f, g, a5]);
      }),
    }));
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'C Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([c, d, e, f, g, a5]);
      }),
    }));
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'D Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([d, e, f, g, a5]);
      }),
    }));
    jumpToTabMenu.append(new nw.MenuItem({
      label: 'E Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([e, f, g, a5]);
      }),
    }));
    this._jumpToExtraBMenu = new nw.MenuItem({
      label: 'Extra B Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([f, g, a5]);
      }),
    });
    jumpToTabMenu.append(this._jumpToExtraBMenu);
    this._jumpToExtraCMenu = new nw.MenuItem({
      label: 'Extra C Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([g, a5]);
      }),
    });
    jumpToTabMenu.append(this._jumpToExtraCMenu);
    this._jumpToExtraDMenu = new nw.MenuItem({
      label: 'Extra D Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.jumpToOneTileOf([h, a5]);
      }),
    });
    jumpToTabMenu.append(this._jumpToExtraDMenu);

    menu.append(new nw.MenuItem({
      label: 'Jump To',
      submenu: jumpToTabMenu,
    }));
  }

  static refreshTilesetMenu() {
    if (!this._extraTilesetMenu) {
      return;
    }

    const tileset = $gameMap.tileset();
    this._mainTilesetMenu.label = `${ tileset.id.padZero(4) } ${ tileset.name }`;

    for (const item of this._extraTilesetMenu.items) {
      const id = parseInt(item.label.substring(0, 4), 10);
      if (id === $gameMap._tilesetId) {
        item.checked = false;
        item.enabled = false;
        continue;
      }

      item.enabled = true;
      item.checked = $gameMap._extraTilesetId === id;
    }
  }

  static addToolsMenu(menu) {
    const toolsMenu = new nw.Menu();

    const resizeTilesets = new nw.MenuItem({
      label: 'Generate 48x48 Tilesets',
      enabled: $gameMap.tileWidth() !== 48 || $gameMap.tileHeight() !== 48,
      click: this.makeMenuEvent(() => {
        const width = $gameMap.tileWidth();
        const height = $gameMap.tileHeight();

        let message;

        if (globalThis.CycloneMaps && CycloneMaps.params.tilesetPath) {
          const realPath = CycloneMaps.params.tilesetPath;
          const fakePath = 'img/tilesets/';
          message = `This option will replace the 48x48 files on ${ fakePath } with resized copies of the ${ width }x${ height } files on ${ realPath }. Are you SURE you want to do this?`;
        } else {
          message = `This option will replace the files on /img/tilesets with resized copies of your ${ width }x${ height } tilesets. Are you SURE you want to do this?`;
        }

        CycloneMapEditor$1.resizeTilesets(message);
      }),
    });

    toolsMenu.append(resizeTilesets);
    menu.append(new nw.MenuItem({
      label: 'Tools',
      submenu: toolsMenu,
    }));
  }

  static addExportMenu(menu) {
    const exportMenu = new nw.Menu();
    const exportLayersMenu = new nw.Menu();
    exportLayersMenu.append(new nw.MenuItem({
      label: 'Layer 1',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportSingleLayer(0);
      }),
    }));
    exportLayersMenu.append(new nw.MenuItem({
      label: 'Layer 2',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportSingleLayer(1);
      }),
    }));
    exportLayersMenu.append(new nw.MenuItem({
      label: 'Layer 3',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportSingleLayer(2);
      }),
    }));
    exportLayersMenu.append(new nw.MenuItem({
      label: 'Layer 4',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportSingleLayer(3);
      }),
    }));

    exportMenu.append(new nw.MenuItem({
      label: 'Layers',
      submenu: exportLayersMenu,
    }));

    const exportRenderedMapMenu = new nw.Menu();
    exportRenderedMapMenu.append(new nw.MenuItem({
      label: 'Lower Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportLowerTiles();
      }),
    }));
    exportRenderedMapMenu.append(new nw.MenuItem({
      label: 'Upper Tiles',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportUpperTiles();
      }),
    }));
    exportRenderedMapMenu.append(new nw.MenuItem( {type: 'separator'}));
    exportRenderedMapMenu.append(new nw.MenuItem({
      label: 'Whole Map',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportWholeMap();
      }),
    }));
    exportMenu.append(new nw.MenuItem({
      label: 'Rendered Map',
      submenu: exportRenderedMapMenu,
    }));

    const exportEventsMenu = new nw.Menu();
    exportEventsMenu.append(new nw.MenuItem({
      label: 'Low Events',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportLowEvents();
      }),
    }));
    exportEventsMenu.append(new nw.MenuItem({
      label: 'Normal Events',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportNormalEvents();
      }),
    }));
    exportEventsMenu.append(new nw.MenuItem({
      label: 'High Events',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportHighEvents();
      }),
    }));
    exportEventsMenu.append(new nw.MenuItem( {type: 'separator'}));
    exportEventsMenu.append(new nw.MenuItem({
      label: 'All Events',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportAllEvents();
      }),
    }));
    exportMenu.append(new nw.MenuItem({
      label: 'Events',
      submenu: exportEventsMenu,
    }));

    const exportCollisionsMenu = new nw.Menu();
    exportCollisionsMenu.append(new nw.MenuItem({
      label: 'Custom Collision',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportCustomCollision();
      }),
    }));
    exportCollisionsMenu.append(new nw.MenuItem({
      label: 'Full Collision',
      click: this.makeMenuEvent(() => {
        CycloneMapEditor$1.exportFullCollision();
      }),
    }));
    exportMenu.append(new nw.MenuItem({
      label: 'Collision',
      submenu: exportCollisionsMenu,
    }));

    menu.append(new nw.MenuItem({
      label: 'Export',
      submenu: exportMenu,
    }));
  }

  static addHelpMenu(menu) {
    const helpMenu = new nw.Menu();
    helpMenu.append(new nw.MenuItem( {
      label: 'Plugin Page',
      key: 'F1',
      click: this.makeMenuEvent(() => {
        if (!globalThis.require) {
          return;
        }

        require('nw.gui').Shell.openExternal('https://makerdevs.com/plugin/cyclone-map-editor');
      }),
    }));

    helpMenu.append(new nw.MenuItem( {
      label: 'Extra Tilesets Add-on',
      click: this.makeMenuEvent(() => {
        if (!globalThis.require) {
          return;
        }

        require('nw.gui').Shell.openExternal('https://hudell.itch.io/cyclone-extra-tilesets');
      }),
    }));

    helpMenu.append(new nw.MenuItem( {
      label: 'Magic (Tile Blend) Add-on',
      click: this.makeMenuEvent(() => {
        if (!globalThis.require) {
          return;
        }

        require('nw.gui').Shell.openExternal('https://hudell.itch.io/cyclone-magic');
      }),
    }));

    menu.append(new nw.MenuItem({
      label: 'Help',
      submenu: helpMenu,
    }));
  }

  static addMenuBar() {
    if (!Utils.isNwjs()) {
      return;
    }
    if (this.menu) {
      return this.refreshMenuVisibility();
    }

    const menu = new nw.Menu({ type: 'menubar' });

    this.addFileMenu(menu);
    this.addEditMenu(menu);
    this.addMapMenu(menu);
    this.addModeMenu(menu);
    this.addLayerMenu(menu);
    this.addBlendMenu(menu);
    this.addTilesetMenu(menu);
    this.addJumpMenu(menu);
    this.addToolsMenu(menu);
    this.addExportMenu(menu);
    this.addHelpMenu(menu);

    this.menu = menu;
    this.refreshTilesetMenu();
    this.refreshMenuVisibility();
  }

  static clearAllData() {
    changeHistory = [];
    undoHistory = [];
    rectangleStartMouseX = 0;
    rectangleStartMouseY = 0;
    rectangleStartX = 0;
    rectangleStartY = 0;
    rectangleWidth = 0;
    rectangleHeight = 0;
    rectangleBackWidth = 0;
    rectangleBackHeight = 0;
    customCollisionTable = {};
    tileBlendingTable = {};

    this.clearSelection();
  }

  static toggleTileset(id) {
    if ($gameMap._extraTilesetId === id) {
      $gameMap._extraTilesetId = 0;
    } else {
      $gameMap._extraTilesetId = id;
    }

    $gameMap.buildTilesetFlags && $gameMap.buildTilesetFlags();

    this.refreshTilesetMenu();
    this.refreshMapEditor();

    if (!this.jumpToTile(Tilemap.TILE_ID_E + 256) && !this.jumpToTile(Tilemap.TILE_ID_E + 512)) {
      this.jumpToLastTile();
    }
  }

  static applyExtraData(data) {
    customCollisionTable = {};
    tileBlendingTable = {};
    $gameMap._extraTilesetId = 0;

    const radix = data?.radix || 10;

    if (data?.collision) {
      for (let i = 0; i < data.collision.length; i++) {
        const col = parseInt(data.collision[i], radix) || 0;
        if (col) {
          customCollisionTable[i] = col;
        }
      }
    }

    if (data?.magic) {
      for (let tileId in data.magic) {
        if (!data.magic[tileId]) {
          continue;
        }

        const line = data.magic[tileId];
        const buffer = new ArrayBuffer(line.length);
        const list = new Uint8Array(buffer);
        for (let i = line.indexOf('1'); i < line.length; i++) {
          if (line[i] !== '0') {
            list[i] = Number(line[i]);
          }
        }
        tileBlendingTable[tileId] = list;
      }
    }

    if (data?.extraTilesetId) {
      $gameMap._extraTilesetId = data.extraTilesetId;
    }

    this.refreshTilesetMenu();
    this.refreshMapEditor();
  }

  static dataVersion() {
    return '02';
  }

  static compress(data) {
    return `v=${ this.dataVersion() };` + LZString.compressToBase64(data);
  }

  static decompress(data) {
    if (!data.startsWith('v=')) {
      return LZString.decompress(data);
    }

    const idx = data.indexOf(';') + 1;
    return LZString.decompressFromBase64(data.substring(idx));
  }

  static parseExtraData(note) {
    let json;
    try {
      json = this.decompress(note);
    } catch(e) {
      console.error('Failed to decompress data from CycloneMapEditor event.');
      console.log(note);
      console.log(e);
      return;
    }

    let data;
    try {
      data = JSON.parse(json);

    } catch(e) {
      console.error('Failed to parse extra data.');
      console.log(json);
      console.log(e);
      return;
    }

    this.applyExtraData(data);
  }

  static loadExtraData() {
    // Check if there's any event called CycloneMapEditor
    for (const event of $dataMap.events) {
      if (!event) {
        continue;
      }

      if (event.name !== 'CycloneMapEditor') {
        continue;
      }

      this.parseExtraData(event.note);
      return;
    }
  }

  static getExtraData() {
    const radix = 36;
    const collision = new Array($dataMap.width * $dataMap.height * 16);
    for (let i = 0; i < collision.length; i++) {
      if (customCollisionTable[i]) {
        if (customCollisionTable[i] >= radix) {
          throw new Error('Invalid collision value: ', customCollisionTable[i]);
        }

        collision[i] = Number(customCollisionTable[i]).toString(radix);
      } else {
        collision[i] = '0';
      }
    }
    const magic = {};
    for (const tileId in tileBlendingTable) {
      if (!tileBlendingTable[tileId]) {
        continue;
      }
      const line = tileBlendingTable[tileId].join('');
      if (!line.includes('1')) {
        continue;
      }
      magic[tileId] = line;
    }

    const puzzle = window.CycloneMagic?.puzzleTiles || undefined;

    return {
      radix,
      collision: collision.join(''),
      magic,
      puzzle,
      extraTilesetId: $gameMap._extraTilesetId,
    };
  }

  static getExtraDataJson() {
    return this.compress(JSON.stringify(this.getExtraData(), null, 0));
  }

  static saveExtraData() {
    const data = this.getExtraDataJson();
    // Check if there's any event called CycloneMapEditor
    for (const event of $dataMap.events) {
      if (!event) {
        continue;
      }

      if (event.name !== 'CycloneMapEditor') {
        continue;
      }

      event.note = data;
      return;
    }

    // Create a new event then
    $dataMap.events.push({
      id: $dataMap.events.length,
      name: 'CycloneMapEditor',
      note: data,
      pages: [],
      x: $dataMap.width,
      y: $dataMap.height,
    });
  }

  static clearSelection() {
    currentTileId = undefined;
    tileCols = 1;
    tileRows = 1;
    selectedTileList = [];
    multiLayerSelection = [];
  }

  static shouldDisplayMenu() {
    if (!editorActive) {
      return false;
    }

    if (!this.isMapEditorScene()) {
      return false;
    }

    return true;
  }

  static isFullScreen() {
    return Graphics._isFullScreen();
  }

  static refreshScreenSize() {
    if (this.resizeTimeout) {
      clearTimeout(this.resizeTimeout);
    }

    if (this.isFullScreen()) {
      return;
    }

    this.resizeTimeout = setTimeout(() => {
      // Adds a second timeout to block the show/hide functionality for a little while
      this.resizeTimeout = setTimeout(() => {
        this.resizeTimeout = false;
      }, 500);

      const xDelta = Graphics.width - window.innerWidth;
      const yDelta = Graphics.height - window.innerHeight;
      if (xDelta !== 0 || yDelta !== 0) {
        window.moveBy(-xDelta / 2, -yDelta / 2);
        window.resizeBy(xDelta, yDelta);
      }
    }, 20);
  }

  static enablePluginOptions() {
    if (this.blendButton) {
      this.blendButton.enabled = Boolean(window.CycloneMagic);
    }
    if (this.puzzleMenu) {
      this.puzzleMenu.enabled = Boolean(window.CycloneMagic);
    }
    if (this._extraTilesetMenuItem) {
      this._extraTilesetMenuItem.enabled = Boolean(window.CycloneExtraTilesets);
    }
    if (this._jumpToExtraBMenu) {
      this._jumpToExtraBMenu.enabled = Boolean(window.CycloneExtraTilesets);
    }
    if (this._jumpToExtraCMenu) {
      this._jumpToExtraCMenu.enabled = Boolean(window.CycloneExtraTilesets);
    }
    if (this._jumpToExtraDMenu) {
      this._jumpToExtraDMenu.enabled = Boolean(window.CycloneExtraTilesets);
    }
  }

  static refreshMenuVisibility() {
    if (!Utils.isNwjs()) {
      return;
    }

    const display = this.shouldDisplayMenu();
    const win = nw.Window.get();

    this.enablePluginOptions();
    if (display && win.menu === this.menu) {
      return;
    }

    if (display) {
      win.menu = this.menu;
    } else {
      win.menu = null;
    }

    this.refreshScreenSize();
  }

  static logImage(canvas, text) {
    logImage(canvas, text);
  }

  static isTabValid(tab) {
    const tileset = $gameMap.tileset();
    if (!tileset) {
      return false;
    }

    const names = tileset.tilesetNames;

    if (tab === 'A') {
      return Boolean(names[0] || names[1] || names[2] || names[3] || names[4]);
    }

    const tilesetIndex = tabs.indexOf(tab) + 4;
    return Boolean(names[tilesetIndex]);
  }

  static validTabs() {
    return tabs.filter(tab => this.isTabValid(tab));
  }

  static areRegionsVisible() {
    return layerVisibility[5];
  }

  static isLayerVisible(index) {
    if (index >= 8 && index <= 10) {
      return currentLayer === index;
    }

    if (index === 7) {
      return true;
    }

    return layerVisibility[index];
  }

  static selectPreviousTab() {
    const validTabs = this.validTabs();
    const oldIndex = validTabs.indexOf(currentTab).clamp(0, validTabs.length - 1);

    const index = oldIndex === 0 ? validTabs.length -1 : oldIndex -1;
    this.changeCurrentTab(validTabs[index % validTabs.length]);
  }

  static selectNextTab() {
    const validTabs = this.validTabs();
    const oldIndex = validTabs.indexOf(currentTab).clamp(0, validTabs.length - 1);

    const index = oldIndex +1;
    this.changeCurrentTab(validTabs[index % validTabs.length]);
  }

  static onKeyDown(event) {
    if (!editorActive) {
      return;
    }

    // const scene = SceneManager._scene;
    if (!this.isMapEditorScene()) {
      return;
    }

    if (event.keyCode === 8 || event.keyCode === 46) {
      this.eraserButton();
      return;
    }

    if (event.keyCode === 33) {
      this.selectPreviousTab();
      return;
    }

    if (event.keyCode === 34) {
      this.selectNextTab();
      return;
    }
  }

  static checkNumKeys(code) {
    switch(code) {
      case 'Numpad0':
        this.changeCurrentLayer(7);
        break;
      case 'Numpad1':
        this.changeCurrentLayer(0);
        break;
      case 'Numpad2':
        this.changeCurrentLayer(1);
        break;
      case 'Numpad3':
        this.changeCurrentLayer(2);
        break;
      case 'Numpad4':
        this.changeCurrentLayer(3);
        break;
      case 'Numpad5':
        this.changeCurrentLayer(4);
        break;
      case 'Numpad6':
        this.changeCurrentLayer(5);
        break;
      case 'Numpad7':
        this.changeCurrentLayer(6);
        break;
      case 'Numpad8':
        this.changeCurrentLayer(8);
        break;
      case 'Numpad9':
        this.changeCurrentLayer(9);
        break;
    }
  }

  static checkLayerKeys(key) {
    switch(key) {
      case '0':
        this.changeCurrentLayer(7);
        break;
      case '1':
        this.changeCurrentLayer(0);
        break;
      case '2':
        this.changeCurrentLayer(1);
        break;
      case '3':
        this.changeCurrentLayer(2);
        break;
      case '4':
        this.changeCurrentLayer(3);
        break;
      case '5':
        this.changeCurrentLayer(4);
        break;
      case '6':
        this.changeCurrentLayer(5);
        break;
      case '8':
        this.changeCurrentLayer(8);
        break;
      case '9':
        this.changeCurrentLayer(9);
        break;
    }
  }

  static checkScrollKeys(key) {
    switch(key.toLowerCase()) {
      case 'w':
        $gameMap.scrollUp(3);
        break;
      case 'a':
        $gameMap.scrollLeft(3);
        break;
      case 's':
        $gameMap.scrollDown(3);
        break;
      case 'd':
        $gameMap.scrollRight(3);
        break;
    }
  }

  static sceneToReload() {
    return Scene_Map;
  }

  static loadMapFile() {
    SceneManager._scene._mapEditorCommands.hide();
    delete mapCaches[$gameMap._mapId];
    const fileName = `Map${ $gameMap._mapId.padZero(3) }.json`;

    const xhr = new XMLHttpRequest();
    const url = `data/${ fileName }`;

    xhr.open('GET', url);
    xhr.overrideMimeType('application/json');

    xhr.onload = () => {
      try {
        const data = JSON.parse(xhr.responseText);

        // eslint-disable-next-line no-global-assign
        $dataMap = data;
        SoundManager.playLoad();
        SceneManager.goto(this.sceneToReload());
      } catch (e) {
        alert('Failed to parse map data.');
        SceneManager._scene.refreshMapEditorWindows();
      }
    };
    xhr.onerror = () => {
      alert('Failed to load map file from disk.');
      SceneManager._scene.refreshMapEditorWindows();
    };

    xhr.send();
  }

  static downloadMapshot(bitmap, fileName) {
    const imageType = 'png';
    const imageQuality = 1;

    const urlData = bitmap.canvas.toDataURL(imageType, imageQuality);
    const strippedData = urlData.replace(/^data:image\/png;base64,/, '');

    const data = atob(strippedData);
    const buffer = new ArrayBuffer(data.length);
    const view = new Uint8Array(buffer);
    for (let i = 0; i < data.length; i++) {
      view[i] = data.charCodeAt(i) & 0xff;
    }
    const blob = new Blob([buffer], { type: 'application/octet-stream'});
    const url = URL.createObjectURL(blob);

    let iframe = document.getElementsByName('image_download')[0];
    if (!iframe) {
      iframe = document.createElement('iframe');
      iframe.setAttribute('name', 'image_download');
      iframe.style.display = 'none';
      document.body.appendChild(iframe);
    }

    const element = document.createElement('a');
    element.setAttribute('href', url);
    element.setAttribute('download', fileName + '.png');
    element.setAttribute('target', 'image_download');
    element.style.display = 'none';
    document.body.appendChild(element);

    element.click();

    document.body.removeChild(element);
  }

  static resizeTilesets(message) {
    const tileset = $dataTilesets[$dataMap.tilesetId];
    if (!tileset) {
      return alert('Tileset data not found.');
    }

    if (!Utils.isNwjs()) {
      return alert('This feature can only be used on a computer with a non web-version.');
    }

    const fileNames = tileset.tilesetNames;
    const existingFiles = [];
    const fs = require('fs');

    for (const fileName of fileNames) {
      if (!fileName) {
        continue;
      }

      if (fs.existsSync(`img/tilesets/${ fileName }.png`)) {
        existingFiles.push(fileName);
      }
    }

    if (existingFiles.length) {
      const overwrittenFilesMessage = `Files that will be replaced: ${ existingFiles.join(', ') }`;
      const newMessage = `${ message }\n${ overwrittenFilesMessage}`;
      if (!confirm(newMessage)) {
        return;
      }
    }

    this.doResizeTiles(fileNames);
  }

  static doResizeTiles(fileNames) {
    const width = $gameMap.tileWidth();
    const height = $gameMap.tileHeight();
    const fs = require('fs');

    for (const fileName of fileNames) {
      if (!fileName) {
        continue;
      }

      const bitmap = ImageManager.loadTileset(fileName);
      if (!bitmap) {
        continue;
      }

      const newWidth = Math.floor(bitmap.width / width * 48);
      const newHeight = Math.floor(bitmap.height / height * 48);

      const newBitmap = new Bitmap(newWidth, newHeight);
      newBitmap.blt(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, newWidth, newHeight);

      const urlData = newBitmap.canvas.toDataURL('image/png', 70);
      const base64Data = urlData.replace(/^data:image\/png;base64,/, '');

      fs.writeFileSync(`img/tilesets/${ fileName }.png`, base64Data, 'base64');
    }
  }

  static exportSingleLayer(layerIndex) {
    const tilemap = new MapshotTileMap();
    tilemap.drawSingleLayer(layerIndex);

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Layer${ layerIndex + 1 }`);
  }

  static exportLowerTiles() {
    const tilemap = new MapshotTileMap();
    tilemap.drawLowerTiles();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Lower`);
  }

  static exportUpperTiles() {
    const tilemap = new MapshotTileMap();
    tilemap.drawUpperTiles();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Upper`);
  }

  static exportWholeMap() {
    const tilemap = new MapshotTileMap();
    tilemap.drawLowerTiles();
    tilemap.drawUpperTiles();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }`);
  }

  static exportLowEvents() {
    const tilemap = new MapshotTileMap();
    tilemap.drawEvents(0);

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Events_0`);
  }

  static exportNormalEvents() {
    const tilemap = new MapshotTileMap();
    tilemap.drawEvents(1);

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Events_1`);
  }

  static exportHighEvents() {
    const tilemap = new MapshotTileMap();
    tilemap.drawEvents(2);

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Events_2`);
  }

  static exportAllEvents() {
    const tilemap = new MapshotTileMap();
    tilemap.drawEvents();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Events`);
  }

  static exportCustomCollision() {
    const tilemap = new MapshotTileMap();
    tilemap.drawCustomCollision();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_Collision`);
  }

  static exportFullCollision() {
    const tilemap = new MapshotTileMap();
    tilemap.drawDefaultCollision();
    tilemap.drawCustomCollision();

    this.downloadMapshot(tilemap, `Map${ $gameMap._mapId.padZero(3) }_FullCollision`);
  }

  static undoButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    if (changeHistory.length) {
      this.undoLastChange();
    }
  }

  static redoButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    if (undoHistory.length) {
      this.redoLastUndoneChange();
    }
  }

  static removeAllBlendsButton() {
    if (confirm('Are you sure you want to remove all blend effects on this map?')) {
      this.removeAllBlends();
    }
  }

  static optimizeBlendsButton() {
    if (confirm('This option will remove the blend from tiles that are completely hidden by the effect and change the tile itself to transparent. Optimize now?')) {
      this.optimizeBlends();
    }
  }

  // eslint-disable-next-line complexity
  static getCollisionSymbol(x, y) {
    const downCollision = !$gameMap.isPassable(x, y, 2);
    const leftCollision = !$gameMap.isPassable(x, y, 4);
    const rightCollision = !$gameMap.isPassable(x, y, 6);
    const upCollision = !$gameMap.isPassable(x, y, 8);

    if (downCollision && leftCollision && rightCollision && upCollision) {
      return 'X';
    }

    if (!downCollision && !leftCollision && !rightCollision && !downCollision) {
      return 'O';
    }

    let collisions = '';
    if (downCollision) {
      collisions += 'd:X ';
    } else {
      collisions += 'd:O ';
    }

    if (leftCollision) {
      collisions += 'l:X ';
    } else {
      collisions += 'l:O ';
    }
    if (rightCollision) {
      collisions += 'r:X ';
    } else {
      collisions += 'r:O ';
    }
    if (upCollision) {
      collisions += 'u:X ';
    } else {
      collisions += 'u:O ';
    }

    return collisions;
  }

  // eslint-disable-next-line complexity
  static updateStatus({ tileId, mapX, mapY, tile1, tile2, tile3, tile4 } = {}) {
    const oldTileId = statusTileId;
    const oldX = statusMapX;
    const oldY = statusMapY;
    const oldTile1 = statusTile1;
    const oldTile2 = statusTile2;
    const oldTile3 = statusTile3;
    const oldTile4 = statusTile4;

    statusTileId = tileId ?? statusTileId;
    statusMapX = mapX ?? statusMapX;
    statusMapY = mapY ?? statusMapY;
    statusTile1 = tile1 ?? statusTile1;
    statusTile2 = tile2 ?? statusTile2;
    statusTile3 = tile3 ?? statusTile3;
    statusTile4 = tile4 ?? statusTile4;

    const changedPos = oldX !== statusMapX || oldY !== statusMapY;
    if (changedPos) {
      statusRegion = $gameMap.regionId(statusMapX, statusMapY);
      statusTag = $gameMap.terrainTag(statusMapX, statusMapY);
      statusBush = $gameMap.isBush(statusMapX, statusMapY);
      statusCounter = $gameMap.isCounter(statusMapX, statusMapY);
      statusDamage = $gameMap.isDamageFloor(statusMapX, statusMapY);
      statusLadder = $gameMap.isLadder(statusMapX, statusMapY);
      statusCollision = this.getCollisionSymbol(statusMapX, statusMapY);
    }

    const changedTile = oldTile1 !== statusTile1 || oldTile2 !== statusTile2 || oldTile3 !== statusTile3 || oldTile4 !== statusTile4;
    const changed = changedTile || oldTileId !== statusTileId || changedPos;
    if (!changed) {
      return;
    }

    if (this.isMapEditorScene()) {
      SceneManager._scene._mapEditorStatus.refresh();
    }
  }

  static showGridButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    showGrid = !showGrid;
    this.showGridButton.checked = showGrid;
    SceneManager._scene._mapEditorGrid.refresh();
  }

  static selectHigherLayer(x, y) {
    if (currentLayer === Layers.collisions) {
      return;
    }

    for (let z = 3; z >= 0; z--) {
      const tileIndex = this.tileIndex(x, y, z);
      const tileId = $dataMap.data[tileIndex];

      if (tileId) {
        this.changeCurrentLayer(z);
        return;
      }
    }
  }

  static updateCurrentTool() {
    rectangleWidth = 0;
    rectangleHeight = 0;
    rectangleBackWidth = 0;
    rectangleBackHeight = 0;
    rectangleStartX = 0;
    rectangleStartY = 0;
    rectangleStartMouseX = 0;
    rectangleStartMouseY = 0;

    if (Utils.isNwjs()) {
      this.pencilMenu.checked = currentTool === Tools.pencil;
      this.rectangleMenu.checked = currentTool === Tools.rectangle;
      this.fillMenu.checked = currentTool === Tools.fill;
      this.puzzleMenu.checked = puzzleMode;
      this.eraserMenu.checked = currentTool === Tools.eraser;

      this.tilePassageMenu.checked = currentTool === Tools.passage;
      this.tilePassage4Menu.checked = currentTool === Tools.passage4;
      this.tileLadderMenu.checked = currentTool === Tools.ladder;
      this.tileBushMenu.checked = currentTool === Tools.bush;
      this.tileCounterMenu.checked = currentTool === Tools.counter;
      this.tileDamageMenu.checked = currentTool === Tools.damage;
      this.tileTerrainMenu.checked = currentTool === Tools.terrain;
    }

    this.refreshMapEditor();
  }

  static pencilButton() {
    this.toolButton(Tools.pencil);
  }

  static rectangleButton() {
    this.toolButton(Tools.rectangle);
  }

  static fillButton() {
    this.toolButton(Tools.fill);
  }

  static eraserButton() {
    this.toolButton(Tools.eraser);
  }

  static puzzleButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    puzzleMode = !puzzleMode;
    if (puzzleMode) {
      if (currentTool !== Tools.eraser) {
        currentTool = Tools.pencil;
      }
      if (CycloneMapEditor$1.currentLayer !== 1) {
        this.changeCurrentLayer(1);
      }
    }
    this.clearSelection();
    this.updateCurrentTool();
  }

  static toolButton(toolType) {
    if (!this.isMapEditorScene()) {
      return;
    }

    if (puzzleMode && ![Tools.pencil, Tools.eraser].includes(toolType)) {
      return;
    }

    currentTool = toolType;
    if ([Tools.pencil, Tools.rectangle].includes(toolType)) {
      lastDrawingTool = toolType;
    }
    this.updateCurrentTool();
  }

  static _doWebSave(json, fileName) {
    const element = document.createElement('a');
    element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(json) }`);
    element.setAttribute('download', fileName);
    element.style.display = 'none';
    document.body.appendChild(element);

    element.click();

    document.body.removeChild(element);
  }

  static _doLocalSave(json, fileName) {
    const fs = require('fs');
    const path = require('path');

    const projectFolder = path.dirname(process.mainModule.filename);
    const dataFolder = path.join(projectFolder, 'data');

    const filePath = path.join(dataFolder, fileName);

    fs.writeFileSync(filePath, json);
  }

  static _makeMapJson() {
    const map = {
      ...$dataMap,
      data: [
        ...$dataMap.data,
      ]
    };

    const size = map.width * map.height;
    const extraTiles = new Array(size * 4);
    let anyExtraTile = false;

    for (let i = 0; i < extraTiles.length; i++) {
      if (map.data[i] >= Tilemap.TILE_ID_E + 256 && map.data[i] < Tilemap.TILE_ID_A5) {
        extraTiles[i] = map.data[i];
        map.data[i] = 0;
        anyExtraTile = true;
      } else if (map.data[i] >= Tilemap.TILE_ID_A5 + 256 && map.data[i] < Tilemap.TILE_ID_A1) {
        extraTiles[i] = map.data[i];
        map.data[i] = 0;
        anyExtraTile = true;
      } else {
        extraTiles[i] = 0;
      }
    }

    let extraTilesTag = '';

    if (anyExtraTile) {
      const compressed = LZString.compressToBase64(JSON.stringify(extraTiles, null, 0));

      extraTilesTag = `<CycloneExtraTiles>${ compressed }</CycloneExtraTiles>`;
    }

    if (map.note?.includes('<CycloneExtraTiles>')) {
      map.note = map.note.replace(/<CycloneExtraTiles>.*<\/CycloneExtraTiles>/i, extraTilesTag);
    } else {
      map.note = `${ map.note ?? ''}\n${ extraTilesTag }`;
    }

    return JSON.stringify(map, null, 0);
  }

  static _doSave() {
    this.saveExtraData();

    const fileName = `Map${ $gameMap._mapId.padZero(3) }.json`;
    const json = this._makeMapJson();

    if (Utils.isNwjs()) {
      this._doLocalSave(json, fileName);
    } else {
      this._doWebSave(json, fileName);
    }
    SoundManager.playSave();
  }

  static saveButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    if (!confirm('Are you sure you want to SAVE the map file?')) {
      SceneManager._scene.refreshMapEditorWindows();
      return;
    }

    SceneManager._scene._mapEditorCommands.hide();

    this._doSave();
    SceneManager._scene.refreshMapEditorWindows();
  }

  static reloadButton() {
    if (!this.isMapEditorScene()) {
      return;
    }

    if (!confirm('Are you sure you want to RELOAD the map file?')) {
      SceneManager._scene.refreshMapEditorWindows();
      return;
    }

    this.clearAllData();
    this.loadMapFile();
  }

  static onKeyPress(event) {
    if (editorActive) {
      if (!Utils.isNwjs()) {
        this.checkScrollKeys(event.key);
      }
    }
  }

  static checkWebShortcuts(key) {
    switch (key) {
      case 'e':
        return this.eraserButton();
      case 'r':
        return this.rectangleButton();
      case 'p':
        return this.pencilButton();
      case 'f':
        return this.fillButton();
    }

  }

  static checkControlKeys(code) {
    switch (code) {
      case 'KeyZ':
        this.undoButton();
        return true;
      case 'KeyY':
        this.redoButton();
        return true;
      case 'KeyS':
        this.saveButton();
        return true;
      case 'KeyR':
        this.reloadButton();
        return true;
      case 'KeyG':
        this.showGridButton();
        return true;
    }
  }

  static onKeyUp(event) {
    if (!Utils.isNwjs()) {
      if (Input.isPressed('shift') || Input.isPressed('control')) {
        if (this.checkControlKeys(event.code)) {
          event.preventDefault();
        }
        return;
      }

      this.checkWebShortcuts(event.key);
      this.checkLayerKeys(event.key);
    }

    if (event.key === 'h') {
      this.toggleMapEditor();
      return;
    }

    this.checkNumKeys(event.code);
  }

  static toggleMapEditor() {
    if (this.resizeTimeout) {
      return;
    }

    const scene = SceneManager._scene;
    if (!this.isMapEditorScene()) {
      return;
    }

    scene.toggleMapEditor();
  }

  static refreshMapEditor() {
    const scene = SceneManager._scene;
    if (!this.isMapEditorScene()) {
      return;
    }

    scene.refreshMapEditorWindows();
  }

  static getTileIdTilesetIndex(tileId) {
    if (tileId !== 0) {
      if (!Tilemap.isVisibleTile(tileId)) {
        return -1;
      }
    }

    return getTilesetIndex(tileId);
  }

  static getTilesetName(tileId) {
    const tileset = $gameMap.tileset();
    if (!tileset) {
      return;
    }

    const tilesetIndex = this.getTileIdTilesetIndex(tileId);
    if (tilesetIndex < 0) {
      return;
    }

    if (tilesetIndex < tileset.tilesetNames.length) {
      return tileset.tilesetNames[tilesetIndex];
    }

    if (!window.CycloneExtraTilesets) {
      return;
    }

    const extraTileset = $gameMap.extraTileset();
    if (!extraTileset) {
      return;
    }

    const extraIndex = tilesetIndex - 9;
    const newIndex = extraIndex + 5;

    return extraTileset.tilesetNames[newIndex];
  }

  static loadTilesetBitmap(tileId) {
    const realFileName = this.getTilesetName(tileId);
    if (realFileName) {
      return ImageManager.loadTileset(realFileName);
    }
  }

  static deselectShadowOrRegion(newLayerIndex) {
    // coming from or to shadows/regions, then de-select the current index
    if (currentLayer === 4 || currentLayer === 5 || newLayerIndex === 4 || newLayerIndex === 5) {
      this.clearSelection();
    }
  }

  static changeCurrentLayer(newIndex) {
    if (newIndex >= layerVisibility.length) {
      return;
    }

    if (newIndex !== 1 && puzzleMode) {
      puzzleMode = false;
      this.updateCurrentTool();
    }

    this.deselectShadowOrRegion(newIndex);

    currentLayer = newIndex;
    if (Utils.isNwjs()) {
      this.layer1Button.checked = newIndex === 0;
      this.layer2Button.checked = newIndex === 1;
      this.layer3Button.checked = newIndex === 2;
      this.layer4Button.checked = newIndex === 3;
      this.shadowsButton.checked = newIndex === 4;
      this.regionsButton.checked = newIndex === 5;
      this.eventsButton.checked = newIndex === 6;
      this.autoLayerButton.checked = newIndex === 7;
      this.collisionsButton.checked = newIndex === 8;
      this.tagsButton.checked = newIndex === 9;
      this.blendButton.checked = newIndex === 10;
    }

    if (this.isMapEditorScene()) {
      SceneManager._scene._mapEditorLayerListWindow.refresh();
      SceneManager._scene._mapEditorWindow.refresh();
      SceneManager._scene._mapEditorStatus.refresh();
      SceneManager._scene._mapEditorGrid.refresh();
      SceneManager._scene._spriteset._mapEditorCursor.updateDrawing();
    }
  }

  static changeCurrentTab(tabLetter) {
    currentTab = tabLetter;
    this.refreshMapEditor();
  }

  static tileIndex(x, y, z) {
    return (z * $gameMap.height() + (y % $gameMap.height())) * $gameMap.width() + (x % $gameMap.width());
  }

  static indexPositionX(index, z) {
    const y = this.indexPositionY(index, z);
    return index - this.tileIndex(0, y, z);
  }

  static indexPositionY(index, z) {
    return Math.floor((index / $gameMap.width()) - (z * $gameMap.height()));
  }

  static getCurrentTileAtPosition(x, y, z, skipPreview = true) {
    if (x < 0 || y < 0 || x >= $gameMap.width() || y >= $gameMap.height()) {
      return 0;
    }

    const tileIndex = this.tileIndex(x, y, z);
    if (!skipPreview) {
      if (previewChanges[tileIndex] !== undefined) {
        return previewChanges[tileIndex];
      }
    }

    return $dataMap.data[tileIndex] ?? 0;
  }

  static isSameKindTile(tileId, x, y, z, skipPreview = true) {
    return Tilemap.isSameKindTile(tileId, this.getCurrentTileAtPosition(x, y, z, skipPreview));
  }

  static getWallColumnTypeForPosition(x, y, z, tileId, skipPreview = true) {
    // wall auto tiles need the left and right columns to have the same amount of rows for it to match
    let hasLeftColumn = true;
    let hasRightColumn = true;

    const compareWallAutoTileLine = (newY, sameCenter) => {
      const leftTileId = this.getCurrentTileAtPosition(x -1, newY, z, skipPreview);
      const rightTileId = this.getCurrentTileAtPosition(x + 1, newY, z, skipPreview);

      if (sameCenter) {
        if (!Tilemap.isSameKindTile(tileId, leftTileId)) {
          hasLeftColumn = false;
        }
        if (!Tilemap.isSameKindTile(tileId, rightTileId)) {
          hasRightColumn = false;
        }
      } else {
        if (Tilemap.isSameKindTile(tileId, leftTileId)) {
          hasLeftColumn = false;
        }
        if (Tilemap.isSameKindTile(tileId, rightTileId)) {
          hasRightColumn = false;
        }
      }
    };

    for (let newY = y; y < $gameMap.height(); y++) {
      const centerTileId = this.getCurrentTileAtPosition(x, newY, z, skipPreview);
      const sameCenter = Tilemap.isSameKindTile(tileId, centerTileId);
      compareWallAutoTileLine(newY, sameCenter);
      if (!sameCenter) {
        break;
      }
    }

    for (let newY = y -1; y >= 0; y--) {
      const centerTileId = this.getCurrentTileAtPosition(x, newY, z, skipPreview);
      const sameCenter = Tilemap.isSameKindTile(tileId, centerTileId);
      compareWallAutoTileLine(newY, sameCenter);
      if (!sameCenter) {
        break;
      }
    }

    if (hasLeftColumn) {
      if (hasRightColumn) {
        return 1;
      }

      return 2;
    }

    if (hasRightColumn) {
      return 0;
    }

    return 3;
  }

  static getWaterfallShapeForPosition(x, y, z, tileId, skipPreview = true) {
    const left = this.isSameKindTile(tileId, x - 1, y, z, skipPreview);
    const right = this.isSameKindTile(tileId, x + 1, y, z, skipPreview);

    if (left && right) {
      return 0;
    }

    if (left) {
      return 1;
    }

    if (right) {
      return 2;
    }

    return 3;
  }

  static getWallShapeForPosition(x, y, z, tileId, skipPreview = true) {
    const columnType = this.getWallColumnTypeForPosition(x, y, z, tileId, skipPreview);

    let shape = 0;
    const above = this.isSameKindTile(tileId, x, y -1, z, skipPreview);
    const below = this.isSameKindTile(tileId, x, y +1, z, skipPreview);

    if (above && below) {
      shape = 0;
    } else if (above) {
      shape = 8;
    } else if (below) {
      shape = 2;
    } else {
      shape = 10;
    }

    switch (columnType) {
      case 0:
        shape += 1;
        break;
      case 2:
        shape += 4;
        break;
      case 3:
        shape += 5;
        break;
    }

    return shape;
  }

  static getShapeForConfiguration(configuration) {
    for (let shape = 0; shape < autoTileShapeTable.length; shape++) {
      const shapeData = autoTileShapeTable[shape];
      let valid = true;

      for (let i = 0; i < configuration.length; i++) {
        const config = shapeData[i];

        if (config === true) {
          if (!configuration[i]) {
            valid = false;
            break;
          }
        } else if (config === false) {
          if (configuration[i]) {
            valid = false;
            break;
          }
        }
      }

      if (valid) {
        return shape;
      }
    }

    return 46;
  }


  static isAutotileMatch(tileId, x, y, z, skipPreview = true) {
    if (!$gameMap.isValid(x, y)) {
      return true;
    }

    const otherTileId = this.getCurrentTileAtPosition(x, y, z, skipPreview);

    if (Tilemap.isSameKindTile(tileId, otherTileId)) {
      return true;
    }

    const specialTiles = [5, 7, 13];
    const leftKind = Tilemap.getAutotileKind(tileId);
    const rightKind = Tilemap.getAutotileKind(otherTileId);

    const leftSpecial = specialTiles.includes(leftKind);
    const rightSpecial = specialTiles.includes(rightKind);
    if (leftSpecial !== rightSpecial) {
      return true;
    }

    return false;
  }

  static getAutoTileShapeForPosition(x, y, z, tileId, skipPreview = true) {
    if (Tilemap.isWallSideTile(tileId) || Tilemap.isRoofTile(tileId)) {
      return this.getWallShapeForPosition(x, y, z, tileId, skipPreview);
    }

    if (Tilemap.isWaterfallTile(tileId)) {
      return this.getWaterfallShapeForPosition(x, y, z, tileId, skipPreview);
    }

    const a = this.isAutotileMatch(tileId, x -1, y -1, z, skipPreview);
    const b = this.isAutotileMatch(tileId, x, y -1, z, skipPreview);
    const c = this.isAutotileMatch(tileId, x +1, y -1, z, skipPreview);

    const d = this.isAutotileMatch(tileId, x -1, y, z, skipPreview);
    const e = this.isAutotileMatch(tileId, x +1, y, z, skipPreview);

    const f = this.isAutotileMatch(tileId, x -1, y +1, z, skipPreview);
    const g = this.isAutotileMatch(tileId, x, y +1, z, skipPreview);
    const h = this.isAutotileMatch(tileId, x +1, y +1, z, skipPreview);

    const config = [a, b, c, d, e, f, g, h];
    return this.getShapeForConfiguration(config);
  }

  static isShiftMapping() {
    if (Input.isPressed('shift')) {
      return true;
    }

    if (SceneManager._scene._mapEditorWindow._manualTileSelected !== undefined) {
      return true;
    }

    return false;
  }

  static changeAutoTileShapeForPosition(x, y, z, tileId, skipPreview = true) {
    if (z >= 4 || this.isShiftMapping()) {
      return tileId;
    }

    const shape = this.getAutoTileShapeForPosition(x, y, z, tileId, skipPreview);
    return Tilemap.TILE_ID_A1 + Math.floor((tileId - Tilemap.TILE_ID_A1) / 48) * 48 + shape;
  }

  static resetTileShape(x, y, z, previewOnly = false) {
    if (x < 0 || x >= $gameMap.width()) {
      return;
    }

    if (y < 0 || y >= $gameMap.height()) {
      return;
    }

    const tileId = this.getCurrentTileAtPosition(x, y, z, !previewOnly);
    if (Tilemap.isAutotile(tileId)) {
      const effectiveTileId = this.changeAutoTileShapeForPosition(x, y, z, tileId, !previewOnly);
      if (tileId !== effectiveTileId) {
        this.setMapTile(x, y, z, effectiveTileId, false, previewOnly);
      }
    }
  }

  static resetCurrentChange() {
    currentChange = {
      tiles: {},
      collision: {},
      blend: {},
      puzzle: {},
    };
  }

  static undoLastChange() {
    if (this.changingTileProps) {
      return;
    }

    if (changeHistory.length === 0) {
      SoundManager.playBuzzer();
      return;
    }

    const lastChange = changeHistory.pop();
    this.resetCurrentChange();

    const size = $gameMap.tileWidth() * $gameMap.tileHeight();

    for (const tileIndex in lastChange.tiles) {
      currentChange.tiles[tileIndex] = $dataMap.data[tileIndex];
      $dataMap.data[tileIndex] = lastChange.tiles[tileIndex];
    }

    for (const tileIndex in lastChange.puzzle) {
      currentChange.puzzle[tileIndex] = CycloneMagic.puzzleTiles[tileIndex];
      CycloneMagic.puzzleTiles[tileIndex] = lastChange.puzzle[tileIndex];
    }

    for (const tileIndex in lastChange.collision) {
      currentChange.collision[tileIndex] = customCollisionTable[tileIndex];
      customCollisionTable[tileIndex] = lastChange.collision[tileIndex];
    }

    for (const tileIndex in lastChange.blend) {
      if (!tileBlendingTable[tileIndex]) {
        const buffer = new ArrayBuffer(size);
        tileBlendingTable[tileIndex] = new Int8Array(buffer);
      }

      const tilePixels = tileBlendingTable[tileIndex];
      currentChange.blend[tileIndex] = {};
      for (const pixelIndex in lastChange.blend[tileIndex]) {
        currentChange.blend[tileIndex][pixelIndex] = tilePixels[pixelIndex];
        tilePixels[pixelIndex] = lastChange.blend[tileIndex][pixelIndex];
      }
    }

    undoHistory.push(currentChange);
    currentChange = false;
    SceneManager._scene._mapEditorCommands.redraw();
    SceneManager._scene._mapEditorGrid.refresh();

    mapCaches[$gameMap._mapId] = $dataMap;
    this.refreshTilemap();
    saveExtraData(true);
  }

  static redoLastUndoneChange() {
    if (this.changingTileProps) {
      return;
    }

    if (undoHistory.length === 0) {
      SoundManager.playBuzzer();
      return;
    }

    const lastChange = undoHistory.pop();
    const size = $gameMap.tileWidth() * $gameMap.tileHeight();
    this.resetCurrentChange();
    let needsSaving = false;

    for (const tileIndex in lastChange.tiles) {
      currentChange.tiles[tileIndex] = $dataMap.data[tileIndex];
      $dataMap.data[tileIndex] = lastChange.tiles[tileIndex];
    }

    for (const tileIndex in lastChange.puzzle) {
      currentChange.puzzle[tileIndex] = CycloneMagic.puzzleTiles[tileIndex];
      CycloneMagic.puzzleTiles[tileIndex] = lastChange.puzzle[tileIndex];
    }

    for (const tileIndex in lastChange.collision) {
      currentChange.collision[tileIndex] = customCollisionTable[tileIndex];
      customCollisionTable[tileIndex] = lastChange.collision[tileIndex];
      needsSaving = true;
    }

    for (const tileIndex in lastChange.blend) {
      if (!tileBlendingTable[tileIndex]) {
        const buffer = new ArrayBuffer(size);
        tileBlendingTable[tileIndex] = new Int8Array(buffer);
      }

      const tilePixels = tileBlendingTable[tileIndex];
      currentChange.blend[tileIndex] = {};
      for (const pixelIndex in lastChange.blend[tileIndex]) {
        currentChange.blend[tileIndex][pixelIndex] = tilePixels[pixelIndex];
        tilePixels[pixelIndex] = lastChange.blend[tileIndex][pixelIndex];
        needsSaving = true;
      }
    }

    this.logChange(false);
    if (needsSaving) {
      saveExtraData(true);
    }
    this.refreshTilemap();
  }

  static getCurrentLayerChangeType() {
    switch (currentLayer) {
      case Layers.collisions:
        return 'collision';
      case Layers.blend:
        return 'blend';
      default:
        return 'tile';
    }
  }

  static logChange(clearUndo = true) {
    if (!currentChange) {
      return;
    }
    const hasTiles = Object.keys(currentChange.tiles).length > 0;
    const hasBlend = Object.keys(currentChange.blend).length > 0;
    const hasCollision = Object.keys(currentChange.collision).length > 0;
    const hasPuzzle = Object.keys(currentChange.puzzle).length > 0;
    const hasChanges = hasTiles || hasBlend || hasCollision || hasPuzzle;

    if (hasChanges) {
      changeHistory.push(currentChange);
      if (clearUndo) {
        undoHistory = [];
      }
    }
    currentChange = false;

    while (changeHistory.length > 300) {
      changeHistory.shift();
    }

    SceneManager._scene._mapEditorCommands.redraw();
    SceneManager._scene._mapEditorGrid.refresh();

    mapCaches[$gameMap._mapId] = $dataMap;

    fullRefresh();
  }

  static maybeUpdateTileNeighbors(x, y, z, expectedUpdate = true, previewOnly = false) {
    if (this.isShiftMapping()) {
      return;
    }

    if (!expectedUpdate) {
      return;
    }

    this.resetTileShape(x -1, y -1, z, previewOnly);
    this.resetTileShape(x, y -1, z, previewOnly);
    this.resetTileShape(x +1, y -1, z, previewOnly);

    this.resetTileShape(x -1, y, z, previewOnly);
    this.resetTileShape(x +1, y, z, previewOnly);

    this.resetTileShape(x -1, y +1, z, previewOnly);
    this.resetTileShape(x, y + 1, z, previewOnly);
    this.resetTileShape(x +1, y +1, z, previewOnly);
  }

  static getDefaultLayerForTileId(tileId) {
    if (!Tilemap.isAutotile(tileId)) {
      return 3;
    }

    if (tileId >= Tilemap.TILE_ID_A3) {
      return 0;
    }

    const kind = Tilemap.getAutotileKind(tileId);
    if (highLayerAutotiles.includes(kind)) {
      return 1;
    }

    return 0;
  }

  static getItemsToChange(x, y, z, tileId, skipPreview = true, updateHigherLayers = true) {
    if (z !== 7) {
      return [{
        x,
        y,
        z,
        tileId,
      }];
    }

    // When using automatic mode, we may need to change more than one layer at the same time
    const items = [];
    let layerId = this.getDefaultLayerForTileId(tileId);

    if (layerId === 1 && Tilemap.isTileA1(tileId)) {
      items.push({
        x,
        y,
        z: 0,
        tileId: Tilemap.TILE_ID_A1,
      });
    }

    if (layerId === 3) {
      // If there's already something on the fourth layer, then move it to the third and place the new tile on the 4th
      const currentTile = this.getCurrentTileAtPosition(x, y, 3, skipPreview);
      if (currentTile === tileId && tileId !== 0) {
        return [];
      }

      if (currentTile) {
        items.push({
          x,
          y,
          z: 2,
          tileId: currentTile,
        });
      }
    }

    items.push({
      x,
      y,
      z: layerId,
      tileId,
    });

    // Remove anything above the new tile
    if (updateHigherLayers) {
      for (let i = layerId + 1; i <= 3; i++) {
        items.push({
          x,
          y,
          z: i,
          tileId: 0,
        });
      }
    }

    return items;
  }

  static canEraseLayer(layerIndex) {
    if (currentTool === 'eraser') {
      return true;
    }

    if (layerIndex >= 2) {
      return true;
    }

    if (multiLayerSelection.length) {
      return true;
    }

    if (currentLayer !== Layers.auto) {
      return true;
    }

    // The lower layers can only be erased with the pen in auto mode when there are multiple layers selected
    return false;
  }

  static _eraseSingleLayerTile(x, y, z, updateNeighbors = true, previewOnly = false, forceErasure = false) {
    if (!forceErasure && !this.canEraseLayer(z)) {
      return;
    }

    const tileIndex = this.tileIndex(x, y, z);
    if (previewOnly) {
      previewChanges[tileIndex] = 0;
    } else {
      const oldTile = $dataMap.data[tileIndex];
      if (currentChange.tiles[tileIndex] === undefined && oldTile !== 0) {
        currentChange.tiles[tileIndex] = oldTile;
      }

      $dataMap.data[tileIndex] = 0;
    }
  }

  static _eraseSingleMapTile(x, y, z, updateNeighbors = true, previewOnly = false, forceErasure = false) {
    if (z > 3 && z !== Layers.auto) {
      this._eraseSingleLayerTile(x, y, z, updateNeighbors, previewOnly, forceErasure);
      return;
    }

    for (let newZ = 0; newZ <= 3; newZ++) {
      if (newZ !== z && z !== Layers.auto) {
        continue;
      }

      this._eraseSingleLayerTile(x, y, newZ, updateNeighbors, previewOnly, forceErasure);
      this.maybeUpdateTileNeighbors(x, y, z, updateNeighbors, previewOnly);
    }
  }

  // eslint-disable-next-line complexity
  static _getBlockCollision(i, j, count, tileId) {
    if (tileId <= 3) {
      return tileId;
    }

    let goesUp = false;
    let goesDown = false;
    let goesRight = false;
    let goesLeft = false;

    if (tileId >= 20) {
      const d = tileId - 20;
      goesUp = !DirectionHelper.goesUp(d);
      goesDown = !DirectionHelper.goesDown(d);
      goesLeft = !DirectionHelper.goesLeft(d);
      goesRight = !DirectionHelper.goesRight(d);
    } else if (tileId > 10) {
      const d = tileId - 10;
      goesUp = DirectionHelper.goesUp(d);
      goesDown = DirectionHelper.goesDown(d);
      goesLeft = DirectionHelper.goesLeft(d);
      goesRight = DirectionHelper.goesRight(d);
    } else if (tileId === 4) {
      goesUp = true;
      goesDown = true;
    } else if (tileId === 5) {
      goesLeft = true;
      goesRight = true;
    }

    const up = goesUp && j === 0;
    const down = goesDown && j === count -1;
    const left = goesLeft && i === 0;
    const right = goesRight && i === count - 1;

    if (up) {
      if (left) {
        if (right) {
          if (down) {
            return 20;
          }

          return 22;
        }

        if (down) {
          return 26;
        }

        return 17;
      }

      if (right) {
        if (down) {
          return 24;
        }

        return 19;
      }

      if (down) {
        return 4;
      }
      return 18;
    }

    if (down) {
      if (left) {
        if (right) {
          return 28;
        }

        return 11;
      }

      if (right) {
        return 13;
      }
      return 12;
    }

    if (left) {
      if (right) {
        return 5;
      }

      return 14;
    }

    if (right) {
      return 16;
    }

    return 1;
  }

  static _applySingleCollision(x, y, tileId, previewOnly = false) {
    if (previewOnly) {
      return;
    }

    const gridRatio = this.getGridRatio();
    const count = 4 / gridRatio;

    for (let i = 0; i < count; i++) {
      for (let j = 0; j < count; j++) {
        const intX = Math.floor(x * 4) + i;
        const intY = Math.floor(y * 4) + j;
        const height = $gameMap.height() * 4;
        const width = $gameMap.width() * 4;
        const index = (intY % height) * width + (intX % width);

        const blockCollision = this._getBlockCollision(i, j, count, tileId);
        const oldTile = customCollisionTable[index] || 0;
        if (currentChange.collision[index] === undefined && oldTile !== blockCollision) {
          currentChange.collision[index] = oldTile;
        }

        if (!blockCollision) {
          delete customCollisionTable[index];
          continue;
        }

        customCollisionTable[index] = blockCollision;
      }
    }
  }

  static _changePixelPositionBlend(x, y, px, py, newBlend, previewOnly = false) {
    const tileWidth = $gameMap.tileWidth();
    const tileHeight = $gameMap.tileHeight();

    const fx = Math.floor(x) + Math.floor(px / tileWidth);
    const fy = Math.floor(y) + Math.floor(py / tileHeight);
    const pixelX = px % tileWidth;
    const pixelY = py % tileHeight;

    if (fx < 0 || fx >= $gameMap.width()) {
      return;
    }
    if (fy < 0 || fy >= $gameMap.height()) {
      return;
    }

    const tileIndex = this.tileIndex(fx, fy, 0);
    const size = tileWidth * tileHeight;

    const fullTable = previewOnly ? window.CycloneMagic.tileBlendingTable : tileBlendingTable;
    if (!fullTable[tileIndex]) {
      const buffer = new ArrayBuffer(size);
      fullTable[tileIndex] = new Int8Array(buffer);
    }
    const table = fullTable[tileIndex];

    const pixelIndex = pixelY * tileWidth + pixelX;

    if (currentChange.blend[tileIndex]?.[pixelIndex] === undefined && (table[pixelIndex] ?? 0) !== newBlend) {
      if (currentChange.blend[tileIndex] === undefined) {
        currentChange.blend[tileIndex] = {};
      }

      currentChange.blend[tileIndex][pixelIndex] = (table[pixelIndex] ?? 0);
    }

    table[pixelIndex] = newBlend;
  }

  static _changePositionBlend(x, y, newBlend) {
    const fx = Math.floor(x);
    const fy = Math.floor(y);
    const tileIndex = this.tileIndex(fx, fy, 0);
    const gridRatio = this.getGridRatio();
    const tileWidth = $gameMap.tileWidth();
    const tileHeight = $gameMap.tileHeight();
    const size = tileWidth * tileHeight;

    if (!tileBlendingTable[tileIndex]) {
      const buffer = new ArrayBuffer(size);
      tileBlendingTable[tileIndex] = new Int8Array(buffer);
    }
    const table = tileBlendingTable[tileIndex];
    const subX = x - fx;
    const subY = y - fy;

    const leftPx = Math.round(subX * tileWidth);
    const topPx = Math.round(subY * tileHeight);
    const blockWidth = Math.floor(tileWidth / gridRatio);
    const blockHeight = Math.floor(tileHeight / gridRatio);

    const rightPx = leftPx + blockWidth;
    const bottomPx = topPx + blockHeight;

    for (let px = leftPx; px < rightPx; px++) {
      for (let py = topPx; py < bottomPx; py++) {
        const pixelIndex = py * tileWidth + px;

        if (currentChange.blend[tileIndex]?.[pixelIndex] === undefined && (table[pixelIndex] ?? 0) !== newBlend) {
          if (currentChange.blend[tileIndex] === undefined) {
            currentChange.blend[tileIndex] = {};
          }

          currentChange.blend[tileIndex][pixelIndex] = (table[pixelIndex] ?? 0);
        }

        table[pixelIndex] = newBlend;
      }
    }
  }

  static isPositionBlendSpriteReady(x, y) {
    if (!SceneManager._scene._spriteset?._blenderTileSprites) {
      return false;
    }

    for (const sprite of SceneManager._scene._spriteset._blenderTileSprites) {
      if (sprite._mapX === x && sprite._mapY === y) {
        return true;
      }
    }

    return false;
  }

  static forceBlenderRefresh(hardRefresh = false) {
    if (!window.CycloneMagic) {
      return;
    }

    SceneManager._scene._spriteset?.forceBlenderRefresh && SceneManager._scene._spriteset.forceBlenderRefresh(hardRefresh);
  }

  static buildSmallCircle() {
    const width = tileWidth / 4;
    const height = tileHeight / 4;
    const bitmap = new Bitmap(width, height);
    bitmap.drawCircle(width / 2, height / 2, Math.min(width, height) / 2, '#0000FF');
    bitmap.drawCircle(width / 2, height / 2, Math.min(width, height) / 2 - 2, '#00FF00');

    const imageData = bitmap.context.getImageData(0, 0, width, height);
    smallCircleData = imageData.data;
  }

  static buildCircle() {
    const width = tileWidth / 2;
    const height = tileHeight / 2;
    const bitmap = new Bitmap(width, height);

    bitmap.drawCircle(width / 2, height / 2, Math.min(width, height) / 2, '#0000FF');
    bitmap.drawCircle(width / 2, height / 2, Math.min(width, height) / 2 - 4, '#00FF00');

    const imageData = bitmap.context.getImageData(0, 0, width, height);
    circleData = imageData.data;
  }

  static getCircleData() {
    if (circleData) {
      if (Input.isPressed('shift')) {
        return smallCircleData;
      }

      return circleData;
    }

    this.buildSmallCircle();
    this.buildCircle();

    return this.getCircleData();
  }

  static optimizeBlends() {
    this.resetCurrentChange();

    for (let x = 0; x < $gameMap.width(); x++) {
      for (let y = 0; y < $gameMap.height(); y++) {
        this.optimizeTileBlend(x, y);
      }
    }

    this.logChange(true);
  }

  static removeAllBlends() {
    this.resetCurrentChange();
    for (let x = 0; x < $gameMap.width(); x++) {
      for (let y = 0; y < $gameMap.height(); y++) {
        this.removeTileBlend(x, y, false);
      }
    }

    this.logChange(true);
  }

  static optimizeTileBlend(x, y) {
    const fx = Math.floor(x);
    const fy = Math.floor(y);

    if (fx < 0 || fx >= $gameMap.width()) {
      return;
    }
    if (fy < 0 || fy >= $gameMap.height()) {
      return;
    }

    const tileIndex = this.tileIndex(fx, fy, 0);
    if (!tileBlendingTable[tileIndex]) {
      return;
    }

    const hasZero = tileBlendingTable[tileIndex].includes(0);
    const hasOne = tileBlendingTable[tileIndex].includes(1);

    if (hasZero === hasOne) {
      return;
    }

    // If it's all blended, then remove whatever tile is on layer 2
    if (hasOne) {
      this._applySingleMapTile(x, y, 1, 0, false, false, true);
    }

    currentChange.blend[tileIndex] = tileBlendingTable[tileIndex];
    delete tileBlendingTable[tileIndex];
  }

  static removeTileBlend(x, y, previewOnly = false) {
    if (previewOnly && !window.CycloneMagic) {
      return;
    }

    const fx = Math.floor(x);
    const fy = Math.floor(y);

    if (fx < 0 || fx >= $gameMap.width()) {
      return;
    }
    if (fy < 0 || fy >= $gameMap.height()) {
      return;
    }

    const tileIndex = this.tileIndex(fx, fy, 0);
    if (previewOnly) {
      if (window.CycloneMagic.tileBlendingTable[tileIndex]) {
        delete window.CycloneMagic.tileBlendingTable[tileIndex];
      }
      return;
    }

    if (tileBlendingTable[tileIndex]) {
      currentChange.blend[tileIndex] = tileBlendingTable[tileIndex];
      delete tileBlendingTable[tileIndex];
    }
  }

  static _applyBlendBrush(x, y, previewOnly = false) {
    if (previewOnly && !window.CycloneMagic) {
      return;
    }

    const tileWidth = $gameMap.tileWidth();
    const tileHeight = $gameMap.tileHeight();

    const divider = Input.isPressed('shift') ? 4 : 2;
    const width = tileWidth / divider;
    const height = tileHeight / divider;
    const pixels = this.getCircleData();

    let index = -1;

    const tileX = Math.floor(x);
    const tileY = Math.floor(y);
    const pixelX = Math.floor((x - tileX) * tileWidth);
    const pixelY = Math.floor((y - tileY) * tileHeight);
    const newBlend = currentTool === Tools.eraser ? 0 : 1;

    for (let py = 0; py < height; py++) {
      for (let px = 0; px < width; px++) {
        index++;

        if (pixels[index * 4 + 1] > 0) {
          this._changePixelPositionBlend(x, y, pixelX + px, pixelY + py, newBlend, previewOnly);
        } else if (pixels[index * 4 + 2] > 0) {
          if (Math.randomInt(10) > 5) {
            this._changePixelPositionBlend(x, y, pixelX + px, pixelY + py, newBlend, previewOnly);
          }
        }
      }
    }

    if (previewOnly) {
      forceBlenderRefresh();
    } else {
      // Let's do a quick refresh first and then save the data a little later
      if (window.CycloneMagic) {
        window.CycloneMagic.tileBlendingTable = tileBlendingTable;
        const maxTileX = tileX + Math.floor((pixelX + width) / tileWidth);
        const maxTileY = tileY + Math.floor((pixelY + height) / tileHeight);

        if (window.CycloneMagic) {
          for (let cacheX = tileX; cacheX <= maxTileX; cacheX++) {
            for (let cacheY = tileY; cacheY <= maxTileY; cacheY++) {
              window.CycloneMagic.clearPositionCache(cacheX, cacheY);
            }
          }
        }

        forceBlenderRefresh();
      }
    }
  }

  static _applySingleBlend(x, y) {
    if (currentTool === Tools.eraser) {
      this._changePositionBlend(x, y, 0);
      return;
    }

    this._changePositionBlend(x, y, 1);
  }

  static _applyPuzzleTile(x, y, tileId, previewOnly) {
    if (!window.CycloneMagic?.puzzleTiles) {
      return;
    }
    if (previewOnly) {
      return;
    }

    const width = $gameMap.width() * 2;
    const index = (y * 2) * width + x * 2;

    const oldTile = CycloneMagic.puzzleTiles[index] ?? 0;
    if (currentChange.puzzle[index] === undefined && oldTile !== tileId) {
      currentChange.puzzle[index] = oldTile;
    }

    if (tileId) {
      CycloneMagic.puzzleTiles[index] = tileId;
    } else if (CycloneMagic.puzzleTiles[index]) {
      delete CycloneMagic.puzzleTiles[index];
    }
  }

  static _applySingleMapTile(x, y, z, tileId, updateNeighbors = true, previewOnly = false, forceErasure = false) {
    if (z === Layers.collisions) {
      return this._applySingleCollision(x, y, tileId, previewOnly);
    }

    if (z === 1 && puzzleMode) {
      this._applyPuzzleTile(x, y, tileId, previewOnly);
      return;
    }

    if (!tileId) {
      this._eraseSingleMapTile(x, y, z, updateNeighbors, previewOnly, forceErasure);
      return;
    }

    const itemsToChange = this.getItemsToChange(x, y, z, tileId, !previewOnly, updateNeighbors);
    for (const {x, y, z, tileId} of itemsToChange) {
      if (z > 5) {
        continue;
      }
      const tileIndex = this.tileIndex(x, y, z);
      let effectiveTileId = tileId;
      if (Tilemap.isAutotile(tileId)) {
        effectiveTileId = this.changeAutoTileShapeForPosition(x, y, z, tileId, false);
      }

      if (z === 1) {
        this.removeTileBlend(x, y, previewOnly);
      }

      if (previewOnly) {
        previewChanges[tileIndex] = effectiveTileId;
      } else {
        const oldTile = $dataMap.data[tileIndex];
        if (currentChange.tiles[tileIndex] === undefined && oldTile !== effectiveTileId) {
          currentChange.tiles[tileIndex] = oldTile;
        }

        $dataMap.data[tileIndex] = effectiveTileId;
      }

      this.maybeUpdateTileNeighbors(x, y, z, updateNeighbors, previewOnly);
    }
  }

  static setMapTile(x, y, z, tileId, updateNeighbors = true, previewOnly = false) {
    if (!$gameMap.isValid(x, y)) {
      return;
    }

    if (currentLayer === Layers.blend) {
      if (!previewOnly) {
        CycloneMapEditor$1._applySingleBlend(x, y);
      }
      return;
    }

    if (currentTool !== 'eraser') {
      if (tileId === undefined ) {
        return;
      }

      if (tileId === 0 && !this.canEraseLayer(z)) {
        return;
      }
    }

    this._applySingleMapTile(x, y, z, tileId, updateNeighbors, previewOnly);
  }

  static getSelectedTileIndex(col, row) {
    if (currentTool === 'eraser') {
      return;
    }

    if (currentTileId === undefined) {
      return;
    }

    if (selectedTileList.length < tileCols * tileRows) {
      return;
    }

    const realCol = col % tileCols;
    const realRow = row % tileRows;
    return realRow * tileCols + realCol;
  }

  static getSelectedTileCell(col, row) {
    const index = this.getSelectedTileIndex(col, row);
    if (index || index === 0) {
      return selectedTileList[index];
    }
  }

  static setSelectionTileMaybeMultiLayer(tileX, tileY, selectionCol, selectionRow, previewOnly = false, effectiveLayer = undefined) {
    effectiveLayer = effectiveLayer ?? currentLayer;
    const index = this.getSelectedTileIndex(selectionCol, selectionRow);

    if (effectiveLayer === 7 && multiLayerSelection.length) {
      for (let z = 0; z <= 3; z++) {
        const tileId = multiLayerSelection[z][index] ?? 0;
        this.setMapTile(tileX, tileY, z, tileId, true, previewOnly);
      }
    } else {
      const tileId = selectedTileList[index] ?? 0;
      this.setMapTile(tileX, tileY, effectiveLayer, tileId, true, previewOnly);

      if (effectiveLayer === 2 && currentLayer === 7 && currentTool === 'eraser') {
        this.setMapTile(tileX, tileY, 3, 0, true, previewOnly);
      }
    }
  }

  static canApplyRectangle() {
    return currentTileId !== undefined || currentTool === 'eraser' || currentLayer === Layers.blend;
  }

  static isAutoEraser() {
    return currentLayer === Layers.auto && currentTool === 'eraser' && !Input.isPressed('shift');
  }

  static getHighestLayerOnArea(startX, startY, width, height) {
    const highestLayer = (() => {
      for (let z = 3; z >= 1; z--) {
        for (let tileY = startY; tileY < startY + height; tileY++) {
          for (let tileX = startX; tileX < startX + width; tileX++) {
            const tileIndex = this.tileIndex(tileX, tileY, z);
            const tileId = $dataMap.data[tileIndex];

            if (tileId > 0) {
              return z;
            }
          }
        }
      }

      return 0;
    })();

    if (highestLayer === 3 && Input.isPressed('control')) {
      return 2;
    }

    return highestLayer;
  }

  static applyRectangle(startX, startY, width, height, previewOnly = false) {
    if (!this.canApplyRectangle()) {
      return;
    }

    this.ensureLayerVisibility();
    const gridRatio = this.getGridRatio();
    let initialRow = 0;
    let initialCol = 0;
    let rowIncrement = 1;
    let colIncrement = 1;

    if (rectangleBackWidth > 0) {
      initialCol = (width * gridRatio) - 1;
      colIncrement *= -1;
    }

    if (rectangleBackHeight > 0) {
      initialRow = (height * gridRatio) - 1;
      rowIncrement *= -1;
    }

    let selectionRow = initialRow;
    let selectionCol = initialCol;

    if (previewOnly) {
      previewChanges = {};
    } else {
      this.resetCurrentChange();
    }

    let effectiveLayer = currentLayer;
    if (this.isAutoEraser()) {
      effectiveLayer = this.getHighestLayerOnArea(startX, startY, width, height);
    }

    const tileIncrement = 1 / gridRatio;

    for (let tileY = startY; tileY < startY + height; tileY += tileIncrement) {
      selectionCol = initialCol;
      for (let tileX = startX; tileX < startX + width; tileX += tileIncrement) {
        this.setSelectionTileMaybeMultiLayer(tileX, tileY, selectionCol, selectionRow, previewOnly, effectiveLayer);
        selectionCol += colIncrement;
      }
      selectionRow += rowIncrement;
    }

    if (previewOnly) {
      SceneManager._scene._spriteset._tilemap.refresh();
      this.maybeRefreshGrid();
    } else {
      this.logChange();
      this.refreshTilemap();
    }
  }

  static maybeRefreshGrid() {
    if (currentLayer !== Layers.regions) {
      return;
    }

    refreshGrid();
  }

  static refreshTilemap() {
    previewChanges = {};
    if (currentLayer === Layers.collisions || currentLayer === Layers.blend) {
      saveExtraData(true);
    }

    if (TouchInput.isLongPressed()) {
      refreshTilemap();
    } else {
      SceneManager._scene._spriteset._tilemap.refresh();
    }
    refreshGrid();
  }

  static copyAutoRectangle(startX, startY, width, height) {
    for (let z = 0; z <= 3; z++) {
      multiLayerSelection[z] = Array(width * height);
    }

    this.iterateRectangle(startX, startY, width, height, (tileX, tileY, index) => {
      for (let z = 0; z <= 3; z++) {
        const tileIndex = this.tileIndex(tileX, tileY, z);
        multiLayerSelection[z][index] = $dataMap.data[tileIndex] || 0;
        selectedTileList[index] = $dataMap.data[tileIndex] || selectedTileList[index] || 0;
        if (currentTileId === undefined) {
          currentTileId = selectedTileList[index];
        }
      }
    });
  }

  static _selectTileIfNoneSelectedYet(tileId) {
    if (currentTileId === undefined) {
      currentTileId = tileId;
    }
  }

  static _shouldSkipRemainingLayersCopy(foundAny, z) {
    if (!foundAny) {
      return false;
    }

    if (Input.isPressed('control')) {
      return z !== 3;
    }

    return true;
  }

  static iterateRectangle(startX, startY, width, height, fn) {
    let index = 0;
    for (let tileY = startY; tileY < startY + height; tileY++) {
      for (let tileX = startX; tileX < startX + width; tileX++) {
        fn(tileX, tileY, index);
        index++;
      }
    }
  }

  static copyHigherAutoRectangle(startX, startY, width, height) {
    for (let z = 0; z <= 3; z++) {
      multiLayerSelection[z] = Array(width * height);
    }

    let foundAny = false;
    for (let z = 3; z >= 0; z--) {
      if (!this.isLayerVisible(z)) {
        continue;
      }

      this.iterateRectangle(startX, startY, width, height, (tileX, tileY, index) => {
        const tileIndex = this.tileIndex(tileX, tileY, z);
        multiLayerSelection[z][index] = $dataMap.data[tileIndex] || 0;
        selectedTileList[index] = $dataMap.data[tileIndex] || selectedTileList[index] || 0;
        this._selectTileIfNoneSelectedYet(selectedTileList[index]);

        if ($dataMap.data[tileIndex]) {
          foundAny = true;
        }
      });

      if (this._shouldSkipRemainingLayersCopy(foundAny, z)) {
        return;
      }
    }
  }

  static copyHigherRectangle(startX, startY, width, height) {
    let foundAny = false;

    for (let z = 3; z >= 0; z--) {
      if (!this.isLayerVisible(z)) {
        continue;
      }

      this.iterateRectangle(startX, startY, width, height, (tileX, tileY, index) => {
        const tileIndex = this.tileIndex(tileX, tileY, z);
        selectedTileList[index] = selectedTileList[index] || $dataMap.data[tileIndex] || 0;
        this._selectTileIfNoneSelectedYet(selectedTileList[index]);
        if ($dataMap.data[tileIndex]) {
          foundAny = true;
        }
      });

      if (this._shouldSkipRemainingLayersCopy(foundAny, z)) {
        return;
      }
    }
  }

  static copyManualRectangle(startX, startY, width, height) {
    this.iterateRectangle(startX, startY, width, height, (tileX, tileY, index) => {
      const tileIndex = this.tileIndex(tileX, tileY, currentLayer);
      selectedTileList[index] = $dataMap.data[tileIndex] || 0;
      this._selectTileIfNoneSelectedYet(selectedTileList[index]);
    });
  }

  static copyRectangle(startX, startY, width, height) {
    if (puzzleMode) {
      return;
    }
    if (!wasRightButtonDown) {
      return;
    }
    if (currentLayer === Layers.collisions) {
      return;
    }

    const gridRatio = this.getGridRatio();
    multiLayerSelection = [];
    selectedTileList = Array((width * gridRatio) * (height * gridRatio));
    currentTileId = undefined;

    if (currentLayer === 7) {
      if (Input.isPressed('shift')) {
        this.copyHigherAutoRectangle(startX, startY, width, height);
      } else {
        this.copyAutoRectangle(startX, startY, width, height);
      }
    } else if (Input.isPressed('shift')) {
      this.copyHigherRectangle(startX, startY, width, height);
    } else {
      this.copyManualRectangle(startX, startY, width, height);
    }

    tileCols = width;
    tileRows = height;
    messySelection = true;

    if (currentTool == 'eraser') {
      this.restoreLastDrawingTool();
    }

    this.refreshTilemap();
    SceneManager._scene._mapEditorWindow._manualTileSelected = undefined;
    SceneManager._scene._mapEditorWindow.refresh();
    SceneManager._scene._mapEditorWindow.ensureSelectionVisible();
  }

  static restoreLastDrawingTool() {
    if (lastDrawingTool === 'rectangle') {
      this.rectangleButton();
    } else {
      this.pencilButton();
    }
  }

  static isSameKindTileCurrentLayer(layers, index) {
    const size = $gameMap.width() * $gameMap.height();

    if (currentLayer > 3) {
      for (let z = 0; z <= 3; z++) {
        const tileId = $dataMap.data[index + z * size];
        if (!Tilemap.isSameKindTile(tileId, layers[z])) {
          return false;
        }
      }

      return true;
    }

    const tileId = $dataMap.data[index];
    return Tilemap.isSameKindTile(layers[currentLayer], tileId);
  }

  static _maybeValidateTileIndexForCollectionList(list, index, area, initialTileIds) {
    if (area[index] !== undefined) {
      return;
    }

    const height = $gameMap.height();
    const width = $gameMap.width();

    area[index] = this.isSameKindTileCurrentLayer(initialTileIds, index);

    if (!area[index]) {
      return;
    }

    const workLayer = currentLayer <= 3 ? currentLayer : 0;

    const y = this.indexPositionY(index, workLayer);
    const x = index - this.tileIndex(0, y, workLayer);

    const leftIndex = x > 0 ? this.tileIndex(x - 1, y, workLayer) : -1;
    const rightIndex = x < width -1 ? this.tileIndex(x + 1, y, workLayer) : -1;
    const upIndex = y > 0 ? this.tileIndex(x, y - 1, workLayer) : -1;
    const downIndex = y < height - 1 ? this.tileIndex(x, y + 1, workLayer) : -1;

    const maybeAddIndex = (index) => {
      if (index >= 0 && !list.includes(index)) {
        list.push(index);
      }
    };

    maybeAddIndex(leftIndex);
    maybeAddIndex(rightIndex);
    maybeAddIndex(upIndex);
    maybeAddIndex(downIndex);
  }

  static collectFillAreaFrom(mapX, mapY) {
    const list = [];
    const initialTileIds = [];

    const area = {};

    if (currentLayer === Layers.auto || currentLayer < 4) {
      for (let z = 0; z <= 3; z++) {
        const tileIndex = this.tileIndex(mapX, mapY, z);

        initialTileIds[z] = $dataMap.data[tileIndex];
        if (z === currentLayer || (currentLayer === 7 && z === 0)) {
          list.push(tileIndex);
        }
      }

      for (let i = 0; i < list.length; i++) {
        const index = list[i];
        this._maybeValidateTileIndexForCollectionList(list, index, area, initialTileIds);
      }
    }

    return Object.keys(area).filter(key => area[key]);
  }

  static applyFillArea(mapX, mapY) {
    if (currentTileId === undefined) {
      return;
    }

    this.ensureLayerVisibility();
    const affectedArea = this.collectFillAreaFrom(mapX, mapY);
    const height = $gameMap.height();
    const width = $gameMap.width();
    const workLayer = currentLayer <= 3 ? currentLayer : 0;

    this.resetCurrentChange();
    for (const tileIndex of affectedArea) {
      const y = this.indexPositionY(tileIndex, workLayer);
      const x = tileIndex - this.tileIndex(0, y, workLayer);

      const xDiff = (x + width - mapX) % tileCols;
      const yDiff = (y + height - mapY) % tileRows;

      this.setSelectionTileMaybeMultiLayer(x, y, xDiff, yDiff, false);
    }

    this.logChange();
    this.refreshTilemap();
  }

  static ensureLayerVisibility() {
    if (!layerVisibility[currentLayer]) {
      layerVisibility[currentLayer] = true;

      if (this.isMapEditorScene()) {
        SceneManager._scene._mapEditorLayerListWindow.refresh();
      }
    }
  }

  static applySelectedTiles(mapX, mapY) {
    if (currentLayer !== Layers.blend) {
      if (currentTileId === undefined) {
        return;
      }

      if (selectedTileList.length < tileCols * tileRows) {
        return;
      }
    }

    this.ensureLayerVisibility();
    let index = 0;
    const gridRatio = this.getGridRatio();
    const increment = 1 / gridRatio;

    for (let y = mapY; y < mapY + tileRows; y += increment) {
      for (let x = mapX; x < mapX + tileCols; x += increment) {
        if (!$gameMap.isValid(x, y)) {
          continue;
        }

        if (currentLayer === 7 && multiLayerSelection.length) {
          for (let z = 0; z <= 3; z++) {
            this.setMapTile(x, y, z, multiLayerSelection[z][index]);
          }
        } else {
          this.setMapTile(x, y, currentLayer, selectedTileList[index]);
        }

        index++;
      }
    }

    this.refreshTilemap();
  }

  static updateRightTouch(x, y) {
    if (CycloneMapEditor$1.isRightButtonDown) {
      if (!CycloneMapEditor$1.wasRightButtonDown) {
        CycloneMapEditor$1.rectangleStartX = x;
        CycloneMapEditor$1.rectangleStartY = y;
        CycloneMapEditor$1.rectangleStartMouseX = TouchInput.x;
        CycloneMapEditor$1.rectangleStartMouseY = TouchInput.y;
      }

      const gridRatio = CycloneMapEditor$1.getGridRatio();

      CycloneMapEditor$1.rectangleWidth = (x - CycloneMapEditor$1.rectangleStartX + (1 / gridRatio)).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleHeight = (y - CycloneMapEditor$1.rectangleStartY + (1 / gridRatio)).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleBackWidth = (CycloneMapEditor$1.rectangleStartX - x).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleBackHeight = (CycloneMapEditor$1.rectangleStartY - y).clamp(0, 30) * gridRatio;

      if (this.crossedHorizontalLoop()) {
        // moved right through the edge, limit the width to it
        if (CycloneMapEditor$1.rectangleStartX > x) {
          CycloneMapEditor$1.rectangleWidth = ($gameMap.width() - CycloneMapEditor$1.rectangleStartX) * gridRatio;
          CycloneMapEditor$1.rectangleBackWidth = 0;
        } else if (x > CycloneMapEditor$1.rectangleStartX) {
          CycloneMapEditor$1.rectangleBackWidth = CycloneMapEditor$1.rectangleStartX * gridRatio;
          CycloneMapEditor$1.rectangleWidth = 0;
        }
      }

      if (this.crossedVerticalLoop()) {
        if (CycloneMapEditor$1.rectangleStartY > y) {
          CycloneMapEditor$1.rectangleHeight = ($gameMap.height() - CycloneMapEditor$1.rectangleStartY) * gridRatio;
          CycloneMapEditor$1.rectangleBackHeight = 0;
        } else if (y > CycloneMapEditor$1.rectangleStartY) {
          CycloneMapEditor$1.rectangleBackHeight = CycloneMapEditor$1.rectangleStartY * gridRatio;
          CycloneMapEditor$1.rectangleHeight = 0;
        }
      }
      SceneManager._scene._spriteset._mapEditorCursor.updateDrawing();
      return;
    }

    if (CycloneMapEditor$1.wasRightButtonDown) {
      this.updateRectangleReleased();
      return;
    }
  }

  static updateCurrentToolTouch(x, y) {
    switch(CycloneMapEditor$1.currentTool) {
      case 'fill':
        this.updateFill(x, y);
        break;
      case 'pencil':
        this.updatePencil(x, y);
        break;
      case 'rectangle':
        this.updateRectangle(x, y);
        break;
      case 'eraser':
        this.updateEraser(x, y);
        break;
    }
  }

  static changeRectangleArea(previewOnly = false) {
    let startX = CycloneMapEditor$1.rectangleStartX;
    let startY = CycloneMapEditor$1.rectangleStartY;
    let applyWidth = 0;
    let applyHeight = 0;
    const gridRatio = CycloneMapEditor$1.getGridRatio();

    if (CycloneMapEditor$1.rectangleWidth > 0) {
      applyWidth = CycloneMapEditor$1.rectangleWidth / gridRatio;
    } else if (CycloneMapEditor$1.rectangleBackWidth > 0) {
      startX -= CycloneMapEditor$1.rectangleBackWidth / gridRatio;
      applyWidth = (CycloneMapEditor$1.rectangleBackWidth + 1) / gridRatio;
    }

    if (CycloneMapEditor$1.rectangleHeight > 0) {
      applyHeight = CycloneMapEditor$1.rectangleHeight / gridRatio;
    } else if (CycloneMapEditor$1.rectangleBackHeight > 0) {
      startY -= CycloneMapEditor$1.rectangleBackHeight / gridRatio;
      applyHeight = (CycloneMapEditor$1.rectangleBackHeight + 1) / gridRatio;
    }

    if (applyWidth > 0 && applyHeight > 0) {
      if (CycloneMapEditor$1.wasRightButtonDown) {
        if (!previewOnly) {
          CycloneMapEditor$1.copyRectangle(startX, startY, applyWidth, applyHeight);
        }
      } else {
        CycloneMapEditor$1.applyRectangle(startX, startY, applyWidth, applyHeight, previewOnly);
      }
    }
  }

  static updateRectangleReleased() {
    this.changeRectangleArea();

    CycloneMapEditor$1.rectangleWidth = 0;
    CycloneMapEditor$1.rectangleHeight = 0;
    CycloneMapEditor$1.rectangleBackWidth = 0;
    CycloneMapEditor$1.rectangleBackHeight = 0;
    SceneManager._scene._spriteset._mapEditorCursor.updateDrawing();
  }

  static crossedHorizontalLoop() {
    if (!$gameMap.isLoopHorizontal()) {
      return false;
    }

    // if moved left but the end position is to the right
    if ((CycloneMapEditor$1.rectangleStartMouseX > TouchInput.x && CycloneMapEditor$1.rectangleWidth > 0) ) {
      return true;
    }

    if ((CycloneMapEditor$1.rectangleStartMouseX < TouchInput.x && CycloneMapEditor$1.rectangleBackWidth > 0)) {
      return true;
    }

    return false;
  }

  static crossedVerticalLoop() {
    if (!$gameMap.isLoopVertical()) {
      return false;
    }

    if ((CycloneMapEditor$1.rectangleStartMouseY > TouchInput.y && CycloneMapEditor$1.rectangleHeight > 0)) {
      return true;
    }

    if ((CycloneMapEditor$1.rectangleStartMouseY < TouchInput.y && CycloneMapEditor$1.rectangleBackHeight > 0)) {
      return true;
    }

    return false;
  }

  static updateRectangle(x, y) {
    if (TouchInput.isPressed()) {
      if (!wasPressing) {
        CycloneMapEditor$1.rectangleStartX = x;
        CycloneMapEditor$1.rectangleStartY = y;
        CycloneMapEditor$1.rectangleStartMouseX = TouchInput.x;
        CycloneMapEditor$1.rectangleStartMouseY = TouchInput.y;
      }

      const gridRatio = CycloneMapEditor$1.getGridRatio();
      CycloneMapEditor$1.rectangleWidth = (x - CycloneMapEditor$1.rectangleStartX + (1 / gridRatio)).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleHeight = (y - CycloneMapEditor$1.rectangleStartY + (1 / gridRatio)).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleBackWidth = (CycloneMapEditor$1.rectangleStartX - x).clamp(0, 30) * gridRatio;
      CycloneMapEditor$1.rectangleBackHeight = (CycloneMapEditor$1.rectangleStartY - y).clamp(0, 30) * gridRatio;

      if (this.crossedHorizontalLoop()) {
        // moved right through the edge, limit the width to it
        if (CycloneMapEditor$1.rectangleStartX > x) {
          CycloneMapEditor$1.rectangleWidth = ($gameMap.width() - CycloneMapEditor$1.rectangleStartX) * gridRatio;
          CycloneMapEditor$1.rectangleBackWidth = 0;
        } else if (x > CycloneMapEditor$1.rectangleStartX) {
          CycloneMapEditor$1.rectangleBackWidth = CycloneMapEditor$1.rectangleStartX * gridRatio;
          CycloneMapEditor$1.rectangleWidth = 0;
        }
      }

      if (this.crossedVerticalLoop()) {
        if (CycloneMapEditor$1.rectangleStartY > y) {
          CycloneMapEditor$1.rectangleHeight = ($gameMap.height() - CycloneMapEditor$1.rectangleStartY) * gridRatio;
          CycloneMapEditor$1.rectangleBackHeight = 0;
        } else if (y > CycloneMapEditor$1.rectangleStartY) {
          CycloneMapEditor$1.rectangleBackHeight = CycloneMapEditor$1.rectangleStartY * gridRatio;
          CycloneMapEditor$1.rectangleHeight = 0;
        }
      }

      this.changeRectangleArea(true);
      SceneManager._scene._spriteset._mapEditorCursor.updateDrawing();
      return;
    }

    if (wasPressing) {
      this.updateRectangleReleased();
      return;
    }
  }

  static updateFill(x, y) {
    if (!TouchInput.isPressed() || wasPressing) {
      return;
    }

    CycloneMapEditor$1.applyFillArea(x, y);
  }

  static updateEraser(x, y) {
    if (this.isLayerVisible(Layers.blend)) {
      this.updatePencil(x, y);
      return;
    }

    this.updateRectangle(x, y);
  }

  static updatePencil(x, y) {
    if (TouchInput.isPressed()) {
      if (!currentChange) {
        this.resetCurrentChange();
      }

      if (currentLayer === Layers.blend) {
        const offset = Input.isPressed('shift') ? 0.125 : 0.25;
        CycloneMapEditor$1._applyBlendBrush(x - offset, y - offset, false);
        return;
      }

      CycloneMapEditor$1.applySelectedTiles(x, y);
      return;
    }

    if (wasPressing) {
      CycloneMapEditor$1.logChange();
    }
  }

  static getGridRatio(drawRatio = false) {
    if (!drawRatio) {
      if (currentLayer === Layers.blend) {
        return 16;
      }
    }

    if (puzzleMode) {
      return 2;
    }

    if (currentLayer === Layers.collisions) {
      if (window.CycloneMovement) {
        return window.CycloneMovement.collisionStepCount;
      }

      const count = this.params.collisionStepCount;
      if ([1, 2, 4].includes(count)) {
        return count;
      }

      return 1;
    }

    return 1;
  }

  static canvasToMapX(x) {
    const gridRatio = this.getGridRatio();
    const originX = $gameMap._displayX * tileWidth;
    const mapX = (originX + x) / tileWidth;
    return Math.floor(mapX * gridRatio) / gridRatio;
  }

  static canvasToMapY(y) {
    const gridRatio = this.getGridRatio();


    const originY = $gameMap._displayY * tileHeight;
    const mapY = (originY + y) / tileHeight;
    return Math.floor(mapY * gridRatio) / gridRatio;
  }

  static requestCollisionRefresh() {
    if (!this.active) {
      return;
    }

    if (currentLayer !== Layers.collisions) {
      return;
    }

    if (this.isMapEditorScene()) {
      SceneManager._scene._mapEditorGrid.requestRefresh();
    }
  }

  static jumpToTile(tileId) {
    return SceneManager._scene._mapEditorWindow && SceneManager._scene._mapEditorWindow.jumpToTile(tileId);
  }

  static jumpToLastTile() {
    if (!SceneManager._scene._mapEditorWindow) {
      return;
    }

    SceneManager._scene._mapEditorWindow.setTopRow(SceneManager._scene._mapEditorWindow.maxTopRow());
  }

  static jumpToOneTileOf(tileList) {
    for (const tileId of tileList) {
      if (this.jumpToTile(tileId)) {
        return;
      }
    }
  }
}

globalThis.CycloneMapEditor = CycloneMapEditor$1;
CycloneMapEditor$1.register();

const regionColors = [
  '#e75858',
  '#c0986f',
  '#cbcf32',
  '#8ab24c',
  '#22aa47',
  '#1cbf97',
  '#7ec1df',
  '#4da4dc',
  '#4f36a9',
  '#725fb9',
  '#d48de4',
  '#fa5e84'
];

CycloneMapEditor.patchClass(Bitmap, $super => class {
  drawNormalTile(tileId, x, y, drawWidth, drawHeight) {
    if (tileId === undefined) {
      return;
    }

    const bitmap =  CycloneMapEditor.loadTilesetBitmap(tileId);
    if (!bitmap) {
      return;
    }

    const sourceX = ((Math.floor(tileId / 128) % 2) * 8 + (tileId % 8)) * CycloneMapEditor.tileWidth;
    const sourceY = (Math.floor((tileId % 256) / 8) % 16) * CycloneMapEditor.tileHeight;

    this.blt(bitmap, sourceX, sourceY, CycloneMapEditor.tileWidth, CycloneMapEditor.tileHeight, x, y, drawWidth ?? CycloneMapEditor.tileWidth, drawHeight ?? CycloneMapEditor.tileHeight);
    return bitmap;
  }

  drawAutoTileTable(bitmap, table, tileX, tileY, x, y, drawWidth, drawHeight) {
    const halfWidth = CycloneMapEditor.tileWidth / 2;
    const halfHeight = CycloneMapEditor.tileHeight / 2;
    const drawHalfWidth = (drawWidth ?? CycloneMapEditor.tileWidth) / 2;
    const drawHalfHeight = (drawHeight ?? CycloneMapEditor.tileHeight) / 2;

    for (let i = 0; i < 4; i++) {
      const tableX = table[i][0];
      const tableY = table[i][1];

      const sourceX = (tileX * CycloneMapEditor.tileWidth) + (tableX * halfWidth);
      const sourceY = (tileY * CycloneMapEditor.tileHeight) + (tableY * halfHeight);
      const targetX = x + (i % 2) * drawHalfWidth;
      const targetY = y + Math.floor(i / 2) * drawHalfHeight;

      this.blt(bitmap, sourceX, sourceY, halfWidth, halfHeight, targetX, targetY, drawHalfWidth, drawHalfHeight);
    }

    return bitmap;
  }

  drawTileA1(bitmap, tileId, x, y, drawWidth, drawHeight) {
    let tileX = 0;
    let tileY = 0;
    let autotileTable = Tilemap.FLOOR_AUTOTILE_TABLE;
    const kind = Tilemap.getAutotileKind(tileId);
    const shape = Tilemap.getAutotileShape(tileId);

    switch(kind) {
      case 0:
        tileX = 0;
        tileY = 0;
        break;
      case 1:
        tileX = 0;
        tileY = 3;
        break;
      case 2:
        tileX = 6;
        tileY = 0;
        break;
      case 3:
        tileX = 6;
        tileY = 3;
        break;
      default:
        tileX = Math.floor((kind % 8) / 4) * 8;
        tileY = Math.floor(kind / 8) * 6 + (Math.floor((kind % 8) / 2) % 2) * 3;

        if (kind % 2 === 1) {
          tileX += 6;
          autotileTable = Tilemap.WATERFALL_AUTOTILE_TABLE;
        }
        break;
    }

    return this.drawAutoTileTable(bitmap, autotileTable[shape], tileX, tileY, x, y, drawWidth, drawHeight);
  }

  drawTileA2(bitmap, tileId, x, y, drawWidth, drawHeight) {
    const kind = Tilemap.getAutotileKind(tileId);
    const tileX = (kind % 8) * 2;
    const tileY = (Math.floor(kind / 8) - 2) * 3;
    const shape = Tilemap.getAutotileShape(tileId);

    return this.drawAutoTileTable(bitmap, Tilemap.FLOOR_AUTOTILE_TABLE[shape], tileX, tileY, x, y, drawWidth, drawHeight);
  }

  drawTileA3(bitmap, tileId, x, y, drawWidth, drawHeight) {
    const kind = Tilemap.getAutotileKind(tileId);
    const tileX = (kind % 8) * 2;
    const tileY = (Math.floor(kind / 8) - 6) * 2;
    const shape = Tilemap.getAutotileShape(tileId);

    return this.drawAutoTileTable(bitmap, Tilemap.WALL_AUTOTILE_TABLE[shape], tileX, tileY, x, y, drawWidth, drawHeight);
  }

  drawTileA4(bitmap, tileId, x, y, drawWidth, drawHeight) {
    const kind = Tilemap.getAutotileKind(tileId);
    const tileX = (kind % 8) * 2;
    const tileY = Math.floor((Math.floor(kind / 8) - 10) * 2.5 + (Math.floor(kind / 8) % 2 === 1 ? 0.5 : 0));
    const shape = Tilemap.getAutotileShape(tileId);
    let autotileTable = Tilemap.FLOOR_AUTOTILE_TABLE;

    if (Math.floor(kind / 8) % 2 === 1) {
      autotileTable = Tilemap.WALL_AUTOTILE_TABLE;
    }

    return this.drawAutoTileTable(bitmap, autotileTable[shape], tileX, tileY, x, y, drawWidth, drawHeight);
  }

  drawAutoTile(tileId, x, y, drawWidth, drawHeight) {
    const bitmap =  CycloneMapEditor.loadTilesetBitmap(tileId);
    if (!bitmap) {
      return;
    }

    if (Tilemap.isTileA1(tileId)) {
      return this.drawTileA1(bitmap, tileId, x, y, drawWidth, drawHeight);
    }

    if (Tilemap.isTileA2(tileId)) {
      return this.drawTileA2(bitmap, tileId, x, y, drawWidth, drawHeight);
    }

    if (Tilemap.isTileA3(tileId)) {
      return this.drawTileA3(bitmap, tileId, x, y, drawWidth, drawHeight);
    }

    if (Tilemap.isTileA4(tileId)) {
      return this.drawTileA4(bitmap, tileId, x, y, drawWidth, drawHeight);
    }
  }

  drawTile(tileId, x, y, drawWidth, drawHeight) {
    if (tileId <= 0) {
      return;
    }

    if (Tilemap.isAutotile(tileId)) {
      return this.drawAutoTile(tileId, x, y, drawWidth, drawHeight);
    }

    return this.drawNormalTile(tileId, x, y, drawWidth, drawHeight);
  }

  drawAutoTilePieceTable(bitmap, tileX, tileY, x, y, drawWidth, drawHeight, pieceX, pieceY) {
    const halfWidth = CycloneMapEditor.tileWidth / 2;
    const halfHeight = CycloneMapEditor.tileHeight / 2;
    const realDrawWidth = (drawWidth ?? CycloneMapEditor.tileWidth);
    const realDrawHeight = (drawHeight ?? CycloneMapEditor.tileHeight);

    const sourceX = (tileX * CycloneMapEditor.tileWidth) + (pieceX * halfWidth);
    const sourceY = (tileY * CycloneMapEditor.tileHeight) + (pieceY * halfHeight);
    const targetX = x;
    const targetY = y;

    this.blt(bitmap, sourceX, sourceY, halfWidth, halfHeight, targetX, targetY, realDrawWidth, realDrawHeight);

    return bitmap;
  }

  drawTilePieceA1(bitmap, tileId, x, y, drawWidth, drawHeight, pieceX, pieceY) {
    let tileX = 0;
    let tileY = 0;
    const kind = Tilemap.getAutotileKind(tileId);

    switch(kind) {
      case 0:
        tileX = 0;
        tileY = 0;
        break;
      case 1:
        tileX = 0;
        tileY = 3;
        break;
      case 2:
        tileX = 6;
        tileY = 0;
        break;
      case 3:
        tileX = 6;
        tileY = 3;
        break;
      default:
        tileX = Math.floor((kind % 8) / 4) * 8;
        tileY = Math.floor(kind / 8) * 6 + (Math.floor((kind % 8) / 2) % 2) * 3;

        if (kind % 2 === 1) {
          tileX += 6;
        }
        break;
    }

    return this.drawAutoTilePieceTable(bitmap, tileX, tileY, x, y, drawWidth, drawHeight, pieceX, pieceY);
  }

  drawTilePieceA2(bitmap, tileId, x, y, drawWidth, drawHeight, pieceX, pieceY) {
    const kind = Tilemap.getAutotileKind(tileId);
    const tileX = (kind % 8) * 2;
    const tileY = (Math.floor(kind / 8) - 2) * 3;

    return this.drawAutoTilePieceTable(bitmap, tileX, tileY, x, y, drawWidth, drawHeight, pieceX, pieceY);
  }

  drawPuzzlePiece(pieceId, x, y, drawWidth, drawHeight) {
    if (pieceId <= 0) {
      return;
    }

    if (!Tilemap.isAutotile(pieceId)) {
      return;
    }

    const kind = Tilemap.getAutotileKind(pieceId);
    const tileId = Tilemap.makeAutotileId(kind, 0);
    const bitmap =  CycloneMapEditor.loadTilesetBitmap(tileId);
    if (!bitmap) {
      return;
    }

    const pieceShape = pieceId - tileId;
    const pieceX = pieceShape % 4;
    const pieceY = Math.floor(pieceShape / 4);

    if (Tilemap.isTileA1(tileId)) {
      return this.drawTilePieceA1(bitmap, tileId, x, y, drawWidth, drawHeight, pieceX, pieceY);
    }

    if (Tilemap.isTileA2(tileId)) {
      return this.drawTilePieceA2(bitmap, tileId, x, y, drawWidth, drawHeight, pieceX, pieceY);
    }
  }

  drawIcon(iconIndex, x, y, drawWidth, drawHeight) {
    const bitmap = ImageManager.loadSystem('IconSet');
    const pw = ImageManager.iconWidth;
    const ph = ImageManager.iconHeight;
    const sx = (iconIndex % 16) * pw;
    const sy = Math.floor(iconIndex / 16) * ph;

    const realDrawWidth = drawWidth ?? pw;
    const realDrawHeight = drawHeight ?? ph;

    this.blt(bitmap, sx, sy, pw, ph, x, y, realDrawWidth, realDrawHeight);
  }

  drawRegion(regionId, x, y, drawWidth, drawHeight, stretchIcon = false) {
    const realDrawWidth = drawWidth ?? CycloneMapEditor.tileWidth;
    const realDrawHeight = drawHeight ?? CycloneMapEditor.tileHeight;

    if (regionId > 0) {
      const color = regionColors[regionId % regionColors.length];
      this.fillRect(x, y, realDrawWidth, realDrawHeight, `${ color}66`);
    }

    let iconIndex = CycloneMapEditor.regionIcons.get(regionId) ?? 0;
    if (iconIndex) {
      const {iconWidth, iconHeight} = ImageManager;
      const diffX = (realDrawWidth - iconWidth) / 2;
      const diffY = (realDrawHeight - iconHeight) / 2;

      const iconDrawWidth = stretchIcon ? realDrawWidth : iconWidth;
      const iconDrawHeight = stretchIcon ? realDrawHeight : iconHeight;
      const iconX = stretchIcon ? x : x + diffX;
      const iconY = stretchIcon ? y : y + diffY;

      this.drawIcon(iconIndex, iconX, iconY, iconDrawWidth, iconDrawHeight);
    } else {
      this.drawText(regionId, x, y, realDrawWidth, realDrawHeight, 'center');
    }
  }

  drawShadow(shadowId, x, y, drawWidth, drawHeight) {
    const halfWidth = (drawWidth ?? CycloneMapEditor.tileWidth) / 2;
    const halfHeight = (drawHeight ?? CycloneMapEditor.tileHeight) / 2;

    if (shadowId <= 0 || shadowId > 15) {
      return;
    }

    const table = shadowId.toString(2).padZero(4);
    for (let i = 0; i < 4; i++) {
      if (table[3 - i] !== '1') {
        continue;
      }

      const drawX = x + (i % 2) * halfWidth;
      const drawY = y + Math.floor(i / 2) * halfHeight;

      this.fillRect(drawX, drawY, halfWidth, halfHeight, '#00000066');
    }
  }

  drawCollisionType(collision, x, y, drawWidth, drawHeight) {
    if (collision === 0) {
      return;
    }

    const realDrawWidth = drawWidth ?? CycloneMapEditor.tileWidth;
    const realDrawHeight = drawHeight ?? CycloneMapEditor.tileHeight;

    const colorIndex = collision <= 3 ? collision - 1 : 0;

    const context = this.context;
    context.save();

    const color = ['#00FF00', '#FF0000', '#FF00FF'][colorIndex];
    context.fillStyle = color;
    context.fillRect(x, y, realDrawWidth, realDrawHeight);

    let goesUp = false;
    let goesDown = false;
    let goesLeft = false;
    let goesRight = false;

    if (collision >= 20) {
      const unblockedDirection = collision - 20;
      goesUp = !DirectionHelper.goesUp(unblockedDirection);
      goesDown = !DirectionHelper.goesDown(unblockedDirection);
      goesLeft = !DirectionHelper.goesLeft(unblockedDirection);
      goesRight = !DirectionHelper.goesRight(unblockedDirection);
    } else if (collision > 10) {
      const blockedDirection = collision - 10;
      goesUp = DirectionHelper.goesUp(blockedDirection);
      goesDown = DirectionHelper.goesDown(blockedDirection);
      goesLeft = DirectionHelper.goesLeft(blockedDirection);
      goesRight = DirectionHelper.goesRight(blockedDirection);
    } else if (collision === 4) {
      goesDown = true;
      goesUp = true;
    } else if (collision === 5) {
      goesLeft = true;
      goesRight = true;
    }

    if (collision > 3) {
      const pieceWidth = Math.floor(realDrawWidth / 4);
      const pieceHeight = Math.floor(realDrawHeight / 4);
      context.fillStyle = '#FF00FF';

      if (goesUp) {
        context.fillRect(x, y, realDrawWidth, pieceHeight);
      }
      if (goesDown) {
        context.fillRect(x, y + realDrawHeight - pieceHeight, realDrawWidth, pieceHeight);
      }

      if (goesLeft) {
        context.fillRect(x, y, pieceWidth, realDrawHeight);
      }

      if (goesRight) {
        context.fillRect(x + realDrawWidth - pieceWidth, y, pieceWidth, realDrawHeight);
      }
    }

    context.strokeStyle = '#000000';
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x + realDrawWidth, y);
    context.stroke();
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x, y + realDrawHeight);
    context.stroke();
  }
});

CycloneMapEditor.patchClass(DataManager, $super => class {
  static loadMapData(mapId) {
    if (mapId > 0 && CycloneMapEditor.mapCaches[mapId]) {
      globalThis.$dataMap = CycloneMapEditor.mapCaches[mapId];
      this.onLoad('$dataMap');
      return;
    }

    return $super.loadMapData.call(this, mapId);
  }
});

CycloneMapEditor.patchClass(Game_Map, $super => class {
  screenTileX() {
    if (!CycloneMapEditor.active) {
      return $super.screenTileX.call(this);
    }

    return (Graphics.width - CycloneMapEditor.windowWidth) / this.tileWidth();
  }

  screenTileY() {
    if (!CycloneMapEditor.active) {
      return $super.screenTileY.call(this);
    }

    return (Graphics.height - 40) / this.tileHeight();
  }

  regionId(x, y) {
    if (CycloneMapEditor.active) {
      return CycloneMapEditor.getCurrentTileAtPosition(x, y, 5, false);
    }

    return $super.regionId.call(this, x, y);
  }

  isLoopHorizontal() {
    if (CycloneMapEditor.active) {
      return false;
    }

    return $super.isLoopHorizontal.call(this);
  }

  isLoopVertical() {
    if (CycloneMapEditor.active) {
      return false;
    }

    return $super.isLoopVertical.call(this);
  }

  canvasToMapX(x) {
    if (!CycloneMapEditor.active || CycloneMapEditor.currentZoom === 1) {
      return $super.canvasToMapX.call(this, x);
    }

    const tileWidth = this.tileWidth() * CycloneMapEditor.currentZoom;
    const originX = this._displayX * tileWidth;
    const mapX = Math.floor((originX + x) / tileWidth);
    return this.roundX(mapX);
  }

  canvasToMapY(y) {
    if (!CycloneMapEditor.active || CycloneMapEditor.currentZoom === 1) {
      return $super.canvasToMapY.call(this, y);
    }

    const tileHeight = this.tileHeight() * CycloneMapEditor.currentZoom;
    const originY = this._displayY * tileHeight;
    const mapY = Math.floor((originY + y) / tileHeight);
    return this.roundY(mapY);
  }

  scrollDown(distance) {
    if (!CycloneMapEditor.active) {
      return $super.scrollDown.call(this, distance);
    }

    const extraTiles = Math.ceil(Graphics.height / this.tileHeight()) - 3;
    const lastY = this._displayY;
    this._displayY = Math.min(this._displayY + distance, this.height() - this.screenTileY() + extraTiles);
    this._parallaxY += this._displayY - lastY;
  }

  scrollLeft(distance) {
    if (!CycloneMapEditor.active) {
      return $super.scrollLeft.call(this, distance);
    }

    const extraTiles = Math.ceil(Graphics.width / this.tileWidth()) - 3;
    const lastX = this._displayX;
    this._displayX = Math.max(this._displayX - distance, -extraTiles);
    this._parallaxX += this._displayX - lastX;
  }

  scrollRight(distance) {
    if (!CycloneMapEditor.active) {
      return $super.scrollRight.call(this, distance);
    }

    const extraTiles = Math.ceil(Graphics.width / this.tileWidth()) - 5;
    const lastX = this._displayX;
    this._displayX = Math.min(this._displayX + distance, this.width() - this.screenTileX() + extraTiles);
    this._parallaxX += this._displayX - lastX;
  }

  scrollUp(distance) {
    if (!CycloneMapEditor.active) {
      return $super.scrollUp.call(this, distance);
    }

    const extraTiles = Math.ceil(Graphics.height / this.tileHeight()) - 3;
    const lastY = this._displayY;
    this._displayY = Math.max(this._displayY - distance, -extraTiles);
    this._parallaxY += this._displayY - lastY;
  }

  checkTileIdPassage(tileId, d) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    return this.getPassageBitType(flag, d);
  }

  getPassageBitType(flag, d) {
    const bit = (1 << (d / 2 - 1)) & 0x0f;
    if ((flag & bit) === 0) {
      // [o] Passable
      return true;
    }
    if ((flag & bit) === bit) {
      // [x] Impassable
      return false;
    }
  }

  getTileFlag(tileId) {
    const flags = this.tilesetFlags();
    return flags[tileId];
  }

  checkTileIdPassageType(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    if ((flag & 0x10) !== 0) {
      if (tileId < Tilemap.TILE_ID_A1) {
        return TilePassageType.star;
      }
      return TilePassageType.free;
    }

    const top = this.getPassageBitType(flag, 8);
    const bottom = this.getPassageBitType(flag, 2);
    const left = this.getPassageBitType(flag, 4);
    const right = this.getPassageBitType(flag, 6);

    if (top === false && bottom === false && left === false && right === false) {
      return TilePassageType.blocked;
    }

    return TilePassageType.free;
  }

  tileIdIsBush(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    return (flag & 0x40) !== 0;
  }

  tileIdIsLadder(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    return (flag & 0x20) !== 0;
  }

  tileIdIsCounter(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    return (flag & 0x80) !== 0;
  }

  tileIdIsDamage(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    return (flag & 0x100) !== 0;
  }

  tileIdTerrainTag(tileId) {
    const flags = this.tilesetFlags();
    const flag = flags[tileId];

    const tag = flag >> 12;
    if (tag > 0) {
      return tag;
    }

    return 0;
  }
});

CycloneMapEditor.patchClass(Game_Player, $super => class {
  centerX() {
    if (!CycloneMapEditor.active) {
      return $super.centerX.call(this);
    }

    return ((Graphics.width - CycloneMapEditor.windowWidth) / $gameMap.tileWidth() - 1) / 2.0;
  }

  centerY() {
    if (!CycloneMapEditor.active) {
      return $super.centerY.call(this);
    }

    return ((Graphics.height - 40) / $gameMap.tileHeight() - 1) / 2.0;
  }

  reserveTransfer(mapId, ...args) {
    if (CycloneMapEditor.changeHistory.length > 0) {
      if (confirm('Do you want to save your map before teleporting away?')) {
        CycloneMapEditor._doSave();
      }
    }

    $super.reserveTransfer.call(this, mapId, ...args);
  }

  executeEncounter() {
    const result = $super.executeEncounter.call(this);

    if (result) {
      if (CycloneMapEditor.changeHistory.length > 0) {
        if (confirm('Do you want to save your map before the battle starts?')) {
          CycloneMapEditor._doSave();
        }
      }
    }

    return result;
  }

  updateMove() {
    $super.updateMove.call(this);
    CycloneMapEditor.requestCollisionRefresh();
  }
});

class WindowCycloneGrid extends Window_Base {
  initialize() {
    const width = Graphics.width;
    const height = Graphics.height;
    const rect = new Rectangle(0, 0, width, height);

    super.initialize(rect);

    this.padding = 0;
    this.refresh();
    this.opacity = 0;

    this.backOpacity = 0;
    this.hide();
    this.deactivate();
  }

  createContents() {
    this._padding = 0;
    super.createContents();
  }

  drawCellGrid(x, y) {
    if (!CycloneMapEditor.showGrid) {
      return;
    }

    const gridRatio = CycloneMapEditor.getGridRatio(true);

    const drawWidth = Math.floor(CycloneMapEditor.tileWidth * CycloneMapEditor.currentZoom) / gridRatio;
    const drawHeight = Math.floor(CycloneMapEditor.tileHeight * CycloneMapEditor.currentZoom) / gridRatio;

    const context = this.contents.context;
    context.strokeStyle = '#666666';

    for (let cellX = 0; cellX < gridRatio; cellX++) {
      for (let cellY = 0; cellY < gridRatio; cellY++) {

        const drawX = x + cellX * drawWidth;
        const drawY = y + cellY * drawHeight;

        context.strokeRect(drawX, drawY, drawWidth, drawHeight);
      }
    }
    context.stroke();
  }

  maybeDrawRegions(x, y) {
    if (!CycloneMapEditor.isLayerVisible(Layers.regions)) {
      return;
    }

    if (CycloneMapEditor.isLayerVisible(Layers.tags)) {
      return;
    }

    const mapX = $gameMap.canvasToMapX(x);
    const mapY = $gameMap.canvasToMapY(y);

    const regionId = $gameMap.regionId(mapX, mapY);
    if (regionId > 0) {
      this.contents.drawRegion(regionId, x, y);
    }
  }

  checkTilePassability(x, y, d) {
    return $gameMap.isPassable(x, y, d);
  }

  drawTilesetCollision(x, y) {
    const mapX = $gameMap.canvasToMapX(x);
    const mapY = $gameMap.canvasToMapY(y);
    const drawWidth = CycloneMapEditor.tileWidth;
    const drawHeight = CycloneMapEditor.tileHeight;

    const downBlocked = !this.checkTilePassability(mapX, mapY, 2);
    const upBlocked = !this.checkTilePassability(mapX, mapY, 8);
    const leftBlocked = !this.checkTilePassability(mapX, mapY, 4);
    const rightBlocked = !this.checkTilePassability(mapX, mapY, 6);
    const same = downBlocked === upBlocked && downBlocked === leftBlocked && downBlocked === rightBlocked;

    if (downBlocked && same) {
      this.contents.fillRect(x, y, drawWidth, drawHeight, '#FF000033');
      return;
    }

    const sideHeight = 2;
    const sideWidth = 2;

    this.contents.fillRect(x, y, drawWidth, drawHeight, '#00FF0033');

    if (downBlocked) {
      this.contents.fillRect(x, y + drawHeight - sideHeight, drawWidth, sideHeight, '#FF00FFAA');
    }
    if (upBlocked) {
      this.contents.fillRect(x, y, drawWidth, sideHeight, '#FF00FFAA');
    }
    if (leftBlocked) {
      this.contents.fillRect(x, y, sideWidth, drawHeight, '#FF00FFAA');
    }
    if (rightBlocked) {
      this.contents.fillRect(x + drawWidth - sideWidth, y, sideWidth, drawHeight, '#FF00FFAA');
    }
  }

  drawCustomCollision(x, y) {
    const mapX = CycloneMapEditor.canvasToMapX(x);
    const mapY = CycloneMapEditor.canvasToMapY(y);
    const customCollisionTable = CycloneMapEditor.customCollisionTable;
    const height = $gameMap.height() * 4;
    const width = $gameMap.width() * 4;
    const tileWidth = CycloneMapEditor.tileWidth;
    const tileHeight = CycloneMapEditor.tileHeight;
    const drawWidth = tileWidth / 4;
    const drawHeight = tileHeight / 4;
    const colors = ['#00FF00AA', '#FF0000AA', '#FF00FFFF'];

    const context = this.contents.context;
    context.save();

    const drawCustomSideCollisions = (goesUp, goesDown, goesLeft, goesRight, drawX, drawY) => {
      context.fillStyle = colors[2];
      const pieceWidth = Math.floor(drawWidth / 4);
      const pieceHeight = Math.floor(drawHeight / 4);

      if (goesUp) {
        context.fillRect(drawX, drawY, drawWidth, pieceHeight);
      }
      if (goesDown) {
        context.fillRect(drawX, drawY + drawHeight - pieceHeight, drawWidth, pieceHeight);
      }

      if (goesLeft) {
        context.fillRect(drawX, drawY, pieceWidth, drawHeight);
      }
      if (goesRight) {
        context.fillRect(drawX + drawWidth - pieceWidth, drawY, pieceWidth, drawHeight);
      }
    };

    for (let cellX = 0; cellX < 4; cellX++) {
      for (let cellY = 0; cellY < 4; cellY++) {

        const intX = Math.floor(mapX * 4) + cellX;
        const intY = Math.floor(mapY * 4) + cellY;
        const index = (intY % height) * width + (intX % width);

        if (customCollisionTable[index]) {
          const drawX = x + (cellX * drawWidth);
          const drawY = y + (cellY * drawHeight);

          context.clearRect(drawX, drawY, drawWidth, drawHeight);

          const collision = customCollisionTable[index];
          const colorIndex = collision <= 3 ? collision - 1 : 0;
          const color = colors[colorIndex];
          context.fillStyle = color;
          context.fillRect(drawX, drawY, drawWidth, drawHeight);

          let goesUp = false;
          let goesDown = false;
          let goesLeft = false;
          let goesRight = false;

          if (collision >= 20) {
            const d = collision - 20;
            goesUp = !DirectionHelper.goesUp(d);
            goesDown = !DirectionHelper.goesDown(d);
            goesLeft = !DirectionHelper.goesLeft(d);
            goesRight = !DirectionHelper.goesRight(d);
          } else if (collision > 10) {
            const d = collision - 10;
            goesUp = DirectionHelper.goesUp(d);
            goesDown = DirectionHelper.goesDown(d);
            goesLeft = DirectionHelper.goesLeft(d);
            goesRight = DirectionHelper.goesRight(d);
          } else if (collision === 4) {
            goesUp = true;
            goesDown = true;
          } else if (collision === 5) {
            goesLeft = true;
            goesRight = true;
          }

          if (collision > 3) {
            drawCustomSideCollisions(goesUp, goesDown, goesLeft, goesRight, drawX, drawY);
          }
        }
      }
    }

    context.restore();
    this.contents._baseTexture.update();
  }

  maybeDrawCollisions(x, y) {
    if (!CycloneMapEditor.isLayerVisible(Layers.collisions)) {
      return;
    }

    this.drawTilesetCollision(x, y);
    this.drawCustomCollision(x, y);
  }

  maybeDrawTags(x, y) {
    if (!CycloneMapEditor.isLayerVisible(Layers.tags)) {
      return;
    }

    const mapX = $gameMap.canvasToMapX(x);
    const mapY = $gameMap.canvasToMapY(y);

    const terrainTag = $gameMap.terrainTag(mapX, mapY);
    if (terrainTag === 0) {
      return;
    }

    const drawWidth = CycloneMapEditor.tileWidth;
    const drawHeight = CycloneMapEditor.tileHeight;

    this.contents.drawText(terrainTag, x, y, drawWidth, drawHeight, 'center');
  }

  drawCell(x, y) {
    const mapX = $gameMap.canvasToMapX(x);
    const mapY = $gameMap.canvasToMapY(y);

    if (!$gameMap.isValid(mapX, mapY)) {
      return false;
    }


    this.maybeDrawCollisions(x, y);
    this.maybeDrawRegions(x, y);
    this.maybeDrawTags(x, y);

    this.drawCellGrid(x, y);
  }

  refresh() {
    this.contents.clear();

    this._lastDisplayX = $gameMap._displayX;
    this._lastDisplayY = $gameMap._displayY;

    const drawWidth = Math.floor(CycloneMapEditor.tileWidth * CycloneMapEditor.currentZoom);
    const drawHeight = Math.floor(CycloneMapEditor.tileHeight * CycloneMapEditor.currentZoom);

    let paddingX;
    let paddingY;

    if ($gameMap._displayX < 0) {
      paddingX = Math.floor($gameMap._displayX * CycloneMapEditor.tileWidth);
    } else {
      paddingX = Math.floor(($gameMap._displayX - Math.floor($gameMap._displayX)) * drawWidth);
    }

    if ($gameMap._displayY < 0) {
      paddingY = Math.floor($gameMap._displayY * CycloneMapEditor.tileHeight);
    } else {
      paddingY = Math.floor(($gameMap._displayY - Math.floor($gameMap._displayY)) * drawHeight);
    }

    const mapStartX = 0 - paddingX;
    const mapStartY = 0 - paddingY;
    const mapEndX = mapStartX + ($gameMap.width() * drawWidth);
    const mapEndY = mapStartY + ($gameMap.height() * drawHeight);

    const rightPos = Math.min(Graphics.width, mapEndX);
    let bottomPos = Math.min(Graphics.height, mapEndY);

    for (let x = mapStartX; x < rightPos; x += drawWidth) {
      if (x + drawWidth < 0) {
        continue;
      }

      for (let y = mapStartY; y < bottomPos; y += drawHeight) {
        if (y + drawHeight < 0) {
          continue;
        }

        this.drawCell(x, y);
      }
    }

    if (CycloneMapEditor.isLayerVisible(Layers.collisions)) {
      this.drawEventsCollision();
      this.drawPlayerCollision();
    }
  }

  drawEventsCollision() {
    const drawWidth = $gameMap.tileWidth();
    const drawHeight = $gameMap.tileHeight();

    for (const event of $gameMap._events) {
      if (!event) {
        continue;
      }
      if (event._priorityType !== 1 || event._through || event._erased) {
        continue;
      }

      const x = event.x * $gameMap.tileWidth();
      const y = event.y * $gameMap.tileHeight();
      const drawX = x - ($gameMap._displayX * $gameMap.tileWidth());
      const drawY = y - ($gameMap._displayY * $gameMap.tileHeight());

      if (drawX + drawWidth < 0 || drawY + drawHeight < 0) {
        continue;
      }

      this.contents.fillRect(drawX, drawY, drawWidth, drawHeight, '#FF00FF66');
    }
  }

  drawPlayerCollision() {
    if (window.CycloneMovement) {
      return this.drawCycloneMovementPlayerCollision();
    }

    const x = $gamePlayer.x * $gameMap.tileWidth();
    const y = $gamePlayer.y * $gameMap.tileHeight();
    const drawWidth = $gameMap.tileWidth();
    const drawHeight = $gameMap.tileHeight();
    const drawX = x - ($gameMap._displayX * $gameMap.tileWidth());
    const drawY = y - ($gameMap._displayY * $gameMap.tileHeight());

    if (drawX + drawWidth < 0 || drawY + drawHeight < 0) {
      return;
    }

    this.contents.fillRect(drawX, drawY, drawWidth, drawHeight, '#0000FF66');
  }

  drawCycloneMovementPlayerCollision() {
    const { top, left, width, height } = $gamePlayer;

    const x = left * $gameMap.tileWidth();
    const y = top * $gameMap.tileHeight();
    const drawWidth = width * $gameMap.tileWidth();
    const drawHeight = height * $gameMap.tileHeight();
    const drawX = x - ($gameMap._displayX * $gameMap.tileWidth());
    const drawY = y - ($gameMap._displayY * $gameMap.tileHeight());

    if (drawX + drawWidth < 0 || drawY + drawHeight < 0) {
      return;
    }

    this.contents.fillRect(drawX, drawY, drawWidth, drawHeight, '#0000FF66');
  }

  update() {
    if (!CycloneMapEditor.active) {
      return;
    }

    if (this._lastDisplayX !== $gameMap._displayX || this._lastDisplayY !== $gameMap._displayY) {
      this.refresh();
    }
  }

  requestRefresh() {
    this._lastDisplayX = -999;
  }
}

const pencilIcon = new Image();
pencilIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQUOhdRws4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABGElEQVRIx9XUIW/CQBiA4ZcFwRQhkEEysEdSNAoFPwKBmcXO4FBTwxB+Av8BDQo1PQi1sGSChKBGMsHUkR7XMnpfQ8KnWvO8vVzv4N4nJQV+Doejfn7MZCwvLcVbShnv55G0FB88P+E1agC0lLIiDyL8NUe5n2cx/wRg0m6eIs4BA6+WAIyInko2e4wdCMMBNqtvaP9akVh7cBEHytUSna8lU99HFYvxftOr8K6Jr/f71FUBCf5vQIpfDCSBRwaSwkMDSeLWOdB4/6VJb7gT45EHreDVGb336HSXItwIBL9ej16JKx66goJXB2C7+DhFXHFjk4N3u17F23gG4IxHBvRMfR/AGbd+0+A9HoRd8dBzoO9xKXyz+QMErgZJQZj0xAAAAABJRU5ErkJggg==';

const lineIcon = new Image();
lineIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQVD1j5N6wAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAeklEQVRIx2NgGAWjYBTQHDAic77/+PGfGoZycnDAzWVBNtxJTY0qrv7+48d/mCUs6JJ89XsYpJ1VyTb8up08AwMDA4MsP///xx8/MjKhK6DE8Kd7b2OIseByAbUAC3LEUCuS1cTFsaciWNhRw5LHHz8yjmbSUTAKIAAAHrcgEXUU5YwAAAAASUVORK5CYII=';

const rectangleIcon = new Image();
rectangleIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQVGNsqsmsAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAA0klEQVRIx2NgGAUEACM2we8/fvwnxzBODg4M81iwGe6kpkaWa7//+PEf3RIWbIbz1e9hkHZWJcnwp3tvMzipqWFYwoJNsbSzKsPTvbdJskDaWZXhUyOELcvP///xx4+MDAwMDEz4NL3beozh3dZjRPOxASZCLhPytkJho/MJASZiDSdXDROt8wHBOCAEKI4DmvpgNA6I8gGhjEYIsJASvoT4RFvwdO9tsgo7ouoDSorrfbduMaiJizMwMDAwwAo7qlY46IbjtABW5JJjCbLhwwMAAI5+ZZrvEYecAAAAAElFTkSuQmCC';

const fillIcon = new Image();
fillIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQVI2ohW08AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABpElEQVRIx82VMUtCURiGH0PQlhxFwusSBxyCwKGMKLiELc5uIQ0FDtFWUzQb1B/oB0Q2NCfq4OQ/EE4tInFxKGlKabgNcS733vQeFaPOdM65l+f9vu+83znw38bHYGB/DAb2pP+Hp4WbQjjzxWg0NNfIs4Zh3x+s2d3bdTtrGBNlsjCLWKs6pFJOYAqBTmQmgev6m2edjMXsuQr8inNU/bOG4TmDZCxmB2UwlYteF1cp5gGkZ7/7/h6auUTKmsX8DgBbx+cA3DzWMIVA9nqzu2gUvH1ZcL4rkSAnhSeBP2TOudr83t/IRWhVh6TXfoqMarxQEHzpogZAadliJZUA4LljAThrtVc+2qchJX4R7RmUlq0fYD880z8d23haAT9MCbnh/uG2bWhcedLNDu3tlFNnN1iNTP+UVnXIRi5C4cyiISUiHvdYV5tBQ0oO93Y9mQB83p1o4VqBdLODKYQj4oYrRwXBp+rkhpSYQlApJyAXAdDCtQIv9SdnLuJxZK+HenCUaBBce7mNutDUkznJRTe20RRIRe6P0A2dKvJRftZF+OfjCySNE5ddU05FAAAAAElFTkSuQmCC';

const eraseIcon = new Image();
eraseIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQVLhSQJ/IAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABK0lEQVRIx2NgGAUEACMxir7/+PEfXYyTg4MovSzEGO6kpkbQUlwWshBj+EwDNQZpNWm4+NNbTxnQLf3+48d/bJawEONNZMNh/C1oYk5qalgtYSLG9YTA01tP4WxZfv7/RFmAy/WkAiZqup5oC0h1/ek7z4i3gBzXz/nyhWHfrVsMauLixCdT7t4Ohg9Q9pO7Txh09qwnyvWPP35kxGkBzPUdsyahaJJRlmH4oJyLInZw53YGk1tP8boeZxzIKMvgDZond58wMDAwMJxxs8freqKSKTZw985lBgYGBobZXbPxuh5nHMBciM1H6HL4XE90UYFsKCmux7CAk4ODEVvpiR7pxLoeqw9glqAXZOiAGNfjrXCQC61bL19iyMMMx+d6oms0bKUkMYYPDwAArdGaa9wnQ0IAAAAASUVORK5CYII=';

const saveIcon = new Image();
saveIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQVN3D7jzIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAA4ElEQVRIx2NgGAUDDRhxSXz/8eM/KQZxcnBgNYsFl+FOamokufT7jx//sVnCgstwC6dMBgl1RQZ1Q0kGFXlJrIbeefic4eb55wwvbt5ncFJTw2oJEy4XSagrMjAwMOA0PNXdBUMtAwMDgyw//3+iLKAWIGjBnYfPsYqXz1pMlAUshBTcPA8JZ5r5gFJA0Acvbt7HK48cwWRZgM8QQpYTbQExBg1fHzARMvTEvukMEuqKWPGJfdMJOoCRWoXdvlu3GNTExRkYGBgYHn/8yEj14hqb4XgtwFZwEQLoho8CogAAz0BbPsc/fBQAAAAASUVORK5CYII=';

const reloadIcon = new Image();
reloadIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQWA3piKEQAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABe0lEQVRIx9VVq7KDMBDdzCBARVbcwaaytpoavoUf4Bv6A/0WDGgsklgGUYkqjorOMpvthuSaO3PXpSTn7OPsKcB/DxVz6bWuG/8tS9Oot0kMaGGM9zsS8fMhwWtdNwr6uP/A+ZLv53GYdlIEpmdKonzgHFSKcZigqmcniVvZQ2ftXkkigbfNVQThFZ0vObTNB5Qnk2u9TcuikiPwW9kDAEBnrfP40455J6JvooaMWSOwOZ2c7/b5hMIYqOoZ2iaPU1GWpooOt7PWAZ6WRfkqjZYpktCsEViqkvd9HKbwoklLxfXNZUyDVu4MOfSQ6ptWyoNXnsTqvzDmiyTXeqOD5/MSVSSB096ivqXBI1GWpmEvknaBy5XaBFbOKw0StM0VbmUPhTG0DY7/SJL92mQpc7qp1OB8Jog+RCv1bjIAQFXP8LiD4zuhzecqUj6Zok3gWbJsaoIS+OGi4WXsu7Qf1AR9my/aAOobL+dab1TnHFTS/6/+kzlxCPRP4w3A6io0yt+JDgAAAABJRU5ErkJggg==';

const undoIcon = new Image();
undoIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQWHYBtFScAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABKUlEQVRIx+1Uq7LCMBA97URwMchiro2s7AdUFF3db0H1W9DVRfABlZWx14DEAK6Ym50lbJqAwXBmOtNNZs7Zx9kAX3waie/iertNAPCzWCQ8dmHvfVA+8lLrByEbhxIJCljyZbtGXeREbGMXPBFJJPWVxsl85ADQ9BWW7Rql1mIbVWhITV/R/26zf7izwnWRo2tBIrySNNYN3TACAA7G0HfZHumcV/i7Wk0vC9RFjqavqOc6y0hkDmko49BZCErytXXSDs/ZHYyBzrL3BbiIdGfJ/87nBMD0loBbCc/enE42pH2Za5+aeypKrcmm3TA+bTPfj8v2KLZPveIi9HmUIf7bF98i3yZ3w0g29Q0/CWXuzkFylTD8eIG5p5oTS+TRAu76u5CIv4jGHaoeqH4byFz/AAAAAElFTkSuQmCC';

const redoIcon = new Image();
redoIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQWJahvrbkAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABIklEQVRIx+1VqxaCQBC96yGAhYjFupHoBxDsZr/FxLeYyRQ/gEikWiRalIZBhzMsuzBosHDPIeze3TvPHYAF/4aSHHo2TWvbD3xfcZ7WHJ5EONFaxD+bpjWNeGPidPGY7wd8VpTgPK1NI6sx8XW6sYoDwGEXY51uemvCNgxbZwRcnC5lRYnH6dY7d8z3bz6PR+u3chGm+KWquo/2JfCmDpC4jiJUdY1Ea2faZkUwBpv3roi8OcKB7yuq0Rm3AU+Rfm2AG7FxJH6939Wkgawou1ZMtEZV1wDQCfPHxz3n4lYDPA3IYxx2MbJ0+Jp5G5MDs0cFRSHpd3pgZgRKMiq4t6YDvI1tKVK/DLup/IvG9TYM20+BIe2c2f8Dc4CZcIkvEOEFIdSyhtt+PqwAAAAASUVORK5CYII=';

class WindowCycloneMapEditorCommands extends Window_Command {
  initialize() {
    const x = Graphics.width - CycloneMapEditor.windowWidth;
    const y = 0;
    const w = CycloneMapEditor.windowWidth;
    const h = (CycloneMapEditor.tileDrawWidth >= 48 && Graphics.width >= 1280) ? 74 : 50;
    super.initialize(new Rectangle(x, y, w, h));
    this.showBackgroundDimmer();
    this.configureHandlers();
  }

  configureHandlers() {
    this.setHandler('undo', () => {
      CycloneMapEditor.undoButton();
      this.activate();
    });
    this.setHandler('redo', () => {
      CycloneMapEditor.redoButton();
      this.activate();
    });
    this.setHandler('pencil', () => {
      CycloneMapEditor.pencilButton();
      this.activate();
    });
    this.setHandler('rectangle', () => {
      CycloneMapEditor.rectangleButton();
      this.activate();
    });
    this.setHandler('fill', () => {
      CycloneMapEditor.fillButton();
      this.activate();
    });
    this.setHandler('eraser', () => {
      CycloneMapEditor.eraserButton();
      this.activate();
    });
    this.setHandler('save', () => {
      CycloneMapEditor.saveButton();
      this.activate();
    });
    this.setHandler('reload', () => {
      CycloneMapEditor.reloadButton();
      this.activate();
    });
  }

  maxScrollY() {
    return 0;
  }

  maxScrollX() {
    return 0;
  }

  processCursorMove() {
  }

  processHandling() {
  }

  updateBackOpacity() {
    this.backOpacity = 255;
  }

  _updateCursor() {
    this._cursorSprite.visible = false;
  }

  makeCommandList() {
    this.addCommand('Undo', 'undo');
    this.addCommand('Redo', 'redo');

    this.addCommand('', '');

    this.addCommand('Pen', 'pencil');
    this.addCommand('Rect', 'rectangle');
    this.addCommand('Fill', 'fill');
    this.addCommand('Erase', 'eraser');
  }

  colSpacing() {
    return 6;
  }

  rowSpacing() {
    return 0;
  }

  maxCols() {
    return 7;
  }

  redraw() {
    Window_Selectable.prototype.refresh.call(this);
  }

  getSymbolIcon(symbol) {
    switch(symbol) {
      case 'undo':
        return undoIcon;
      case 'redo':
        return redoIcon;
      case Tools.pencil:
        return pencilIcon;
      case Tools.rectangle:
        return rectangleIcon;
      case Tools.fill:
        return fillIcon;
      case Tools.eraser:
        return eraseIcon;
      case 'save':
        return saveIcon;
      case 'reload':
        return reloadIcon;
    }
  }

  itemRect(index) {
    const rect = super.itemRect(index);

    if (Graphics.width < 1280) {
      rect.width += 3;
    }

    return rect;
  }

  lineHeight() {
    if (Graphics.width >= 1280) {
      if (CycloneMapEditor.tileDrawWidth < 48) {
        return 14;
      }

      return 36;
    }

    return 14;
  }

  drawItem(index) {
    const symbol = this.commandSymbol(index);
    const rect = this.itemRect(index);

    if (CycloneMapEditor.changingTileProps && ['undo', 'redo'].includes(symbol)) {
      return;
    }

    if (CycloneMapEditor.currentLayer === Layers.blend) {
      if (symbol === Tools.fill) {
        return;
      }
    } else if (CycloneMapEditor.puzzleMode) {
      if ([Tools.rectangle, Tools.fill].includes(symbol)) {
        return;
      }
    }

    if (symbol === CycloneMapEditor.currentTool) {
      this.contents.fillRect(rect.x, rect.y + 2, rect.width, rect.height, '#00FF0066');

      this.contents.fillRect(rect.x -2, rect.y, rect.width + 4, 4, '#000000');
      this.contents.fillRect(rect.x -2, rect.y + 2 + rect.height, rect.width + 6, 4, '#000000');
      this.contents.fillRect(rect.x - 2, rect.y, 4, rect.height + 4, '#000000');
      this.contents.fillRect(rect.x + rect.width, rect.y, 4, rect.height + 4, '#000000');

      this.contents.fillRect(rect.x, rect.y + 2, rect.width, 2, '#FFFFFF');
      this.contents.fillRect(rect.x, rect.y + 2 + rect.height, rect.width + 2, 2, '#FFFFFF');
      this.contents.fillRect(rect.x, rect.y + 2, 2, rect.height, '#FFFFFF');
      this.contents.fillRect(rect.x + rect.width, rect.y + 2, 2, rect.height, '#FFFFFF');
    }

    const icon = this.getSymbolIcon(symbol);

    if (!icon) {
      return super.drawItem(index);
    }

    this.resetTextColor();
    if (symbol === 'undo') {
      this.changePaintOpacity(CycloneMapEditor.changeHistory.length > 0);
    } else if (symbol === 'redo') {
      this.changePaintOpacity(CycloneMapEditor.undoHistory.length > 0);
    } else {
      this.changePaintOpacity(true);
    }

    const ctx = this.contents._canvas.getContext('2d');
    ctx.imageSmoothingEnabled = false;
    const iconWidth = (CycloneMapEditor.tileDrawWidth >= 48 && Graphics.width >= 1280) ? 48 : 24;
    ctx.drawImage(icon, rect.x + 1, rect.y, iconWidth, iconWidth);
  }

  drawAllItems() {
    super.drawAllItems();
  }

  playCursorSound() {
  }

  playOkSound() {
  }

  playBuzzerSound() {
  }
}

const hiddenIcon = new Image();
hiddenIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQWFPmxrYMAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABBElEQVRIx+2UMQqDMBSGf4uDTo6dXCMIXsJLCEIPUfAYBc8gQqF3EM/woFBo1k4dOzWbnSJRk5jSsf4gmOfL95u8lwCbNv0qzzXxLcSgjsMgcJrru4JzxrTxNSN/DS7BVV1Pvsn4W4jBZuKtwSU4STMAwP12neSdjkf0nBtXsluDJ2k2wlUREZI0Q1XXyBlb1MhoMIdLXc7t4u/lymwmO9MWEdFkXJQHENH4FOVBmxtH0WAtchgEnlpcFaS+qyvrmgY952D7vVsXzU108Mu5BYAF/PF6eU5tqjNR1TUNAFjhTifZdNB6zgHACne+KuIoGvjzOYmp+22Cf3UX6TrEBt70R/oAQESSsFk1AwIAAAAASUVORK5CYII=';

const visibleIcon = new Image();
visibleIcon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bpUUqgmYo4pChdbIgKuIoVSyChdJWaNXB5NIvaGJIUlwcBdeCgx+LVQcXZ10dXAVB8APEydFJ0UVK/F9SaBHjwXE/3t173L0D/M0aU82ecUDVLCOTTIj5wooYfEUIEQgYRExipp7KLuTgOb7u4ePrXZxneZ/7c/QrRZMBPpF4lumGRbxOPL1p6Zz3iQVWkRTic+Ixgy5I/Mh12eU3zmWH/TxTMHKZOWKBWCx3sdzFrGKoxFPEUUXVKN+fd1nhvMVZrdVZ+578heGitpzlOs0RJLGIFNIQIaOOKmqwEKdVI8VEhvYTHv5hx58ml0yuKhg55rEBFZLjB/+D392apckJNymcAHpfbPsjBgR3gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WvQIGNgGLq47mrwHXO4AkSddMiRHCtD0l0rA+xl9UwEYugX6Vt3e2vs4fQBy1NXSDXBwCIyWKXvN492h7t7+PdPu7weVs3K1THf6MgAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QIGBQWDOrdNdUAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABOUlEQVRIx+2Uu07DMBSG/6AM6WR1apdYQkiRMrUbZM1SXgB2RkYY8hBdsvYJOtCpEqqYuvQpsIQiAQOdok7xdphOidM2NmIkv+ThHNvfufgCdOr0V3muCyutqW73gsBpr+8KTqPoqN8WyLfBGTyfxcYc+yutqS2Ib4MzeJguDTD70yiyBjkKT6SkYjWhSmtjJFJSIiUt7sZUaU3FakKJlNQ8I9ZZW+acNQA8318dnANXNp/F+0qcW/T5+oVh+mPH2RMecWvY9bWsUAj62O08pxZt8tFBi5pjk4/2LQqFoFAIslbQCwKPWzXFJQCgf73AxbkEALwV7yhfbgAAWV5irRSiwQAA0Mzec72m04e+MZflJQC0wp1e8qmHtlYKAFrhzl9FKASp7dbwMbgN/qu/iAPVbett6fQ/9A0c7tBBCKKL7gAAAABJRU5ErkJggg==';

class WindowCycloneMapEditorLayerList extends Window_Base {
  initialize() {
    const x = 0;
    const w = 180;
    const y = 0;
    const h = Graphics.height - 40;
    super.initialize(new Rectangle(x, y, w, h));
    this.showBackgroundDimmer();
  }

  update() {
    super.update();
  }

  updateBackOpacity() {
    this.backOpacity = 255;
  }

  refresh() {
    this.drawContents();
    SceneManager._scene.redrawMap();
  }

  drawContents() {
    this.contents.clear();
    const ctx = this.contents._canvas.getContext('2d');

    const names = [
      'Auto Layer',
      'Layer 1',
      'Layer 2',
      'Layer 3',
      'Layer 4',
      'Shadows',
      'Regions',
      'Events',
    ];
    this.contents.fontSize = 22;

    ctx.imageSmoothingEnabled = false;
    for (let i = 0; i < 8; i++) {
      const layerIndex = i === 0 ? Layers.auto : i - 1;
      this.contents.fontBold = CycloneMapEditor.currentLayer === layerIndex;
      this.changeTextColor(CycloneMapEditor.currentLayer === layerIndex ? ColorManager.powerUpColor() : ColorManager.normalColor());

      if (layerIndex !== Layers.auto) {
        ctx.drawImage(CycloneMapEditor.layerVisibility[layerIndex] ? visibleIcon : hiddenIcon, -4, 30 * i - 4, 48, 48);
        this.drawText(names[i], 40, i * 30, this.contents.width - 40, 'left');
      } else {
        this.drawText(names[i], 10, i * 30, this.contents.width - 10, 'left');
      }

    }
  }

  toggleLayerVisibility(layerIndex) {
    CycloneMapEditor.layerVisibility[layerIndex] = !CycloneMapEditor.layerVisibility[layerIndex];
    this.refresh();
    SceneManager._scene._mapEditorGrid.refresh();
  }

  getLayerIndex(y) {
    const padding = this.padding + 10;

    if (y < padding || y > this.height - padding + 6) {
      return -1;
    }

    const layerIndex = Math.floor((y - padding) / 30);
    if (y > padding + (layerIndex * 30) + 22) {
      return -1;
    }

    if (layerIndex > CycloneMapEditor.layerVisibility.length) {
      return -1;
    }

    if (layerIndex === 0) {
      return Layers.auto;
    }

    return layerIndex - 1;
  }

  onMapTouch(x, y) {
    let layerIndex = this.getLayerIndex(y);
    if (layerIndex < 0) {
      return;
    }

    if (x >= CycloneMapEditor.windowWidth / 2) {
      x -= CycloneMapEditor.windowWidth / 2;
      layerIndex += 4;
    }

    if (x < 50 && layerIndex < 7) {
      this.toggleLayerVisibility(layerIndex);
      return;
    }

    CycloneMapEditor.changeCurrentLayer(layerIndex);
    this.refresh();
  }
}

class WindowCycloneMapEditorStatus extends Window_Base {
  initialize() {
    const h = 40;
    super.initialize(new Rectangle(0, Graphics.height - h, Graphics.width, h));
    this.showBackgroundDimmer();
  }

  createContents() {
    this._padding = 0;
    super.createContents();
  }

  updateBackOpacity() {
    this.backOpacity = 255;
  }

  refresh() {
    this.drawContents();
  }

  lineHeight() {
    return 16;
  }

  makeLine() {
    let line = '';

    const addConditional = (paramName, newPart) => {
      if (CycloneMapEditor.params[paramName]) {
        if (line && newPart) {
          return `, ${ newPart }`;
        }

        return newPart;
      }

      return '';
    };

    line += addConditional('showMapId', `Map: ${ $gameMap._mapId }`);
    line += addConditional('showTilesetId', `Tileset: ${ $gameMap._tilesetId }`);
    line += addConditional('showPosition', `Pos: ${ CycloneMapEditor.statusMapX }, ${ CycloneMapEditor.statusMapY }`);

    if (CycloneMapEditor.params.showCellTiles) {
      const { statusTile1, statusTile2, statusTile3, statusTile4 } = CycloneMapEditor;
      if (line) {
        line += ' - ';
      }
      line += `Tiles: (${ statusTile1 }, ${ statusTile2 }, ${ statusTile3 }, ${ statusTile4 })`;
    }

    line += addConditional('showRegionId', `Region: ${ CycloneMapEditor.statusRegion }`);
    line += addConditional('showTag', `Tag: ${ CycloneMapEditor.statusTag }`);
    line += addConditional('showCollision', `Collision: ${ CycloneMapEditor.statusCollision }`);
    line += addConditional('showLadder', CycloneMapEditor.statusLadder ? ' Ladder' : '');
    line += addConditional('showBush', CycloneMapEditor.statusBush ? ' Bush' : '');
    line += addConditional('showCounter', CycloneMapEditor.statusCounter ? ' Counter' : '');
    line += addConditional('showDamageFloor', CycloneMapEditor.statusDamage ? ' Damage' : '');

    return line;
  }

  textY() {
    return 12;
  }

  drawMainLine() {
    const line = this.makeLine();
    this.drawText(line, 8, this.textY(), this.width - 8, 'left');
  }

  drawRightLine() {
    this.drawText(`TileId: ${ CycloneMapEditor.statusTileId }`, 0, this.textY(), this.width - 8, 'right');
  }

  drawContents() {
    this.contents.clear();
    this.contents.fontSize = 16;

    this.drawMainLine();
    this.drawRightLine();
  }
}

// import { Layers, Tools } from './constants';

class WindowCycloneMapEditorTabs extends Window_Command {
  initialize() {
    const x = Graphics.width - CycloneMapEditor.windowWidth;
    const y = SceneManager._scene._mapEditorCommands.y + SceneManager._scene._mapEditorCommands.height;
    const w = CycloneMapEditor.windowWidth;
    const h = 74;
    super.initialize(new Rectangle(x, y, w, h));
    this.showBackgroundDimmer();
    this.configureHandlers();
  }

  configureHandlers() {
    this.setHandler('a', () => {
      CycloneMapEditor.jumpToOneTileOf([Tilemap.TILE_ID_A1, Tilemap.TILE_ID_A2, Tilemap.TILE_ID_A3, Tilemap.TILE_ID_A4, Tilemap.TILE_ID_A5]);
      this.activate();
    });
    this.setHandler('b', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_B);
      this.activate();
    });
    this.setHandler('c', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_C);
      this.activate();
    });
    this.setHandler('d', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_D);
      this.activate();
    });
    this.setHandler('e', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_E);
      this.activate();
    });
    this.setHandler('f', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_E + 256);
      this.activate();
    });
    this.setHandler('g', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_E + 512);
      this.activate();
    });
    this.setHandler('h', () => {
      CycloneMapEditor.jumpToTile(Tilemap.TILE_ID_A5 + 256);
      this.activate();
    });
  }
  maxScrollY() {
    return 0;
  }

  maxScrollX() {
    return 0;
  }

  processCursorMove() {
  }

  processHandling() {
  }

  updateBackOpacity() {
    this.backOpacity = 255;
  }

  _updateCursor() {
    this._cursorSprite.visible = false;
  }

  makeCommandList() {
    this.addCommand('A', 'a');
    this.addCommand('B', 'b');
    this.addCommand('C', 'c');
    this.addCommand('D', 'd');
    this.addCommand('E', 'e');
    this.addCommand('F', 'f', Boolean(window.CycloneExtraTilesets));
    this.addCommand('G', 'g', Boolean(window.CycloneExtraTilesets));
    this.addCommand('H', 'h', Boolean(window.CycloneExtraTilesets));
  }

  colSpacing() {
    return 6;
  }

  rowSpacing() {
    return 0;
  }

  maxCols() {
    return 8;
  }

  redraw() {
    Window_Selectable.prototype.refresh.call(this);
  }

  // itemRect(index) {
  //   const rect = super.itemRect(index);

  //   if (Graphics.width < 1280) {
  //     rect.width += 3;
  //   }

  //   return rect;
  // }

  // lineHeight() {
  //   if (Graphics.width >= 1280) {
  //     if (CycloneMapEditor.tileDrawWidth < 48) {
  //       return 14;
  //     }

  //     return 36;
  //   }

  //   return 14;
  // }

  playCursorSound() {
  }

  playOkSound() {
  }

  playBuzzerSound() {
  }
}

class WindowCycloneMapEditor extends Window_Command {
  initialize() {
    const x = Graphics.width - CycloneMapEditor.windowWidth;
    const y = SceneManager._scene._mapEditorTabsWindow.y + SceneManager._scene._mapEditorTabsWindow.height;
    const w = CycloneMapEditor.windowWidth;
    const h = Graphics.height - y - SceneManager._scene._mapEditorStatus.height;
    super.initialize(new Rectangle(x, y, w, h));
    this.showBackgroundDimmer();
  }

  onMapTouch(x, y) {

  }

  updateBackOpacity() {
    this.backOpacity = 255;
  }

  _updateCursor() {
    this._cursorSprite.visible = false;
  }

  processCursorMove() {
  }

  processHandling() {
  }

  addTile(tileId) {
    if (!CycloneMapEditor.getTilesetName(tileId)) {
      return;
    }

    if (Tilemap.isAutotile(tileId)) {
      if (Tilemap.isWallSideTile(tileId) || Tilemap.isRoofTile(tileId)) {
        this.addCommand(tileId, 'tile', true, tileId);
      } else if (Tilemap.isWaterfallTile(tileId)) {
        this.addCommand(tileId, 'tile', true, tileId);
      } else {
        this.addCommand(tileId, 'tile', true, tileId + 46);
      }
      return;
    }

    this.addCommand(tileId, 'tile', true, tileId);
  }

  makeManualTilesList() {
    const tileId = this._manualTileSelected;
    let maxShape = 46;

    if (Tilemap.isWallSideTile(tileId) || Tilemap.isRoofTile(tileId)) {
      maxShape = 15;
    } else if (Tilemap.isWaterfallTile(tileId)) {
      maxShape = 3;
    }

    for (let i = 0; i <= maxShape; i++) {
      this.addCommand(tileId + i, 'tile', true, tileId + i);
    }
  }

  makeShadowList() {
    for (let i = 0; i <= 15; i++) {
      this.addCommand(i, 'shadow', true, i);
    }
  }

  makeRegionList() {
    for (let i = 0; i <= 255; i++) {
      this.addCommand(i, 'region', true, i);
    }
  }

  isTileLayer() {
    return CycloneMapEditor.currentLayer === 7 || CycloneMapEditor.currentLayer < 4;
  }

  makeTileList() {
    if (CycloneMapEditor.puzzleMode) {
      this.makePuzzleList();
      return;
    }

    for (let tileId = Tilemap.TILE_ID_A1; tileId < Tilemap.TILE_ID_MAX; tileId += 48) {
      this.addTile(tileId);
    }

    for (let tileId = Tilemap.TILE_ID_A5; tileId < Tilemap.TILE_ID_A5 + 128; tileId++) {
      this.addTile(tileId);
    }

    for (let tileId = Tilemap.TILE_ID_B; tileId < Tilemap.TILE_ID_A5; tileId++) {
      this.addTile(tileId);
    }

    for (let tileId = Tilemap.TILE_ID_A5 + 256; tileId < Tilemap.TILE_ID_A5 + 512; tileId++) {
      this.addTile(tileId);
    }
  }

  makePuzzleList() {
    const min = CycloneMapEditor.getTilesetName(Tilemap.TILE_ID_A1) ? Tilemap.TILE_ID_A1 : Tilemap.TILE_ID_A2;
    const max = CycloneMapEditor.getTilesetName(Tilemap.TILE_ID_A2) ? Tilemap.TILE_ID_A3 : Tilemap.TILE_ID_A2;

    const tileList = [];

    for (let tileId = min; tileId < max; tileId += 48) {
      if (Tilemap.isWaterfallTile(tileId)) {
        continue;
      }
      if (tileId === 2144 || tileId === 2192) {
        continue;
      }

      tileList.push(tileId);
    }

    for (let i = 0; i < tileList.length; i += 4) {
      for (let pieceY = 0; pieceY < 6; pieceY++) {
        for (let idx = 0; idx <= 3; idx++) {
          const tileId = tileList[i + idx];
          if (!tileId) {
            continue;
          }

          for (let pieceX = 0; pieceX < 4; pieceX++) {
            const pieceId = tileId + pieceX + pieceY * 4;
            this.addCommand(pieceId, 'puzzle', true, pieceId);
          }
        }
      }
    }
  }

  makeCommandList() {
    if (CycloneMapEditor.changingTileProps) {
      this.makeTileList();
      return;
    }

    if (this._manualTileSelected) {
      this.makeManualTilesList();
      return;
    }

    if (CycloneMapEditor.currentLayer === 4) {
      this.makeShadowList();
      return;
    }

    if (CycloneMapEditor.currentLayer === 5) {
      this.makeRegionList();
      return;
    }

    if (this.isTileLayer()) {
      this.makeTileList();
      return;
    }

    if (CycloneMapEditor.currentLayer === 8) {
      this.makeCollisionList();
      return;
    }
  }

  makeCollisionList() {
    this.addCommand(0, 'collision', true, 0);
    this.addCommand(1, 'collision', true, 1);
    this.addCommand(2, 'collision', true, 2);

    this.addCommand(17, 'collision', true, 17);
    this.addCommand(18, 'collision', true, 18);
    this.addCommand(19, 'collision', true, 19);

    this.addCommand(14, 'collision', true, 14);
    this.addCommand(20, 'collision', true, 20);
    this.addCommand(16, 'collision', true, 16);

    this.addCommand(11, 'collision', true, 11);
    this.addCommand(12, 'collision', true, 12);
    this.addCommand(13, 'collision', true, 13);

    this.addCommand(22, 'collision', true, 22);
    this.addCommand(26, 'collision', true, 26);
    this.addCommand(24, 'collision', true, 24);
    this.addCommand(28, 'collision', true, 28);

    this.addCommand(4, 'collision', true, 4);
    this.addCommand(5, 'collision', true, 5);
  }

  getTileRow(tileId) {
    const index = this._list.findIndex(item => item?.name === tileId);
    if (index >= 0) {
      return Math.floor(index / this.maxCols());
    }

    return -1;
  }

  jumpToTile(tileId) {
    const row = this.getTileRow(tileId);
    if (row < 0) {
      return false;
    }

    this.setTopRow(row || 0);
    return true;
  }

  ensureSelectionVisible() {
    if (this._selectionIndex < 0 || CycloneMapEditor.currentTileId === undefined) {
      return;
    }

    const row = Math.floor(this._selectionIndex / this.maxCols());
    if (row < this.topRow()) {
      this.setTopRow(Math.min(row, this.maxTopRow()));
    } else if (row > this.topRow() + this.maxPageRows()) {
      this.setTopRow(Math.min(row, this.maxTopRow()));
    }
  }

  redraw() {
    Window_Selectable.prototype.refresh.call(this);

    if (!CycloneMapEditor.changingTileProps) {
      // Force the tilemap cursor to redraw too
      SceneManager._scene._spriteset._mapEditorCursor.updateDrawing();
    }
  }

  colSpacing() {
    if (CycloneMapEditor.currentLayer === Layers.collisions) {
      return 0;
    }

    return Math.floor((this.width - (this.maxCols() * this.itemWidth())) / this.maxCols());
  }

  rowSpacing() {
    return 0;
  }

  maxCols() {
    if (CycloneMapEditor.currentLayer === Layers.collisions) {
      return 3;
    }

    if (CycloneMapEditor.puzzleMode) {
      return 16;
    }

    return 8;
  }

  itemWidth() {
    const w = CycloneMapEditor.tileDrawWidth;
    if (CycloneMapEditor.puzzleMode) {
      return w / 2;
    }

    return w;
  }

  itemHeight() {
    const h = CycloneMapEditor.tileDrawHeight;
    if (CycloneMapEditor.puzzleMode) {
      return h / 2;
    }

    return h;
  }

  drawRegion(index) {
    const rect = this.itemRect(index);
    this.contents.fontSize = Graphics.width < 1280 ? 14 : 18;
    this.contents.drawRegion(index, rect.x, rect.y, rect.width, rect.height, true);
  }

  drawCollision(index) {
    if (index === 0) {
      return;
    }
    const collision = this._list[index].ext ?? index;
    if (collision === 0) {
      return;
    }

    const rect = this.itemRect(index);
    this.contents.drawCollisionType(collision, rect.x, rect.y, rect.width, rect.height);
  }

  drawPuzzle(index) {
    const pieceId = this._list[index].ext;
    if (!pieceId) {
      return;
    }

    const rect = this.itemRect(index);
    this.contents.drawPuzzlePiece(pieceId, rect.x, rect.y, rect.width, rect.height);
  }

  drawShadow(index) {
    const rect = this.itemRect(index);
    const shadowId = index;
    const x = rect.x;
    const y = rect.y;
    const drawWidth = rect.width;
    const drawHeight = rect.height;

    const halfWidth = (drawWidth ?? CycloneMapEditor.tileWidth) / 2;
    const halfHeight = (drawHeight ?? CycloneMapEditor.tileHeight) / 2;

    if (shadowId < 0 || shadowId > 15) {
      return;
    }

    const table = shadowId.toString(2).padZero(4);
    for (let i = 0; i < 4; i++) {
      let color = '#000000';
      if (table[3 - i] !== '1') {
        color = '#FFFFFF99';
      }

      const drawX = x + (i % 2) * halfWidth;
      const drawY = y + Math.floor(i / 2) * halfHeight;

      this.contents.fillRect(drawX, drawY, halfWidth, halfHeight, color);
    }

    const context = this.contents.context;
    context.save();
    context.strokeStyle = '#FF0000';
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x + drawWidth, y);
    context.stroke();
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x, y + drawHeight);
    context.stroke();
  }

  updateOpacityForTile(tileId) {
    if (!CycloneMapEditor.changingTileProps || Input.isPressed('shift')) {
      return this.changePaintOpacity(true);
    }

    return this.changePaintOpacity(false);
  }

  drawItem(index) {
    this.resetTextColor();
    this.changePaintOpacity(this.isCommandEnabled(index));

    const symbol = this.commandSymbol(index);

    if (symbol === 'region') {
      this.drawRegion(index);
      return;
    }

    if (symbol === 'shadow') {
      this.drawShadow(index);
      return;
    }

    if (symbol === 'collision') {
      this.drawCollision(index);
      return;
    }

    if (symbol === 'puzzle') {
      this.drawPuzzle(index);
      return;
    }

    const rect = this.itemRect(index);
    const tileId = this._list[index].ext;

    this.updateOpacityForTile(tileId);

    const bitmap = this.contents.drawTile(tileId, rect.x, rect.y, this.itemWidth(), this.itemHeight());
    if (!bitmap) {
      return;
    }

    if (!bitmap.isReady() && bitmap._loadListeners.length < 2) {
      bitmap.addLoadListener(() => {
        this._needsRedraw = true;
      });
    }

    this.changePaintOpacity(true);
    if (!this._needsRedraw && CycloneMapEditor.changingTileProps) {
      this.drawTileProp(this.commandName(index), rect);
    }
  }

  translucentOpacity() {
    return 90;
  }

  drawTileProp(tileId, rect) {
    if (Input.isPressed('shift')) {
      return;
    }

    switch(CycloneMapEditor.currentTool) {
      case Tools.passage:
        return this.drawTilePassage(tileId, rect);
      case Tools.passage4:
        return this.drawTilePassage4(tileId, rect);
      case Tools.ladder:
        return this.drawTileLadder(tileId, rect);
      case Tools.bush:
        return this.drawTileBush(tileId, rect);
      case Tools.counter:
        return this.drawTileCounter(tileId, rect);
      case Tools.damage:
        return this.drawTileDamage(tileId, rect);
      case Tools.terrain:
        return this.drawTileTerrain(tileId, rect);
    }
  }

  drawTilePassage(tileId, rect) {
    const passageType = $gameMap.checkTileIdPassageType(tileId);
    const context = this.contents.context;

    if (passageType === TilePassageType.blocked) {
      context.strokeStyle = '#000000';
      context.lineWidth = 6;
      context.beginPath();
      context.moveTo(rect.x + 8, rect.y + 8);
      context.lineTo(rect.x + rect.width - 8, rect.y + rect.height - 8);
      context.stroke();

      context.beginPath();
      context.moveTo(rect.x + rect.width - 8, rect.y + 8);
      context.lineTo(rect.x + 8, rect.y + rect.height - 8);
      context.stroke();

      context.strokeStyle = '#FFFFFF';
      context.lineWidth = 4;
      context.beginPath();
      context.moveTo(rect.x + 8, rect.y + 8);
      context.lineTo(rect.x + rect.width - 8, rect.y + rect.height - 8);
      context.stroke();

      context.beginPath();
      context.moveTo(rect.x + rect.width - 8, rect.y + 8);
      context.lineTo(rect.x + 8, rect.y + rect.height - 8);
      context.stroke();

      return;
    }

    if (passageType === TilePassageType.star) {
      let rot = Math.PI / 5;
      const step = Math.PI / 5;
      const outerRadius = Math.floor(Math.min(rect.width, rect.height) / 3);
      const innerRadius = Math.floor(Math.min(rect.width, rect.height) / 6);
      const baseX = Math.floor(rect.x + (rect.width / 2));
      const baseY = Math.floor(rect.y + (rect.height / 2));

      context.beginPath();
      context.moveTo(baseX, baseY - outerRadius);
      for (let i = 0; i < 5; i++) {
        const x = baseX + Math.cos(rot) * outerRadius;
        const y = baseY + Math.sin(rot) * outerRadius;
        context.lineTo(x, y);
        rot += step;

        const inX = baseX + Math.cos(rot) * innerRadius;
        const inY = baseY + Math.sin(rot) * innerRadius;
        context.lineTo(inX, inY);
        rot += step;
      }
      context.lineTo(baseX, baseY - outerRadius);
      context.closePath();
      context.lineWidth = 5;
      context.strokeStyle = '#000000';
      context.stroke();
      context.fillStyle = '#FFFFFF';
      context.fill();
      return;
    }

    context.strokeStyle = '#000000';
    context.lineWidth = 8;
    context.beginPath();
    context.arc(Math.floor(rect.x + rect.width / 2), Math.floor(rect.y + rect.height / 2), Math.floor(Math.min(rect.width - 10, rect.height - 10) / 2), 0, Math.PI * 2, false);
    context.stroke();

    context.strokeStyle = '#FFFFFF';
    context.lineWidth = 4;
    context.beginPath();
    context.arc(Math.floor(rect.x + rect.width / 2), Math.floor(rect.y + rect.height / 2), Math.floor(Math.min(rect.width - 10, rect.height - 10) / 2), 0, Math.PI * 2, false);
    context.stroke();
  }

  drawTilePassage4(tileId, rect) {
    const flag = $gameMap.getTileFlag(tileId);
    const top = $gameMap.getPassageBitType(flag, 8);
    const bottom = $gameMap.getPassageBitType(flag, 2);
    const left = $gameMap.getPassageBitType(flag, 4);
    const right = $gameMap.getPassageBitType(flag, 6);
    const margin = 3;

    const middleX = rect.x + Math.floor(rect.width / 2);
    const middleY = rect.y + Math.floor(rect.height / 2);

    const context = this.contents.context;
    context.lineWidth = 6;
    context.strokeStyle = '#000000';

    const drawArrow = (x, y, x2, y2) => {
      const headLen = Math.floor(rect.width / 5);
      const angle1 = Math.PI / 13;
      const angle2 = Math.atan2(y2 - y, x2 - x);
      const diff1 = angle2 - angle1;
      const diff2 = angle2 + angle1;

      context.beginPath();
      context.moveTo(x, y);
      context.lineTo(x2, y2);
      context.moveTo(x2, y2);
      context.lineTo(x2 - headLen * Math.cos(diff1), y2 - headLen * Math.sin(diff1));

      context.moveTo(x2, y2);
      context.lineTo(x2 - headLen * Math.cos(diff2), y2 - headLen * Math.sin(diff2));
      context.closePath();
      context.stroke();
    };

    const drawArrows = () => {
      if (top) {
        drawArrow(middleX, middleY, middleX, rect.y + margin);
      }

      if (bottom) {
        drawArrow(middleX, middleY, middleX, rect.y + rect.height - margin);
      }

      if (left) {
        drawArrow(middleX, middleY, rect.x + margin, middleY);
      }

      if (right) {
        drawArrow(middleX, middleY, rect.x + rect.width - margin, middleY);
      }
    };

    drawArrows();
    context.lineWidth = 2;
    context.strokeStyle = '#FFFFFF';
    drawArrows();
  }

  drawTileLadder(tileId, rect) {
    if (!$gameMap.tileIdIsLadder(tileId)) {
      return;
    }

    const context = this.contents.context;
    const w = Math.floor(rect.width / 4);
    const h = Math.floor(rect.height / 3);
    const x = Math.floor(rect.x + (rect.width / 2) - (w / 2));
    const y = Math.floor(rect.y + (rect.height / 2) - (w / 2));

    const drawLadder = () => {
      context.beginPath();
      context.moveTo(x, y);
      context.lineTo(x, y + h);

      context.moveTo(x + w, y);
      context.lineTo(x + w, y + h);

      context.moveTo(x, y + Math.floor(h / 3));
      context.lineTo(x + w, y + Math.floor(h / 3));

      context.moveTo(x, y + Math.floor(h / 3) * 2);
      context.lineTo(x + w, y + Math.floor(h / 3) * 2);

      context.closePath();

      context.stroke();
    };

    context.strokeStyle = '#000000';
    context.lineWidth = 6;
    drawLadder();
    context.strokeStyle = '#FFFFFF';
    context.lineWidth = 2;
    drawLadder();
  }

  drawTileBush(tileId, rect) {
    if (!$gameMap.tileIdIsBush(tileId)) {
      return;
    }

    this.contents.drawText('~', rect.x, rect.y, rect.width, rect.height - 8, 'center');
    this.contents.drawText('~', rect.x, rect.y + 8, rect.width, rect.height - 8, 'center');
  }

  drawTileCounter(tileId, rect) {
    if (!$gameMap.tileIdIsCounter(tileId)) {
      return;
    }

    const context = this.contents.context;
    const w = Math.floor(rect.width / 2);
    const h = Math.floor(rect.height / 2);
    const x = rect.x + w;
    const y = rect.y + h / 2;

    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x - w / 2, y + h / 2);
    context.lineTo(x, y + h);
    context.lineTo(x + w / 2, y + h / 2);

    context.closePath();

    context.strokeStyle = '#000000';
    context.lineWidth = 4;
    context.stroke();
    context.fillStyle = '#FFFFFF';
    context.fill();
  }

  drawTileDamage(tileId, rect) {
    if (!$gameMap.tileIdIsDamage(tileId)) {
      return;
    }

    this.contents.drawText('DMG', rect.x, rect.y, rect.width, rect.height, 'center');
  }

  drawTileTerrain(tileId, rect) {
    const tag = $gameMap.tileIdTerrainTag(tileId);
    if (!tag) {
      return;
    }

    this.contents.drawText(tag, rect.x, rect.y, rect.width, rect.height, 'center');
  }

  drawAllItems() {
    super.drawAllItems();
    this.drawSelection();
  }

  drawMessySelection() {
    this._selectionIndex = -1;

    for (let index = 0; index < this._list.length; index++) {
      const item = this._list[index];
      let isSelected = Tilemap.isSameKindTile(item.name, CycloneMapEditor.currentTileId);
      if (isSelected) {
        this._selectionIndex = index;
      } else {
        for (const tileId of CycloneMapEditor.selectedTileList) {
          if (Tilemap.isSameKindTile(tileId, item.name)) {
            isSelected = true;
          }
        }
      }

      if (!isSelected) {
        continue;
      }

      this._drawSelection(index, 1, 1);
    }
  }

  _drawSelection(topIndex, rowDrawCount, colDrawCount) {
    const rect = this.itemRect(topIndex);
    const { x, y } = rect;

    if (CycloneMapEditor.puzzleMode) {
      rowDrawCount = 0.5;
      colDrawCount = 0.5;
    } else if (!this._manualTileSelected && CycloneMapEditor.selectedTileList.length >= 2 && Tilemap.isSameKindTile(CycloneMapEditor.selectedTileList[0], CycloneMapEditor.selectedTileList[1])) {
      rowDrawCount = 1;
      colDrawCount = 1;
    }

    const selectionWidth = CycloneMapEditor.tileDrawWidth * colDrawCount;
    const selectionHeight = CycloneMapEditor.tileDrawHeight * rowDrawCount;

    const context = this.contents.context;
    context.fillStyle = '#000000';

    context.fillRect(x - 1, y - 1, selectionWidth + 2, 4);
    context.fillRect(x - 1, y + selectionHeight - 2, selectionWidth + 2, 4);
    context.fillRect(x - 1, y, 4, selectionHeight);
    context.fillRect(x + selectionWidth - 1, y, 4, selectionHeight);

    context.fillStyle = '#FFFFFF';
    context.fillRect(x + 2, y + 2, selectionWidth - 3, 2);
    context.fillRect(x + 2, y + selectionHeight - 4, selectionWidth - 3, 2);
    context.fillRect(x + 2, y + 2, 2, selectionHeight - 4);
    context.fillRect(x + selectionWidth - 3, y + 2, 2, selectionHeight - 4);
  }

  isSelectedTile(tileId) {
    if (!Tilemap.isSameKindTile(tileId, CycloneMapEditor.currentTileId)) {
      return false;
    }

    if (tileId !== CycloneMapEditor.currentTileId) {
      if (this._manualTileSelected !== undefined) {
        return false;
      }

      if (CycloneMapEditor.puzzleMode) {
        return false;
      }
    }

    return true;
  }

  drawSelection() {
    if (CycloneMapEditor.changingTileProps) {
      return;
    }

    if (CycloneMapEditor.messySelection) {
      this.drawMessySelection();
      return;
    }

    const cols = this.maxCols();
    this._selectionIndex = -1;

    for (let index = 0; index < this._list.length; index++) {
      const item = this._list[index];
      if (!this.isSelectedTile(item.name)) {
        continue;
      }

      this._selectionIndex = index;

      let col = index % cols;
      let row = Math.floor(index / cols);
      let rowCount = CycloneMapEditor.tileRows;
      let colCount = CycloneMapEditor.tileCols;
      let rowDrawCount = CycloneMapEditor.tileRows <= 0 ? Math.abs(CycloneMapEditor.tileRows) + 2 : CycloneMapEditor.tileRows;
      let colDrawCount = CycloneMapEditor.tileCols <= 0 ? Math.abs(CycloneMapEditor.tileCols) + 2 : CycloneMapEditor.tileCols;

      while (rowCount <= 0) {
        rowCount++;
        row--;
      }

      while (colCount <= 0) {
        colCount++;
        col--;
      }

      const topIndex = (row * cols) + col;
      this._drawSelection(topIndex, rowDrawCount, colDrawCount);
      break;
    }
  }

  playCursorSound() {
  }

  playOkSound() {
  }

  playBuzzerSound() {
  }

  selectTileId(tileId, cols = 1, rows = 1) {
    if (CycloneMapEditor.currentTool === 'eraser') {
      CycloneMapEditor.restoreLastDrawingTool();
    }

    CycloneMapEditor.currentTileId = tileId;
    CycloneMapEditor.tileCols = cols ?? 1;
    CycloneMapEditor.tileRows = rows ?? 1;
    CycloneMapEditor.messySelection = false;
    CycloneMapEditor.multiLayerSelection = [];

    const topIndex = this._list.findIndex((item) => item.name === tileId);
    if (topIndex < 0) {
      CycloneMapEditor.currentTileId = undefined;
      CycloneMapEditor.selectedTileList = [];
      this.redraw();
      return;
    }

    CycloneMapEditor.selectedTileList = Array(cols * rows);
    CycloneMapEditor.selectedTileList[0] = CycloneMapEditor.currentTileId;

    const maxCols = this.maxCols();
    const topRow = Math.floor(topIndex / maxCols);
    const leftCol = topIndex % maxCols;

    let selectionIndex = 0;
    for (let y = topRow; y < topRow + CycloneMapEditor.tileRows; y++) {
      for (let x = leftCol; x < leftCol + CycloneMapEditor.tileCols; x++) {
        const newIndex = y * maxCols + x;
        const newTileId = this.commandName(newIndex);
        CycloneMapEditor.selectedTileList[selectionIndex] = newTileId;

        selectionIndex++;
      }
    }

    this.redraw();
  }

  startSelectingTile() {
    if (!this._mouseDown) {
      const index = this.hitIndex();
      if (index < 0) {
        return;
      }
      const tileId = this.commandName(index);
      this.selectTileId(tileId);
      this._mouseDown = true;
    }
  }

  findName(name) {
    return this._list.findIndex(item => item.name === name);
  }

  continueSelectingTile() {
    const index = this.hitIndex();
    const prevCols = CycloneMapEditor.tileCols;
    const prevRows = CycloneMapEditor.tileRows;

    if (index >= 0) {
      let initialIndex = this.findName(CycloneMapEditor.currentTileId);
      if (initialIndex < 0) {
        initialIndex = this._index;
      }

      const initialCol = initialIndex % this.maxCols();
      const initialRow = Math.floor(initialIndex / this.maxCols());
      const newCol = index % this.maxCols();
      const newRow = Math.floor(index / this.maxCols());

      CycloneMapEditor.tileCols = (newCol - initialCol) + 1;
      CycloneMapEditor.tileRows = (newRow - initialRow) + 1;
    }

    if (this._mouseDown) {
      if (!TouchInput.isPressed() || CycloneMapEditor.changingTileProps || CycloneMapEditor.puzzleMode) {
        this.finalizeTileSelection();
      } else if (TouchInput.isMoved()) {
        if (prevCols !== CycloneMapEditor.tileCols || prevRows !== CycloneMapEditor.tileRows) {
          this.redraw();
        }
      }
    }
  }

  finalizeTileSelection() {
    this._mouseDown = false;

    const cols = this.maxCols();
    for (let index = 0; index < this._list.length; index++) {
      const item = this._list[index];
      if (item.name !== CycloneMapEditor.currentTileId) {
        continue;
      }

      let col = index % cols;
      let row = Math.floor(index / cols);
      let rowCount = CycloneMapEditor.tileRows;
      let colCount = CycloneMapEditor.tileCols;
      const newTileRows = CycloneMapEditor.tileRows <= 0 ? Math.abs(CycloneMapEditor.tileRows) + 2 : CycloneMapEditor.tileRows;
      const newTileCols = CycloneMapEditor.tileCols <= 0 ? Math.abs(CycloneMapEditor.tileCols) + 2 : CycloneMapEditor.tileCols;

      while (rowCount <= 0) {
        rowCount++;
        row--;
      }

      while (colCount <= 0) {
        colCount++;
        col--;
      }

      const topIndex = (row * cols) + col;
      if (topIndex >= 0) {
        const newTileId = this.commandName(topIndex);
        if (newTileId || newTileId === 0) {
          this.selectTileId(newTileId, newTileCols, newTileRows);
        } else {
          this.selectTileId(CycloneMapEditor.currentTileId);
        }
      } else {
        this.selectTileId(0);
      }

      break;
    }

    this.redraw();
  }

  activateManualTile() {
    const index = this.hitIndex();
    if (index < 0) {
      return;
    }

    const tileId = this.commandName(index);
    if (Tilemap.isAutotile(tileId)) {
      this._manualTileSelected = tileId;
      this._selectionIndex = -1;
    }
  }

  toggleManualTiles() {
    if (this._manualTileSelected === undefined) {
      this.activateManualTile();
    } else {
      this._manualTileSelected = undefined;
    }

    this.refresh();
    this._mouseDown = false;
    CycloneMapEditor.wasRightButtonDown = CycloneMapEditor.isRightButtonDown;
  }

  processTouchScroll() {
    if (TouchInput.isTriggered() && this.isTouchedInsideFrame()) {
      this.startSelectingTile();
    } else if (CycloneMapEditor.isRightButtonDown && !CycloneMapEditor.wasRightButtonDown && !this._mouseDown && !CycloneMapEditor.changingTileProps) {
      this.toggleManualTiles();
      return;
    }

    if (this._mouseDown) {
      this._mouseMoved = true;
      this.continueSelectingTile();
    }
  }

  update() {
    const shift = Input.isPressed('shift');
    if (shift !== this._oldShift) {
      this._needsRedraw = true;
      this._oldShift = shift;
    }

    if (this._needsRedraw) {
      this._needsRedraw = false;
      this.redraw();
    }

    super.update();
  }
}

let lastDisplayX = 0;
let lastDisplayY = 0;

CycloneMapEditor.patchClass(Scene_Map, $super => class {
  createAllWindows() {
    $super.createAllWindows.call(this);

    this.createMapEditorWindows();
    CycloneMapEditor.clearAllData();
    this.refreshMapEditorWindows();
    CycloneMapEditor.addMenuBar();

    CycloneMapEditor.loadExtraData();
  }

  toggleMapEditor() {
    if (CycloneMapEditor.active && CycloneMapEditor.changeHistory.length > 0) {
      if (confirm('Do you want to save your map before hiding the map editor?')) {
        CycloneMapEditor._doSave();
      }
    }

    CycloneMapEditor.tileWidth = $gameMap.tileWidth();
    CycloneMapEditor.tileHeight = $gameMap.tileHeight();
    CycloneMapEditor.active = !CycloneMapEditor.active;

    this.refreshMapEditorWindows();
    this._spriteset._mapEditorCursor.updateDrawing();
    this._spriteset.updatePosition();
  }

  createMapEditorWindows() {
    CycloneMapEditor.tileWidth = $gameMap.tileWidth();
    CycloneMapEditor.tileHeight = $gameMap.tileHeight();

    const neededWidth = CycloneMapEditor.tileDrawWidth * 8 + 24;
    if (neededWidth > CycloneMapEditor.windowWidth) {
      CycloneMapEditor.windowWidth = neededWidth;
    }

    this._mapEditorGrid = new WindowCycloneGrid();
    this.addChild(this._mapEditorGrid);
    this._mapEditorGrid.hide();
    this._mapEditorGrid.deactivate();

    this._mapEditorCommands = new WindowCycloneMapEditorCommands();
    this.addChild(this._mapEditorCommands);
    this._mapEditorCommands.hide();
    this._mapEditorCommands.deactivate();

    this._mapEditorLayerListWindow = new WindowCycloneMapEditorLayerList();
    this.addChild(this._mapEditorLayerListWindow);
    this._mapEditorLayerListWindow.hide();
    this._mapEditorLayerListWindow.deactivate();

    this._mapEditorTabsWindow = new WindowCycloneMapEditorTabs();
    this.addChild(this._mapEditorTabsWindow);
    this._mapEditorTabsWindow.hide();
    this._mapEditorTabsWindow.deactivate();

    this._mapEditorStatus = new WindowCycloneMapEditorStatus();
    this.addChild(this._mapEditorStatus);
    this._mapEditorStatus.hide();
    this._mapEditorStatus.deactivate();

    this._mapEditorWindow = new WindowCycloneMapEditor();
    this.addChild(this._mapEditorWindow);
    this._mapEditorWindow.hide();
    this._mapEditorWindow.deactivate();
  }

  refreshMapEditorWindows() {
    const { active } = CycloneMapEditor;

    this._mapEditorGrid.visible = active;
    this._mapEditorCommands.visible = active;
    this._mapEditorLayerListWindow.visible = active;
    this._mapEditorWindow.visible = active;
    this._mapEditorStatus.visible = active;
    this._mapEditorTabsWindow.visible = active;

    this._mapEditorCommands.active = active;
    this._mapEditorLayerListWindow.active = active;
    this._mapEditorWindow.active = active;
    this._mapEditorTabsWindow.active = active;

    this._mapEditorCommands.refresh();
    this._mapEditorLayerListWindow.refresh();
    this._mapEditorTabsWindow.refresh();
    this._mapEditorWindow.refresh();
    this._mapEditorGrid.refresh();
    this._mapEditorStatus.refresh();

    if (active) {
      this._spriteset._mapEditorCursor.updateDrawing();
    }
    CycloneMapEditor.refreshMenuVisibility();
  }

  redrawMap() {
    this._spriteset._tilemap.refresh();
  }

  processMapTouch() {
    if (!CycloneMapEditor.active) {
      $super.processMapTouch.call(this);
      return;
    }

    this._touchCount = 0;
    if (TouchInput.isPressed() && !this.isAnyButtonPressed()) {
      this.onMapTouch();
    }
  }

  onMapTouch() {
    if (!CycloneMapEditor.active) {
      $super.onMapTouch.call(this);
      return;
    }
  }

  editorX() {
    return Graphics.width - CycloneMapEditor.windowWidth;
  }

  canUpdateMouse() {
    return CycloneMapEditor.active && this._mapEditorWindow && this._mapEditorLayerListWindow;
  }

  updateMenuTouch(x, y, pressed) {
    if (!pressed) {
      return;
    }

    if (x > this._mapEditorLayerListWindow.x && x < this._mapEditorLayerListWindow.x + this._mapEditorLayerListWindow.width) {
      if (y < this._mapEditorLayerListWindow.height + this._mapEditorLayerListWindow.y) {
        if (!CycloneMapEditor.wasPressing) {
          this._mapEditorLayerListWindow.onMapTouch(x - this._mapEditorLayerListWindow.x, y - this._mapEditorLayerListWindow.y);
          CycloneMapEditor.wasPressing = true;
        }

        return true;
      }
    }

    if (x > this._mapEditorWindow.x && x < this._mapEditorWindow.x + this._mapEditorWindow.width) {
      this._mapEditorWindow.onMapTouch(x - this._mapEditorWindow.x, y - this._mapEditorWindow.y);
      return true;
    }
  }

  updateRightMouse() {
    if (!this.canUpdateMouse()) {
      CycloneMapEditor.isRightButtonDown = false;
      CycloneMapEditor.wasRightButtonDown = false;
      return;
    }

    if (!CycloneMapEditor.isRightButtonDown && !CycloneMapEditor.wasRightButtonDown) {
      return;
    }

    const { x, y } = TouchInput;
    if (this.updateMenuTouch(x, y, CycloneMapEditor.isRightButtonDown)) {
      return;
    }

    const mapX = CycloneMapEditor.canvasToMapX(x);
    const mapY = CycloneMapEditor.canvasToMapY(y);

    if (mapX >= 0 && mapY >= 0) {
      CycloneMapEditor.updateRightTouch(mapX, mapY);
    }

    CycloneMapEditor.wasRightButtonDown = CycloneMapEditor.isRightButtonDown;
  }

  updateDisplayPositionData() {
    if (lastDisplayX === $gameMap._displayX && lastDisplayY === $gameMap._displayY) {
      return;
    }

    const xDiff = $gameMap._displayX - lastDisplayX;
    const yDiff = $gameMap._displayY - lastDisplayY;

    if (xDiff > 10 || yDiff > 10) {
      // If the difference is too big, then we don't update
      return;
    }

    if ((CycloneMapEditor.rectangleWidth > 0 || CycloneMapEditor.rectangleBackWidth > 0) && (CycloneMapEditor.rectangleHeight > 0 || CycloneMapEditor.rectangleBackHeight > 0)) {
      CycloneMapEditor.rectangleStartMouseX += xDiff * CycloneMapEditor.tileWidth;
      CycloneMapEditor.rectangleStartMouseY += yDiff * CycloneMapEditor.tileHeight;
    }
  }

  getSelectionTileAt(x, y) {
    if (x <= this._mapEditorWindow.x || x >= this._mapEditorWindow.x + this._mapEditorWindow.width) {
      return CycloneMapEditor.currentTileId;
    }

    if (y >= this._mapEditorWindow.height + this._mapEditorWindow.y) {
      return CycloneMapEditor.currentTileId;
    }

    const index = this._mapEditorWindow.hitIndex();
    if (index >= 0) {
      return this._mapEditorWindow.commandName(index);
    }
  }

  updateMouse() {
    if (!this.canUpdateMouse()) {
      CycloneMapEditor.wasPressing = false;
      return;
    }

    this.updateDisplayPositionData();
    lastDisplayX = $gameMap._displayX;
    lastDisplayY = $gameMap._displayY;

    const pressed = TouchInput.isPressed();
    const { x, y } = TouchInput;
    const mapX = CycloneMapEditor.canvasToMapX(x);
    const mapY = CycloneMapEditor.canvasToMapY(y);
    const fx = Math.floor(mapX);
    const fy = Math.floor(mapY);

    const tile1 = CycloneMapEditor.getCurrentTileAtPosition(fx, fy, 0, true);
    const tile2 = CycloneMapEditor.getCurrentTileAtPosition(fx, fy, 1, true);
    const tile3 = CycloneMapEditor.getCurrentTileAtPosition(fx, fy, 2, true);
    const tile4 = CycloneMapEditor.getCurrentTileAtPosition(fx, fy, 3, true);
    const tileId = this.getSelectionTileAt(x, y) ?? '';

    CycloneMapEditor.updateStatus({
      mapX,
      mapY,
      tile1,
      tile2,
      tile3,
      tile4,
      tileId,
    });

    if (!pressed && !CycloneMapEditor.wasPressing) {
      return;
    }

    if (this.updateMenuTouch(x, y, pressed)) {
      return;
    }

    let minX = 0;
    let minY = 0;

    if (CycloneMapEditor.isLayerVisible(Layers.blend) && [Tools.pencil, Tools.eraser].includes(CycloneMapEditor.currentTool)) {
      minX--;
      minY--;
    }

    if (mapX >= minX && mapY >= minY) {
      if (Input.isPressed('control') && !CycloneMapEditor.wasPressing) {
        CycloneMapEditor.selectHigherLayer(mapX, mapY);
      } else {
        CycloneMapEditor.updateCurrentToolTouch(mapX, mapY);
      }
    }

    CycloneMapEditor.wasPressing = pressed;
  }


  isMenuEnabled() {
    if (CycloneMapEditor.active) {
      return false;
    }

    return $super.isMenuEnabled.call(this);
  }

  update() {
    $super.update.call(this);

    if (!CycloneMapEditor.active) {
      return;
    }

    if (CycloneMapEditor.wasPressing || CycloneMapEditor.wasRightButtonDown) {
      if (this._isControlPressed !== Input.isPressed('control') || this._isShiftPressed !== Input.isPressed('shift')) {
        this.updateMouse();
      }
    }

    this._isControlPressed = Input.isPressed('control');
    this._isShiftPressed = Input.isPressed('shift');
  }
});

CycloneMapEditor.patchClass(SceneManager, $super => class {
  static onSceneTerminate() {
    CycloneMapEditor.refreshMenuVisibility();
  }
});

class SpriteMapEditorCursor extends Sprite {
  initialize() {
    super.initialize(new Bitmap(CycloneMapEditor.tileWidth, CycloneMapEditor.tileHeight));
  }

  update() {
    super.update();
    if (this.visible !== CycloneMapEditor.active) {
      this.visible = CycloneMapEditor.active;
    }

    if (CycloneMapEditor.active) {
      this.updatePosition();
    }
  }

  updateDrawing() {
    if (CycloneMapEditor.isRightButtonDown) {
      return this.updateRectangle();
    }

    switch (CycloneMapEditor.currentTool) {
      case Tools.fill:
        return this.updateTiles();
      case Tools.pencil:
        if (CycloneMapEditor.isLayerVisible(Layers.blend)) {
          return this.updateBrush();
        }
        return this.updateTiles();
      case Tools.eraser:
        if (CycloneMapEditor.isLayerVisible(Layers.blend)) {
          return this.updateBrush();
        }
        return this.updateEraser();
      case Tools.rectangle:
        if ((!CycloneMapEditor.rectangleWidth && !CycloneMapEditor.rectangleBackWidth) || (!CycloneMapEditor.rectangleHeight && !CycloneMapEditor.rectangleBackHeight)) {
          this.updateTiles();
          return;
        }

        return this.updateRectangle();
      default:
        return this.updateOther();
    }
  }

  getNewBitmapWidth() {
    return ((CycloneMapEditor.tileWidth * (CycloneMapEditor.rectangleWidth || (CycloneMapEditor.rectangleBackWidth + 1))) || 1) / CycloneMapEditor.getGridRatio();
  }

  getNewBitmapHeight() {
    return ((CycloneMapEditor.tileHeight * (CycloneMapEditor.rectangleHeight || (CycloneMapEditor.rectangleBackHeight + 1))) || 1) / CycloneMapEditor.getGridRatio();
  }

  updateRectangle() {
    const width = this.getNewBitmapWidth();
    const height = this.getNewBitmapHeight();

    if (width !== this.bitmap.width || height !== this.bitmap.height) {
      this.bitmap = new Bitmap(width, height);
    } else {
      this.bitmap.clear();
    }

    const fillColor = CycloneMapEditor.isRightButtonDown ? '#00000033' : '#00FF0033';

    if (CycloneMapEditor.currentLayer === 5) {
      this.drawTiles();
    }

    if (width > 8 && height > 8) {
      this.bitmap.fillRect(0, 0, width, 4, '#000000');
      this.bitmap.fillRect(0, height - 4, width, 4, '#000000');
      this.bitmap.fillRect(0, 0, 4, height, '#000000');
      this.bitmap.fillRect(width - 4, 0, 4, height, '#000000');

      this.bitmap.fillRect(2, 2, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, height - 4, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, 2, 2, height - 4, '#FFFFFF');
      this.bitmap.fillRect(width - 4, 2, 2, height - 4, '#FFFFFF');

      this.bitmap.fillRect(4, 4, width - 8, height - 8, fillColor);
    } else if (width > 0 && height > 0) {
      this.bitmap.fillRect(0, 0, width, height, fillColor);
    }
  }

  updateBrush() {
    const size = Input.isPressed('shift') ? 4 : 2;

    const width = $gameMap.tileWidth() / size;
    const height = $gameMap.tileHeight() / size;

    if (width !== this.bitmap.width || height !== this.bitmap.height) {
      this.bitmap = new Bitmap(width, height);
    } else {
      this.bitmap.clear();
    }

    const fillColor = CycloneMapEditor.currentTool === Tools.eraser ? '#99000099' : '#00999999';
    this.bitmap.drawCircle(width / 2, height / 2, Math.min(width, height) / 2, fillColor);
  }

  updateEraser() {
    const width = this.getNewBitmapWidth();
    const height = this.getNewBitmapHeight();

    if (width !== this.bitmap.width || height !== this.bitmap.height) {
      this.bitmap = new Bitmap(width, height);
    } else {
      this.bitmap.clear();
    }

    if (width > 8 && height > 8) {
      this.bitmap.fillRect(0, 0, width, 4, '#000000');
      this.bitmap.fillRect(0, height - 4, width, 4, '#000000');
      this.bitmap.fillRect(0, 0, 4, height, '#000000');
      this.bitmap.fillRect(width - 4, 0, 4, height, '#000000');

      this.bitmap.fillRect(2, 2, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, height - 4, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, 2, 2, height - 4, '#FFFFFF');
      this.bitmap.fillRect(width - 4, 2, 2, height - 4, '#FFFFFF');

      this.bitmap.fillRect(4, 4, width - 8, height - 8, '#FF000033');
    } else if (width > 0 && height > 0) {
      this.bitmap.fillRect(0, 0, width, height, '#FF000033');
    }
  }

  drawMultiLayerTiles() {
    for (let z = 0; z < CycloneMapEditor.multiLayerSelection.length; z++) {
      let column = 0;
      let row = 0;

      for (const tileId of CycloneMapEditor.multiLayerSelection[z]) {
        if (column >= CycloneMapEditor.tileCols) {
          column = 0;
          row++;
        }

        const x = column * CycloneMapEditor.tileWidth;
        const y = row * CycloneMapEditor.tileHeight;

        this.bitmap.drawTile(tileId, x, y);
        column++;
      }
    }
  }

  drawTiles() {
    if (CycloneMapEditor.currentLayer === Layers.auto && CycloneMapEditor.multiLayerSelection.length) {
      this.drawMultiLayerTiles();
      return;
    }

    let column = 0;
    let row = 0;

    for (const tileId of CycloneMapEditor.selectedTileList) {
      if (column >= CycloneMapEditor.tileCols) {
        column = 0;
        row++;
      }

      const x = column * CycloneMapEditor.tileWidth;
      const y = row * CycloneMapEditor.tileHeight;

      if (CycloneMapEditor.currentLayer === 5) {
        this.bitmap.drawRegion(tileId, x, y);
      } else if (CycloneMapEditor.currentLayer === 4) {
        this.bitmap.drawShadow(tileId, x, y);
      } else if (CycloneMapEditor.currentLayer === 8) {
        this.drawCollision(tileId, x, y);
      } else if (CycloneMapEditor.puzzleMode) {
        this.bitmap.drawPuzzlePiece(tileId, x, y, CycloneMapEditor.tileWidth / 2, CycloneMapEditor.tileHeight / 2);
      } else {
        this.bitmap.drawTile(tileId, x, y);
      }
      column++;
    }
  }

  drawCollision(tileId, x, y) {
    const drawWidth = CycloneMapEditor.tileWidth;
    const drawHeight = CycloneMapEditor.tileHeight;

    if (tileId === 0) {
      return;
    }

    this.bitmap.drawCollisionType(tileId, x, y, drawWidth, drawHeight);
  }

  updateOther() {
    this.bitmap.clear();
  }

  updateTiles() {
    const gridRatio = CycloneMapEditor.getGridRatio();

    const width = CycloneMapEditor.tileWidth * CycloneMapEditor.tileCols / gridRatio;
    const height = CycloneMapEditor.tileHeight * CycloneMapEditor.tileRows / gridRatio;

    if (width !== this.bitmap.width || height !== this.bitmap.height) {
      this.bitmap = new Bitmap(width, height);
    } else {
      this.bitmap.clear();
    }

    this.drawTiles();

    if (width > 8 && height > 8) {
      this.bitmap.fillRect(0, 0, width, 4, '#000000');
      this.bitmap.fillRect(0, height - 4, width, 4, '#000000');
      this.bitmap.fillRect(0, 0, 4, height, '#000000');
      this.bitmap.fillRect(width - 4, 0, 4, height, '#000000');

      this.bitmap.fillRect(2, 2, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, height - 4, width - 4, 2, '#FFFFFF');
      this.bitmap.fillRect(2, 2, 2, height - 4, '#FFFFFF');
      this.bitmap.fillRect(width - 4, 2, 2, height - 4, '#FFFFFF');
    }
  }

  getCursorTileX() {
    if (CycloneMapEditor.currentTool === 'rectangle' || CycloneMapEditor.currentTool === 'eraser' || CycloneMapEditor.isRightButtonDown) {
      if (CycloneMapEditor.rectangleWidth > 0) {
        return CycloneMapEditor.rectangleStartX;
      }
      if (CycloneMapEditor.rectangleBackWidth > 0) {
        return CycloneMapEditor.rectangleStartX - CycloneMapEditor.rectangleBackWidth / CycloneMapEditor.getGridRatio();
      }
    }

    if (SceneManager._scene._mapEditorWindow) {
      if (TouchInput.x >= SceneManager._scene._mapEditorWindow.x) {
        return CycloneMapEditor.canvasToMapX(SceneManager._scene._mapEditorWindow.x);
      }
    }

    return CycloneMapEditor.canvasToMapX(TouchInput.x);
  }

  getCursorTileY() {
    if (CycloneMapEditor.currentTool === 'rectangle' || CycloneMapEditor.currentTool === 'eraser' || CycloneMapEditor.isRightButtonDown) {
      if (CycloneMapEditor.rectangleHeight > 0) {
        return CycloneMapEditor.rectangleStartY;
      }
      if (CycloneMapEditor.rectangleBackHeight > 0) {
        return CycloneMapEditor.rectangleStartY - CycloneMapEditor.rectangleBackHeight / CycloneMapEditor.getGridRatio();
      }
    }

    return CycloneMapEditor.canvasToMapY(TouchInput.y);
  }

  updatePosition() {
    if (!CycloneMapEditor.active) {
      return;
    }

    const tileX = this.getCursorTileX();
    const tileY = this.getCursorTileY();

    let offsetX = 0;
    let offsetY = 0;

    if (CycloneMapEditor.isLayerVisible(Layers.blend) && [Tools.eraser, Tools.pencil].includes(CycloneMapEditor.currentTool)) {
      offsetX -= Math.floor(this.bitmap.width / 2);
      offsetY -= Math.floor(this.bitmap.height / 2);
    }

    this.x = Math.floor($gameMap.adjustX(tileX) * CycloneMapEditor.tileWidth) + offsetX;
    this.y = Math.floor($gameMap.adjustY(tileY) * CycloneMapEditor.tileHeight) + offsetY;
  }
}

CycloneMapEditor.patchClass(Spriteset_Map, $super => class {
  initialize() {
    $super.initialize.call(this);

    this.createMapEditorCursor();
  }

  createMapEditorCursor() {
    this._mapEditorCursor = new SpriteMapEditorCursor();
    this.addChild(this._mapEditorCursor);
  }

  forceBlenderRefresh(hardRefresh = false) {
    if (!window.CycloneMagic) {
      return;
    }

    if (hardRefresh) {
      for (const sprite of this._blenderTileSprites) {
        sprite.parent.removeChild(sprite);
        sprite.destroy();
      }
      this._blenderTileSprites = [];
      this.createBlenderTiles();
      return;
    }

    const magicTiles = $gameMap.magicTiles();
    for (const tile of magicTiles) {
      let found = false;

      for (const sprite of this._blenderTileSprites) {
        if (sprite._mapX !== tile.x || sprite._mapY !== tile.y) {
          continue;
        }

        found = true;
        if (!window.CycloneMagic.isSpriteCached(sprite.spriteId)) {
          sprite._bitmap = null;
        }
        break;
      }

      if (!found) {
        const newSprite = new window.CycloneMagic.SpriteBlenderTile(tile.tiles, tile.x, tile.y, 1, 1);
        this._blenderTileSprites.push(newSprite);
        this._tilemap.addChild(newSprite);
      }
    }

    this._tilemap.refresh();
  }

  // updatePosition() {
  //   if (!CycloneMapEditor.active) {
  //     return $super.updatePosition.call(this);
  //   }

  //   const scale = $gameMap.zoom ?? { x : 1, y : 1};
  //   const screen = $gameScreen;
  //   this.x = -($gameMap.zoom?.x ?? 1) * (scale.x - 1);
  //   this.y = -($gameMap.zoom?.y ?? 1) * (scale.y - 1);
  //   this.x = this.x + screen.shake();

  //   if (this.scale.x !== scale.x || this.scale.y !== scale.y) {
  //     const sw = Graphics.width / scale.x + this._tilemap._margin * 2;
  //     const sh = Graphics.height / scale.y + this._tilemap._margin * 2;

  //     if (sw !== this._tilemap.width || sh !== this._tilemap.height) {
  //       this._tilemap.width = sw;
  //       this._tilemap.height = sh;
  //       this._tilemap.refresh();
  //     }

  //     this.scale = new PIXI.Point(scale.x, scale.y);
  //     this._weather.scale = new PIXI.Point(1.0 / scale.x,  1.0 / scale.y);
  //     this._parallax.move(this._parallax.x, this._parallax.y, Graphics.width / scale.x, Graphics.height / scale.y);
  //   }

  // }
});

CycloneMapEditor.patchClass(Tilemap, $super => class {
  _readMapData(x, y, z) {
    if (!$gameMap.isValid(x, y)) {
      return 0;
    }

    if (z <= 4 && !CycloneMapEditor.layerVisibility[z]) {
      return 0;
    }

    const tileIndex = CycloneMapEditor.tileIndex(x, y, z);
    if (CycloneMapEditor.previewChanges?.[tileIndex] !== undefined) {
      return CycloneMapEditor.previewChanges[tileIndex];
    }

    return $super._readMapData.call(this, x, y, z);
  }

  canUpdateAnimationCount() {
    if (CycloneMapEditor.active && CycloneMapEditor.isLayerVisible(Layers.blend) && TouchInput.isPressed()) {
      return false;
    }

    return true;
  }

  update() {
    // Prevent the water animation while modifying blending
    if (!this.canUpdateAnimationCount()) {
      this.animationCount--;
    }

    $super.update.call(this);
  }
});

CycloneMapEditor.patchClass(TouchInput, $super => class {
  static _onLeftButtonDown(event) {
    $super._onLeftButtonDown.call(this, event);

    if (SceneManager._scene instanceof Scene_Map) {
      SceneManager._scene.updateMouse();
    }
  }

  static _onMouseMove(event) {
    $super._onMouseMove.call(this, event);

    if (SceneManager._scene instanceof Scene_Map) {
      SceneManager._scene.updateMouse();
      SceneManager._scene.updateRightMouse();
    }
  }

  static _onMouseUp(event) {
    $super._onMouseUp.call(this, event);

    if (SceneManager._scene instanceof Scene_Map) {
      if (event.button === 0) {
        SceneManager._scene.updateMouse();
      } else if (event.button === 2) {
        CycloneMapEditor.isRightButtonDown = false;
        SceneManager._scene.updateRightMouse();
      }
    }
  }

  static _onRightButtonDown(event) {
    $super._onRightButtonDown.call(this, event);

    if (SceneManager._scene instanceof Scene_Map) {
      CycloneMapEditor.isRightButtonDown = true;
      SceneManager._scene.updateRightMouse();
    }
  }
});

CycloneMapEditor.patchClass(Sprite_Character, $super => class {
  update(...args) {
    if (CycloneMapEditor.active) {
      this.visible = CycloneMapEditor.isLayerVisible(Layers.events);
      if (!this.visible) {
        return;
      }
    }
    $super.update.call(this, ...args);
  }
});

CycloneMapEditor.patchClass(Scene_Boot, $super => class {
  resizeScreen() {
    if (Utils.isNwjs() && $dataSystem.advanced.screenWidth < 1280) {
      const minWidth = Math.min(1920, screen.availWidth - (window.outerWidth - window.innerWidth));
      const minHeight = Math.min(1080, screen.availHeight - (window.outerHeight - window.innerHeight));

      const { screenWidth, screenHeight, uiAreaWidth, uiAreaHeight } = $dataSystem.advanced;

      if (screenWidth < minWidth) {
        $dataSystem.advanced.screenWidth = minWidth;
      }
      if (uiAreaWidth < minWidth) {
        $dataSystem.advanced.uiAreaWidth = minWidth;
      }

      if (screenHeight < minHeight) {
        $dataSystem.advanced.screenHeight = minHeight;
      }
      if (uiAreaHeight < minHeight) {
        $dataSystem.advanced.uiAreaHeight = minHeight;
      }
    }

    $super.resizeScreen.call(this);
  }

});

let delaysTried = 0;

const addFilter = () => {
  if (!window.CycloneMagic) {
    if (delaysTried > 10) {
      return;
    }

    setTimeout(addFilter, 100);
    delaysTried++;
    return;
  }

  CycloneMapEditor.patchClass(window.CycloneMagic.SpriteBlenderTile, $super => class {
    update() {
      $super.update.call(this);
      this.visible = CycloneMapEditor.isLayerVisible(1);
    }
  });
};

setTimeout(addFilter, 200);
})();