//=============================================================================
// Cyclone Engine - Movement
//=============================================================================

/*:
 * @target MZ
 * @plugindesc Adds new movement features to the game v1.01.02
 *
 * <pluginName:CycloneMovement>
 * @author Hudell
 * @url https://makerdevs.com/plugin/cyclone-movement
 *
 * @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'
 * Movement                                                          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.
 * ===========================================================================
 * Change Log
 * ===========================================================================
 * 2020-09-18 - Version 1.01.01
 *   * Added .terrainTag method to character class.
 *
 * 2020-09-18 - Version 1.01.00
 *   * Fixed some incompatibilities with VisuMZ's EventMove Core.
 *   * Fixed directional passability tests when Pixel Movement is disabled.
 *   * New settings to control the sidestep feature.
 * 2020-09-14 - Version 1.00.00
 * ===========================================================================
 * @param stepCount
 * @text Steps per Tile
 * @desc How many steps the player will need to take to move an entire tile?
 * @type select
 * @default 1
 * @option 4
 * @option 2
 * @option 1
 *
 * @param collisionStepCount
 * @text Collision Blocks per Tile
 * @desc You can customize the map collision with the Cyclone Map Editor plugin
 * @type select
 * @default 1
 * @option 4
 * @option 2
 * @option 1
 *
 * @param followerStepsBehind
 * @text Follower Distance
 * @desc How many steps behind should the followers be? Min = 1 step, Max = 1 tile
 * @type number
 * @min 1
 * @max 12
 * @default 3
 *
 * @param triggerAllEvents
 * @text Trigger All Events
 * @desc If true, the player may trigger multiple events when you press a button if there are more than one event in front of you
 * @type boolean
 * @on Trigger
 * @off Skip
 * @default false
 *
 * @param ignoreEmptyEvents
 * @text Ignore Empty Events
 * @desc if true, the game won't try to trigger events that have no commands
 * @type boolean
 * @on Ignore
 * @off Don't Ignore
 * @default true
 *
 * @param autoLeaveVehicles
 * @text Leave Vehicles Automatically
 * @desc If true, the player will leave boats and ships automatically when they reach land
 * @type boolean
 * @on Leave
 * @off Don't Leave
 * @default false
 *
 * @param diagonalPathfinding
 * @text Diagonal Pathfinding
 * @type boolean
 * @on Enable
 * @off Disable
 * @desc
 * @default true
 *
 * @param disableMouseMovement
 * @text Disable Mouse Movement
 * @type boolean
 * @on Disable
 * @off Don't Disable
 * @desc
 * @default false
 *
 * @param maxOffset
 * @text Max Slide Distance
 * @type number
 * @desc How many tiles should the player be able to sidestep when trying to avoid map obstacles?
 * @default 0.75
 * @decimals 2
 *
 * @param sidestepEvents
 * @text Sidestep Events?
 * @type boolean
 * @desc Should the player also sidestep to avoid events?
 * @default false
 *
 *
 * @param playerHitbox
 * @text Player Hitbox
 * @type struct<Hitbox>
 * @default {"x":"6","y":"24","width":"36","height":"18"}
 **/
/*~struct~Hitbox:
 * @param x
 * @type number
 * @default 0
 * @desc The hitbox X offset
 *
 * @param y
 * @type number
 * @default 0
 * @desc The hitbox Y offset
 *
 * @param width
 * @type number
 * @default 48
 * @desc The hitbox width
 *
 * @param height
 * @type number
 * @default 48
 * @desc The hitbox height
 */

(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({value:t,defaultValue:e,type:r}){if(r.endsWith("[]"))return this.parseArrayParam({value:t,type:r});if(r.startsWith("struct<"))return this.parseStructParam({value:t,defaultValue:e,type:r});if(void 0===t)return e;switch(r){case"int":return this.getIntParam({value:t,defaultValue:e});case"float":return this.getFloatParam({value:t,defaultValue:e});case"boolean":return "boolean"==typeof t?t:this.isTrue(String(t).trim());default:return t}}static getPluginParam(t){return this.params.get(t)}static defaultValueForType(t){switch(t){case"int":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({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 parseStructParam({value:t,defaultValue:e,type:r}){let s;if(t)try{s=JSON.parse(t);}catch(e){console.error("Cyclone Engine failed to parse param structure: ",t),console.error(e);}s||(s=JSON.parse(e));const a=this.getRegexMatch(r,/struct<(.*)>/i,1);if(!a)return console.error(`Unknown plugin param type: ${r}`),s;const n=this.structs.get(a);if(!n)return console.error(`Unknown param structure type: ${a}`),s;for(const t in n){if(!n.hasOwnProperty(t))continue;let e=n[t];"string"==typeof e&&(e={type:e,defaultValue:this.defaultValueForType(e)}),s[t]=this.getParam({value:s[t],defaultValue:e.defaultValue,type:e.type});}return s}static parseList(t,e){let r=t;r.startsWith("[")&&(r=r.substr(1)),r.endsWith("]")&&(r=r.substr(0,r.length-1));const s=r.split(",");return e?s.map((t=>e(t))):s}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){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;

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 t,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(t=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);

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

const addPixelMovementToClass = (classRef) => {
  CycloneMovement.patchClass(classRef, $super => class {
    get left() {
      return this._x + this.hitboxX;
    }
    get right() {
      return this._x + this.hitboxX + this.width;
    }
    get top() {
      return this._y + this.hitboxY;
    }
    get bottom() {
      return this._y + this.hitboxY + this.height;
    }

    get firstY() {
      return this.firstYAt(this._y);
    }
    get lastY() {
      return this.lastYAt(this._y);
    }

    get firstX() {
      return this.firstXAt(this._x);
    }
    get lastX() {
      return this.lastXAt(this._x);
    }
    get middleX() {
      return Math.round(this.left + (this.getWidth() / 2));
    }
    get middleY() {
      return Math.round(this.top + (this.getHeight() / 2));
    }

    getWidth() {
      return 1;
    }
    getHeight() {
      return 1;
    }

    getHitboxX() {
      return 0;
    }

    getHitboxY() {
      return 0;
    }
    firstXAt(x) {
      return Math.floor(x + this.hitboxX);
    }
    lastXAt(x) {
      const right = x + this.hitboxX + this.width;
      if (CycloneMovement.isRoundNumber(right)) {
        return right - 1;
      }

      return Math.floor(right);
    }
    firstYAt(y) {
      return Math.floor(y + this.hitboxY);
    }
    lastYAt(y) {
      const bottom = y + this.hitboxY + this.height;
      if (CycloneMovement.isRoundNumber(bottom)) {
        return bottom - 1;
      }

      return Math.floor(bottom);
    }

    firstCollisionXAt(x) {
      const count = CycloneMovement.collisionStepCount;
      return Math.floor((x + this.hitboxX) * count) / count;
    }

    lastCollisionXAt(x) {
      const count = CycloneMovement.collisionStepCount;
      const right = (x + this.hitboxX + this.width) * count;
      if (CycloneMovement.isRoundNumber(right)) {
        return (right - 1) / count;
      }

      return Math.floor(right) / count;
    }

    firstCollisionYAt(y) {
      const count = CycloneMovement.collisionStepCount;
      return Math.floor((y + this.hitboxY) * count) / count;
    }

    lastCollisionYAt(y) {
      const count = CycloneMovement.collisionStepCount;
      const bottom = (y + this.hitboxY + this.height) * count;
      if (CycloneMovement.isRoundNumber(bottom)) {
        return (bottom - 1) / count;
      }

      return Math.floor(bottom) / count;
    }

    shouldSkipExtraPassabilityTests() {
      return false;
    }

    updateHitbox() {
      this.width = this.getWidth();
      this.height = this.getHeight();
      this.hitboxX = this.getHitboxX();
      this.hitboxY = this.getHitboxY();
    }

    update(...args) {
      this.updateHitbox();
      this.updateIsMoving();
      $super.update.call(this, ...args);
    }

    shouldPassThrough() {
      if (this.isThrough() || this.isDebugThrough()) {
        return true;
      }

      return false;
    }

    canPass(x, y, d) {
      if (CycloneMovement.isDiagonal(d)) {
        const d1 = CycloneMovement.getFirstDirection(d);
        const d2 = CycloneMovement.getAlternativeDirection(d1, d);
        return this.canPassDiagonally(x, y, d2, d1);
      }

      const x2 = CycloneMovement.roundXWithDirection(x, d);
      const y2 = CycloneMovement.roundYWithDirection(y, d);

      this._blockingReason = 'free';
      if (!$gameMap.isValid(x2, y2)) {
        this._blockingReason = 'invalid';
        return false;
      }

      if (this.shouldPassThrough()) {
        return true;
      }

      if (!this.isMapPassable(x, y, d)) {
        this._blockingReason = 'tile';
        return false;
      }

      if (this.shouldSkipExtraPassabilityTests()) {
        return true;
      }

      if (!this.isMapPassable(x2, y2, this.reverseDir(d))) {
        this._blockingReason = 'tileReverse';
        return false;
      }

      if (this.isCollidedWithCharacters(x2, y2)) {
        this._blockingReason = 'characters';
        return false;
      }

      return true;
    }

    canPassDiagonally(x, y, horz, vert) {
      const y2 = CycloneMovement.roundYWithDirection(y, vert);
      const x2 = CycloneMovement.roundXWithDirection(x, horz);

      this._blockingReason = 'free';
      if (!$gameMap.isValid(x2, y2)) {
        this._blockingReason = 'invalid';
        return false;
      }

      if (this.shouldPassThrough()) {
        return true;
      }

      // Can move vertically at the current position?
      if (!this.isMapPassable(x, y, vert)) {
        this._blockingReason = 'tile';
        return false;
      }

      // Can move horizontally at the current position?
      if (!this.isMapPassable(x, y, horz)) {
        this._blockingReason = 'tile';
        return false;
      }

      // Can move horizontally at the new Y position?
      if (!this.isMapPassable(x, y2, horz)) {
        this._blockingReason = 'tile';
        return false;
      }

      // Can move vertically at the new X position?
      if (!this.isMapPassable(x2, y, vert)) {
        this._blockingReason = 'tile';
        return false;
      }

      if (this.shouldSkipExtraPassabilityTests()) {
        return true;
      }

      const reverseHorz = this.reverseDir(horz);
      const reverseVert = this.reverseDir(vert);

      // Can move vertically at the current position? (reverse)
      if (!this.isMapPassable(x2, y2, reverseVert)) {
        this._blockingReason = 'tileReverse';
        return false;
      }

      // Can move horizontally at the current position? (reverse)
      if (!this.isMapPassable(x2, y2, reverseHorz)) {
        this._blockingReason = 'tileReverse';
        return false;
      }

      // Can move horizontally at the new Y position? (reverse)
      const y3 = CycloneMovement.roundYWithDirection(y2, vert);
      if (!this.isMapPassable(x2, y3, reverseHorz)) {
        this._blockingReason = 'tileReverse';
        return false;
      }

      // Can move vertically at the new X position? (reverse)
      const x3 = CycloneMovement.roundXWithDirection(x2, horz);
      if (!this.isMapPassable(x3, y2, reverseVert)) {
        this._blockingReason = 'tileReverse';
        return false;
      }

      // Finally, check if the destination position doesn't have an event on it
      if (this.isCollidedWithCharacters(x2, y2)) {
        this._blockingReason = 'characters';
        return false;
      }

      return true;
    }

    isMapPassable(x, y, d) {
      if (CycloneMovement.goesUp(d)) {
        if (!this.canGoUp(x, y)) {
          return false;
        }
      } else if (CycloneMovement.goesDown(d)) {
        if (!this.canGoDown(x, y)) {
          return false;
        }
      }

      if (CycloneMovement.goesLeft(d)) {
        if (!this.canGoLeft(x, y)) {
          return false;
        }
      } else if (CycloneMovement.goesRight(d)) {
        if (!this.canGoRight(x, y)) {
          return false;
        }
      }

      return true;
    }

    canGoLeft(x, y) {
      const left = x + this.hitboxX;
      const firstY = this.firstCollisionYAt(y);
      const lastY = this.lastCollisionYAt(y);
      const destinationLeft = left - CycloneMovement.stepSize;

      // Run the collision check for every Y tile the character is touching
      for (let newY = firstY; newY <= lastY; newY += CycloneMovement.collisionSize) {
        const checkUp = newY > firstY;
        const checkDown = newY < lastY;

        if (this.checkLeftPassage(left, newY, destinationLeft, checkUp, checkDown) === false) {
          return false;
        }
      }

      return true;
    }

    isPositionPassable(x, y, d) {
      return CycloneMovement.isPositionPassable(x, y, d);
    }

    checkLeftPassage(left, y, destinationLeft, checkUp = false, checkDown = false) {
      const count = CycloneMovement.collisionStepCount;
      const leftFloor = Math.floor(left * count) / count;
      const destinationLeftFloor = Math.floor(destinationLeft * count) / count;

      // if we're entering a new left tile
      if (destinationLeftFloor < leftFloor) {
        // check if the current left-most tile allows moving left
        if (!this.isPositionPassable(leftFloor, y, 4)) {
          return false;
        }

        // and check if the new left-most tile allows moving right
        if (!this.isPositionPassable(destinationLeftFloor, y, 6)) {
          return false;
        }

        if (this.checkVerticalPassage(destinationLeftFloor, y, checkUp, checkDown) === false) {
          return false;
        }
      }

      return null;
    }

    canGoRight(x, y) {
      const right = x + this.hitboxX + this.width;
      const firstY = this.firstCollisionYAt(y);
      const lastY = this.lastCollisionYAt(y);
      const destinationRight = right + CycloneMovement.stepSize;

      for (let newY = firstY; newY <= lastY; newY += CycloneMovement.collisionSize) {
        const checkUp = newY > firstY;
        const checkDown = newY < lastY;

        if (this.checkRightPassage(right, newY, destinationRight, checkUp, checkDown) === false) {
          return false;
        }
      }

      return true;
    }

    checkRightPassage(right, y, destinationRight, checkUp = false, checkDown = false) {
      const lastXDestination = this.lastCollisionXAt((destinationRight - this.width - this.hitboxX));
      const lastX = this.lastCollisionXAt((right - this.width - this.hitboxX));

      // if we're entering a new right tile
      if (lastXDestination > lastX) {
        // check if the current right-most tile allows moving right
        if (!this.isPositionPassable(lastX, y, 6)) {
          return false;
        }

        // and check if the new right-most tile allows moving left
        if (!this.isPositionPassable(lastXDestination, y, 4)) {
          return false;
        }

        if (this.checkVerticalPassage(lastXDestination, y, checkUp, checkDown) === false) {
          return false;
        }
      }

      return null;
    }

    canGoUp(x, y) {
      const top = y + this.hitboxY;
      const firstX = this.firstCollisionXAt(x);
      const lastX = this.lastCollisionXAt(x);
      const destinationTop = (top - CycloneMovement.stepSize);

      for (let newX = firstX; newX <= lastX; newX += CycloneMovement.collisionSize) {
        const checkLeft = newX > firstX;
        const checkRight = newX < lastX;

        if (this.checkUpPassage(newX, top, destinationTop, checkLeft, checkRight) === false) {
          return false;
        }
      }

      return true;
    }

    checkVerticalPassage(x, y, checkUp, checkDown) {
      if (checkUp && !this.isPositionPassable(x, y, 8)) {
        return false;
      }
      if (checkDown && !this.isPositionPassable(x, y, 2)) {
        return false;
      }
    }

    checkHorizontalPassage(x, y, checkLeft, checkRight) {
      if (checkLeft && !this.isPositionPassable(x, y, 4)) {
        return false;
      }
      if (checkRight && !this.isPositionPassable(x, y, 6)) {
        return false;
      }
    }

    checkUpPassage(x, top, destinationTop, checkLeft = false, checkRight = false) {
      const count = CycloneMovement.collisionStepCount;
      const topFloor = Math.floor(top * count) / count;
      const destinationTopFloor = Math.floor(destinationTop * count) / count;

      // if we're entering a new top tile
      if (destinationTopFloor < topFloor) {
        // check if the current top tile allows moving up
        if (!this.isPositionPassable(x, topFloor, 8)) {
          return false;
        }

        // and check if the new top tile allows moving down
        if (!this.isPositionPassable(x, destinationTopFloor, 2)) {
          return false;
        }

        if (this.checkHorizontalPassage(x, destinationTopFloor, checkLeft, checkRight) === false) {
          return false;
        }
      }

      return null;
    }

    canGoDown(x, y) {
      const bottom = y + this.hitboxY + this.height;
      const firstX = this.firstCollisionXAt(x);
      const lastX = this.lastCollisionXAt(x);
      const destinationBottom = (bottom + CycloneMovement.stepSize);

      for (let newX = firstX; newX <= lastX; newX += CycloneMovement.collisionSize) {
        const checkLeft = newX > firstX;
        const checkRight = newX < lastX;

        if (this.checkDownPassage(newX, bottom, destinationBottom, checkLeft, checkRight) === false) {
          return false;
        }
      }

      return true;
    }

    checkDownPassage(x, bottom, destinationBottom, checkLeft = false, checkRight = false) {
      const lastYDestination = this.lastCollisionYAt((destinationBottom - this.height - this.hitboxY));
      const lastY = this.lastCollisionYAt((bottom - this.height - this.hitboxY));

      // if we're entering a new bottom tile
      if (lastYDestination > lastY) {
        // check if the current bottom tile allows moving down
        if (!this.isPositionPassable(x, lastY, 2)) {
          return false;
        }

        // and check if the new bottom tile allows moving up
        if (!this.isPositionPassable(x, lastYDestination, 8)) {
          return false;
        }

        if (this.checkHorizontalPassage(x, lastYDestination, checkLeft, checkRight) === false) {
          return false;
        }
      }

      return null;
    }

    addNewPosition(x, y) {
      if (this instanceof Game_Vehicle) {
        return;
      }

      if (CycloneMovement.followerStepsBehind <= 1) {
        return;
      }

      if (!this._positionHistory) {
        this._positionHistory = [];
      }

      this._positionHistory.push({x, y});

      if (this._positionHistory.length > CycloneMovement.followerStepsBehind + 1) {
        this._positionHistory.shift();
      }
    }

    getPositionToFollow() {
      if (!this._positionHistory) {
        this._positionHistory = [];
        if (CycloneMovement.followerStepsBehind > 1) {
          return false;
        }
      }

      if (!$gamePlayer.areFollowersGathering()) {
        if (this._positionHistory.length < CycloneMovement.followerStepsBehind - 1) {
          return false;
        }
      }

      if (this._positionHistory.length === 0) {
        return {
          x : this._x,
          y : this._y,
        };
      }

      return this._positionHistory.shift();
    }

    locate(...args) {
      this._positionHistory = [];
      $super.locate.call(this, ...args);
    }

    _moveStraight(d) {
      this.setMovementSuccess(this.canPass(this._x, this._y, d));
      if (this.isMovementSucceeded()) {
        this.setDirection(d);

        const { stepCount } = CycloneMovement;

        this._x = Math.round(CycloneMovement.roundXWithDirection(this._x, d) * stepCount) / stepCount;
        this._y = Math.round(CycloneMovement.roundYWithDirection(this._y, d) * stepCount) / stepCount;
        this._realX = CycloneMovement.xWithDirection(this._x, this.reverseDir(d));
        this._realY = CycloneMovement.yWithDirection(this._y, this.reverseDir(d));
        this.updateIsMoving();

        this.updateAnimationCount();
        this.addNewPosition(this._x, this._y);
        this.increaseSteps();
      } else {
        this.setDirection(d);
        this.checkEventTriggerTouchFront(d);
      }
    }

    _moveDiagonally(horz, vert) {
      this.setMovementSuccess(this.canPassDiagonally(this._x, this._y, horz, vert));

      if (this.isMovementSucceeded()) {
        this._x = CycloneMovement.roundXWithDirection(this._x, horz);
        this._y = CycloneMovement.roundYWithDirection(this._y, vert);
        this._realX = CycloneMovement.xWithDirection(this._x, this.reverseDir(horz));
        this._realY = CycloneMovement.yWithDirection(this._y, this.reverseDir(vert));
        this.updateIsMoving();

        this.updateAnimationCount();
        this.addNewPosition(this._x, this._y);
        this.increaseSteps();
      }

      if (this._direction === this.reverseDir(horz)) {
        this.setDirection(horz);
      }
      if (this._direction === this.reverseDir(vert)) {
        this.setDirection(vert);
      }
    }

    moveStraight(d) {
      return this._moveStraight(d);
    }

    moveDiagonally(horz, vert) {
      return this._moveDiagonally(horz, vert);
    }

    isTouchingPos(x, y) {
      if (!(x >= this.firstX && x <= this.lastX)) {
        return false;
      }

      if (!(y >= this.firstY && y <= this.lastY)) {
        return false;
      }

      return true;
    }

    isTouchingRect(left, top, right, bottom) {
      return this.wouldTouchRectAt(left, top, right, bottom, this._x, this._y);
    }

    isTouchingCharacter(character) {
      return this.wouldTouchCharacterAt(character, this._x, this._y);
    }

    wouldTouchRectAt(left, top, right, bottom, x, y) {
      const firstX = this.firstCollisionXAt(x);
      const lastX = this.lastCollisionXAt(x);
      const firstY = this.firstCollisionYAt(y);
      const lastY = this.lastCollisionYAt(y);

      if (right < firstX) {
        return false;
      }

      if (left > lastX) {
        return false;
      }

      if (bottom < firstY) {
        return false;
      }

      if (top > lastY) {
        return false;
      }

      return true;
    }

    wouldTouchCharacterAt(character, x, y) {
      const {
        left = character.x,
        right = character.x + 1,
        top = character.y,
        bottom = character.y + 1,
      } = character;

      return this.wouldTouchRectAt(left, top, right, bottom, x, y);
    }

    pos(x, y) {
      if (this._x === x && this._y === y) {
        return true;
      }

      return this.isTouchingPos(x, y);
    }

    iterateNewTiles(callback) {
      const firstX = Math.floor(this.firstCollisionXAt(this.x));
      const firstRealX = Math.floor(this.firstCollisionXAt(this._realX));
      const lastX = Math.floor(this.lastCollisionXAt(this.x));
      const lastRealX = Math.floor(this.lastCollisionXAt(this._realX));
      const firstY = Math.floor(this.firstCollisionYAt(this.y));
      const firstRealY = Math.floor(this.firstCollisionYAt(this._realY));
      const lastY = Math.floor(this.lastCollisionYAt(this.y));
      const lastRealY = Math.floor(this.lastCollisionYAt(this._realY));

      const left = Math.min(firstX, firstRealX);
      const right = Math.max(lastX, lastRealX);
      const top = Math.min(firstY, firstRealY);
      const bottom = Math.max(lastY, lastRealY);

      for (let x = left; x <= right; x++) {
        const isNewX = x < firstRealX || x > lastRealX;

        for (let y = top; y <= bottom; y++) {
          const isNewY = y < firstRealY || y > lastRealY;

          if (!isNewX && !isNewY) {
            continue;
          }

          if (callback.call(this, x, y) === true) {
            return true;
          }
        }
      }

      return false;
    }

    iterateTiles(callback) {
      return this.runForAllTiles(this._x, this._y, callback);
    }

    runForAllTiles(x, y, callback) {
      const firstX = Math.floor(this.firstCollisionXAt(x));
      const lastX = Math.floor(this.lastCollisionXAt(x));
      const firstY = Math.floor(this.firstCollisionYAt(y));
      const lastY = Math.floor(this.lastCollisionYAt(y));

      for (let newX = firstX; newX <= lastX; newX++) {
        for (let newY = firstY; newY <= lastY; newY++) {
          if (callback.call(this, newX, newY) === true) {
            return true;
          }
        }
      }

      return false;
    }

    iteratePositions(callback) {
      return this.runForAllPositions(this._x, this._y, callback);
    }

    runForAllPositions(x, y, callback) {
      const firstX = this.firstCollisionXAt(x);
      const lastX = this.lastCollisionXAt(x);
      const firstY = this.firstCollisionYAt(y);
      const lastY = this.lastCollisionYAt(y);

      for (let newX = firstX; newX <= lastX; newX += CycloneMovement.collisionSize) {
        for (let newY = firstY; newY <= lastY; newY += CycloneMovement.collisionSize) {
          if (callback.call(this, newX, newY) === true) {
            return true;
          }
        }
      }

      return false;
    }

    isCollidedWithEvents(x, y) {
      return this.runForAllTiles(x, y, function(blockX, blockY) {
        //If the player is "inside" it, then this event won't be considered,
        //because if it did, the player would be locked on it
        //this shouldn't be possible on normal conditions.

        if (this.isTouchingPos(blockX, blockY)) {
          return false;
        }

        return $gameMap.eventsXyNt(blockX, blockY).some(event  => event.isNormalPriority());
      });
    }

    isOnBush() {
      let bushCount = 0;
      let nonBushCount = 0;

      this.iteratePositions((x, y) => {
        if ($gameMap.isBush(Math.floor(x), Math.floor(y))) {
          bushCount++;
        } else {
          nonBushCount++;
        }
      });

      return bushCount > nonBushCount;
    }

    isOnLadder() {
      let ladderCount = 0;
      let nonLadderCount = 0;

      this.iteratePositions((x, y) => {
        if ($gameMap.isLadder(Math.floor(x), Math.floor(y))) {
          ladderCount++;
        } else {
          nonLadderCount++;
        }
      });

      return ladderCount > nonLadderCount;
    }

    isCollidedWithVehicles() {
      return false;
    }

    chasePosition(x, y) {
      const sx = this.deltaXFrom(x);
      const sy = this.deltaYFrom(y);

      const sxAbs = Math.abs(sx);
      const syAbs = Math.abs(sy);
      const { stepSize } = CycloneMovement;

      if (sxAbs >= stepSize && syAbs >= stepSize) {
        this.moveDiagonally(sx > 0 ? 4 : 6, sy > 0 ? 8 : 2);
      } else if (sxAbs >= stepSize) {
        this.moveStraight(sx > 0 ? 4 : 6);
      } else if (syAbs >= stepSize) {
        this.moveStraight(sy > 0 ? 8 : 2);
      } else if (sxAbs > 0 || syAbs > 0) {
        this._x = x;
        this._y = y;
      }

      this.setMoveSpeed($gamePlayer.realMoveSpeed());
    }

    setDirection(d) {
      if (CycloneMovement.goesUp(d)) {
        $super.setDirection.call(this, 8);
      } else if (CycloneMovement.goesDown(d)) {
        $super.setDirection.call(this, 2);
      } else if (CycloneMovement.goesLeft(d)) {
        $super.setDirection.call(this, 4);
      } else if (CycloneMovement.goesRight(d)) {
        $super.setDirection.call(this, 6);
      }
    }

    _findNextBestNode(best, x1, y1, direction, closedList, goalX, goalY, current, openList, nodeList) {
      const x2 = CycloneMovement.roundXWithDirection(x1, direction);
      const y2 = CycloneMovement.roundYWithDirection(y1, direction);

      const pos2 = y2 * $gameMap.width() + x2;

      if (closedList.contains(pos2)) {
        return best;
      }

      if (Math.floor(x1) === goalX && Math.floor(y1) === goalY) {
        return false;
      }

      if (!this.canPass(x1, y1, direction)) {
        return best;
      }

      let g2 = current.g + CycloneMovement.stepSize;
      if (CycloneMovement.isDiagonal(direction)) {
        g2 += CycloneMovement.stepSize;
      }

      const index2 = openList.indexOf(pos2);
      if (index2 < 0 || g2 < nodeList[index2].g) {
        let neighbor;
        if (index2 >= 0) {
          neighbor = nodeList[index2];
        } else {
          neighbor = {};
          nodeList.push(neighbor);
          openList.push(pos2);
        }

        neighbor.parent = current;
        neighbor.x = x2;
        neighbor.y = y2;
        neighbor.g = g2;
        neighbor.f = g2 + $gameMap.distance(x2, y2, goalX, goalY);

        if (!best || neighbor.f - neighbor.g < best.f - best.g) {
          return neighbor;
        }
      }

      return best;
    }

    getDirectionNode(start, goalX, goalY) {
      const searchLimit = this.searchLimit();
      const mapWidth = $gameMap.width();
      const nodeList = [];
      const openList = [];
      const closedList = [];
      let best = start;

      if (this.x === goalX && this.y === goalY) {
        return undefined;
      }

      nodeList.push(start);
      openList.push(start.y * mapWidth + start.x);

      while (nodeList.length) {
        let bestIndex = 0;
        for (let i = 0; i < nodeList.length; i++) {
          if (nodeList[i].f < nodeList[bestIndex].f) {
            bestIndex = i;
          }
        }

        const current = nodeList[bestIndex];
        const x1 = current.x;
        const y1 = current.y;
        const pos1 = y1 * mapWidth + x1;
        const g1 = current.g;

        nodeList.splice(bestIndex, 1);
        openList.splice(openList.indexOf(pos1), 1);
        closedList.push(pos1);

        if (this._positionMatch(current.x, current.y, goalX, goalY)) {
          best = current;
          break;
        }

        if (g1 >= searchLimit) {
          continue;
        }

        for (let d = 1; d <= 9; d++) {
          if (d === 5) {
            continue;
          }

          if (!CycloneMovement.diagonalPathfinding && CycloneMovement.isDiagonal(d)) {
            continue;
          }

          const nextBest = this._findNextBestNode(best, x1, y1, d, closedList, goalX, goalY, current, openList, nodeList);
          if (nextBest === false) {
            break;
          }

          best = nextBest;
        }
      }

      return best;
    }

    clearCachedNode() {
      this.setCachedNode();
    }

    setCachedNode(node, goalX, goalY) {
      this._cachedNode = node;
      this._cachedGoalX = goalX;
      this._cachedGoalY = goalY;

      this._cacheTTL = 2 * CycloneMovement.collisionStepCount * CycloneMovement.stepCount - 1;
    }

    _getDirectionFromDeltas(deltaX, deltaY) {
      if (CycloneMovement.diagonalPathfinding) {
        if (deltaY > 0) {
          if (deltaX > 0) {
            return 3;
          }
          if (deltaX < 0) {
            return 1;
          }
        } else if (deltaY < 0) {
          if (deltaX < 0) {
            return 7;
          }
          if (deltaX > 0) {
            return 9;
          }
        }
      }

      if (deltaY > 0) {
        return 2;
      }

      if (deltaX < 0) {
        return 4;
      }

      if (deltaX > 0) {
        return 6;
      }

      if (deltaY < 0) {
        return 8;
      }

      return 0;
    }

    _returnDirection(direction, goalX, goalY, canRetry) {
      if (direction) {
        if (!this.canPass(this._x, this._y, direction)) {
          this.clearCachedNode();
          if (canRetry) {
            return this.findDirectionTo(goalX, goalY);
          }

          this._direction = direction;
          return 0;
        }
      }

      return direction;
    }

    _positionMatch(x1, y1, x2, y2) {
      return x1 === x2 && y1 === y2;
    }

    _nodeIsNotNextStep(node, x, y) {
      if (!node.parent) {
        return false;
      }

      return !this._positionMatch(node.parent.x, node.parent.y, x, y);
    }

    _findDirectionTo(goalX, goalY) {
      let node = this._cachedNode;
      const start = {};
      start.parent = null;
      start.x = this.x;
      start.y = this.y;
      start.g = 0;
      start.f = $gameMap.distance(start.x, start.y, goalX, goalY);

      let canRetry = true;
      if (node === undefined) {
        node = this.getDirectionNode(start, goalX, goalY);
        this.setCachedNode(node, goalX, goalY);
        if (node === undefined) {
          return 0;
        }
        canRetry = false;
      }

      if (node.x !== start.x || node.y !== start.y) {
        while (this._nodeIsNotNextStep(node, start.x, start.y)) {
          node = node.parent;
        }

        if (!node.parent) {
          this.clearCachedNode();
          if (canRetry) {
            node = this.getDirectionNode(start, goalX, goalY);
            this.setCachedNode(node, goalX, goalY);

            if (node === undefined) {
              return 0;
            }
          }
        }
      }

      const deltaX1 = $gameMap.deltaX(node.x, start.x);
      const deltaY1 = $gameMap.deltaY(node.y, start.y);
      const deltaD = this._getDirectionFromDeltas(deltaX1, deltaY1);

      if (deltaD) {
        return deltaD;
      }

      const deltaX2 = this.deltaXFrom(goalX);
      const deltaY2 = this.deltaYFrom(goalY);
      let direction = 0;

      if (Math.abs(deltaX2) > Math.abs(deltaY2)) {
        direction = deltaX2 > 0 ? 4 : 6;
      } else if (deltaY2 !== 0) {
        direction = deltaY2 > 0 ? 8 : 2;
      }

      return this._returnDirection(direction, goalX, goalY, canRetry);
    }

    findDirectionTo(goalX, goalY) {
      if (this.x === goalX && this.y === goalY) {
        return 0;
      }

      if (this._cachedNode) {
        if (this._cachedGoalX !== goalX || this._cachedGoalY !== goalY) {
          this.clearCachedNode();
        } else if (this._cacheTTL > 0) {
          this._cacheTTL--;
        } else {
          this.clearCachedNode();
        }
      }

      return this._findDirectionTo(goalX, goalY);
    }

    originalIsMoving() {
      return $super.isMoving.call(this);
    }

    isMoving() {
      if (this.distancePerFrame() >= CycloneMovement.stepSize) {
        return this._isMoving;
      }

      return this.originalIsMoving();
    }

    updateIsMoving() {
      this._isMoving = this.originalIsMoving();
    }

    setPosition(...args) {
      $super.setPosition.call(this, ...args);
      this.updateIsMoving();
    }

    copyPosition(character) {
      $super.copyPosition.call(this, character);
      this.updateIsMoving();
    }

    updateJump() {
      $super.updateJump.call(this);
      this.updateIsMoving();
    }

    jump(...args) {
      $super.jump.call(this, ...args);
      this.updateIsMoving();
    }

    terrainTag() {
      return $gameMap.terrainTag(this.middleX, this.middleY);
    }
  });
};

let currentMapCollisionTable = false;

class CycloneMovement$1 extends CyclonePlugin {
  static register() {
    super.initialize('CycloneMovement');

    this.structs.set('CycloneHitbox', {
      x: {
        type: 'int',
        defaultValue: 0,
      },
      y: {
        type: 'int',
        defaultValue: 0,
      },
      width: {
        type: 'int',
        defaultValue: 48,
      },
      height: {
        type: 'int',
        defaultValue: 48,
      },
    });

    super.register({
      stepCount: {
        type: 'int',
        defaultValue: 1,
      },
      collisionStepCount: {
        type: 'int',
        defaultValue: 1,
      },
      followerStepsBehind: {
        type: 'int',
        defaultValue: 3,
      },
      triggerAllEvents: 'boolean',
      ignoreEmptyEvents: {
        type: 'boolean',
        defaultValue: true,
      },
      autoLeaveVehicles: 'boolean',
      diagonalPathfinding: {
        type: 'boolean',
        defaultValue: true,
      },
      disableMouseMovement: 'boolean',
      maxOffset: {
        type: 'float',
        defaultValue: 0.75,
      },
      sidestepEvents: 'boolean',
      playerHitbox: {
        type: 'struct<CycloneHitbox>',
        defaultValue: '{"x":6,"y":24,"width":36,"height":18}',
      },
    });

    this.stepCount = [1, 2, 4].includes(this.params.stepCount) ? this.params.stepCount : 1;
    this.collisionStepCount = Math.min(this.stepCount, [1, 2, 4].includes(this.params.collisionStepCount) ? this.params.collisionStepCount : 1);
    this.stepSize = 1 / this.stepCount;
    this.collisionSize = 1 / this.collisionStepCount;
    this.followerStepsBehind = Number(this.params.followerStepsBehind || 1).clamp(1, 12);
    this.triggerAllEvents = this.params.triggerAllEvents === true;
    this.autoLeaveVehicles = this.params.autoLeaveVehicles === true;
    this.ignoreEmptyEvents = this.params.ignoreEmptyEvents !== false;
    this.diagonalPathfinding = this.params.diagonalPathfinding !== false;
    this.disableMouseMovement = this.params.disableMouseMovement === true;

    addPixelMovementToClass(Game_Player);
    addPixelMovementToClass(Game_Follower);
  }

  static get currentMapCollisionTable() {
    return currentMapCollisionTable;
  }

  static isRoundNumber(n) {
    return Math.floor(n) === n;
  }

  static goesLeft(d) {
    return DirectionHelper.goesLeft(d);
  }

  static goesRight(d) {
    return DirectionHelper.goesRight(d);
  }

  static goesUp(d) {
    return DirectionHelper.goesUp(d);
  }

  static goesDown(d) {
    return DirectionHelper.goesDown(d);
  }

  static isDiagonal(d) {
    return DirectionHelper.isDiagonal(d);
  }

  static isVertical(d) {
    return DirectionHelper.isVertical(d);
  }

  static isHorizontal(d) {
    return DirectionHelper.isHorizontal(d);
  }

  static shareADirection(dir1, dir2) {
    return DirectionHelper.shareADirection(dir1, dir2);
  }

  static getFirstDirection(diagonalDirection) {
    return DirectionHelper.getFirstDirection(diagonalDirection);
  }

  static getAlternativeDirection(direction, diagonalDirection) {
    return DirectionHelper.getAlternativeDirection(direction, diagonalDirection);
  }

  static xWithDirection(x, d, stepSize = undefined) {
    stepSize = stepSize ?? this.stepSize;

    if (this.goesLeft(d)) {
      return x - stepSize;
    }

    if (this.goesRight(d)) {
      return x + stepSize;
    }

    return x;
  }

  static yWithDirection(y, d, stepSize = undefined) {
    stepSize = stepSize ?? this.stepSize;

    if (this.goesDown(d)) {
      return y + stepSize;
    }

    if (this.goesUp(d)) {
      return y - stepSize;
    }

    return y;
  }

  static roundXWithDirection(x, d, stepSize = undefined) {
    return $gameMap.roundX(this.xWithDirection(x, d, stepSize));
  }

  static roundYWithDirection(y, d, stepSize = undefined) {
    return $gameMap.roundY(this.yWithDirection(y, d, stepSize));
  }

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

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

  static parseCollisionData(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 data from CycloneMapEditor event.');
      console.log(json);
      console.log(e);
      return;
    }

    return data;
  }

  static setupCollision() {
    if (!$gameMap || !$gameMap._loaded){
      return;
    }

    const stepCount = this.collisionStepCount;
    currentMapCollisionTable = new Array($dataMap.width * $dataMap.height * stepCount * stepCount);
    this.loadDefaultCollisionTable();
    this.loadCustomCollision();
  }

  static loadDefaultCollisionTable() {
    const { width, height } = $dataMap;
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const downPassable = $gameMap.isPassable(x, y, 2);
        const leftPassable = $gameMap.isPassable(x, y, 4);
        const rightPassable = $gameMap.isPassable(x, y, 6);
        const upPassable = $gameMap.isPassable(x, y, 8);

        this.applyTileCollision(x, y, downPassable, leftPassable, rightPassable, upPassable);
      }
    }
  }

  static setBlockCollision(x, y, collision) {
    const index = this.collisionIndex(x, y);
    currentMapCollisionTable[index] = collision;
  }

  static applySingleTileCollision(x, y, blockUp, blockDown, blockLeft, blockRight) {
    const collision = this._mergeCustomCollisionValues(blockUp, blockDown, blockLeft, blockRight) || 1;
    this.setBlockCollision(x, y, collision);
  }

  static applyFullTileCollision(x, y, collision) {
    const size = this.collisionSize;
    for (let subX = x; subX < x + 1; subX += size) {
      for (let subY = y; subY < y + 1; subY += size) {
        this.setBlockCollision(subX, subY, collision);
      }
    }
  }

  static applyTileDirectionCollision(x, y, direction, collision) {
    const size = this.collisionSize;

    if (direction === 2 || direction === 8) {
      const subY = y + (direction === 2 ? 1 - size : 0);
      for (let subX = x; subX < x + 1; subX += size) {
        this.setBlockCollision(subX, subY, collision);
      }
      return;
    }

    const subX = x + (direction === 6 ? 1 - size : 0);
    for (let subY = y; subY < y + 1; subY += size) {
      this.setBlockCollision(subX, subY, collision);
    }
  }

  static applyTileCornerCollision(x, y, horz, vert, collision) {
    const size = this.collisionSize;

    const blockY = vert === 2 ? y + 1 - size : y;
    const blockX = horz === 6 ? x + 1 - size : x;

    this.setBlockCollision(blockX, blockY, collision);
  }

  static collisionIndex(x, y, useEditorStepCount = false) {
    const stepCount = useEditorStepCount ? 4 : this.collisionStepCount;

    const intX = Math.floor(x * stepCount);
    const intY = Math.floor(y * stepCount);
    const height = $gameMap.height() * stepCount;
    const width = $gameMap.width() * stepCount;
    return (intY % height) * width + (intX % width);
  }

  // eslint-disable-next-line complexity
  static _mergeCustomCollisionValues(blockUp, blockDown, blockLeft, blockRight) {
    if (blockLeft && blockRight && blockDown && blockUp) {
      return 20;
    }

    if (blockUp) {
      if (blockLeft) {
        if (blockRight) {
          return 22;
        }
        return 17;
      }
      if (blockRight) {
        if (blockDown) {
          return 24;
        }
        return 19;
      }

      if (blockDown) {
        return 4;
      }

      return 18;
    }

    if (blockDown) {
      if (blockLeft) {
        if (blockRight) {
          return 28;
        }

        return 11;
      }
      if (blockRight) {
        return 13;
      }
      return 12;
    }

    if (blockLeft) {
      if (blockRight) {
        return 5;
      }
      return 14;
    }

    if (blockRight) {
      return 16;
    }
  }

  // If the collision is using less than 4 blocks per tile, then merge the sub-blocks into bigger blocks.
  // This is needed for the directional passabilities to work properly
  // eslint-disable-next-line complexity
  static _mergeCustomCollisions(x, y, data) {
    const radix = data.radix ?? 10;
    if (this.collisionStepCount === 4) {
      const editorIndex = this.collisionIndex(x, y);
      return parseInt(data.collision[editorIndex], radix) || 0;
    }

    // merge every sub-block into a single one
    const diffCount = Math.floor(4 / this.collisionStepCount);
    const diffSize = 1 / diffCount;
    let result = false;
    let blockUp = false;
    let blockDown = false;
    let blockRight = false;
    let blockLeft = false;

    for (let blockX = 0; blockX < diffCount; blockX++) {
      for (let blockY = 0; blockY < diffCount; blockY++) {
        const editorIndex = this.collisionIndex(x + blockX * diffSize, y + blockY * diffSize, true);
        const customCollision = parseInt(data.collision[editorIndex], radix) || 0;

        if (customCollision === 2) {
          return 2;
        }

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

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

        if (goesUp && blockY === 0) {
          blockUp = true;
        }

        if (goesDown && blockY === diffCount - 1) {
          blockDown = true;
        }

        if (goesLeft && blockX === 0) {
          blockLeft = true;
        }

        if (goesRight && blockX === diffCount - 1) {
          blockRight = true;
        }
      }
    }

    return this._mergeCustomCollisionValues(blockUp, blockDown, blockLeft, blockRight) || result || 0;
  }

  static setupCustomCollision(compressedData) {
    const data = CycloneMovement$1.parseCollisionData(compressedData);
    if (!data || !data.collision) {
      return;
    }

    const increment = this.collisionSize;

    for (let x = 0; x < $dataMap.width; x += increment) {
      for (let y = 0; y < $dataMap.height; y += increment) {
        const customCollision = this._mergeCustomCollisions(x, y, data);
        if (customCollision > 0) {
          this.setBlockCollision(x, y, customCollision);
        }
      }
    }
  }

  static loadCustomCollision() {
    for (const event of $dataMap.events) {
      if (!event) {
        continue;
      }

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

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

  static isPositionPassable(x, y, d) {
    const index = this.collisionIndex(x, y);

    const collision = currentMapCollisionTable[index];
    if (!collision || collision === 1) {
      return true;
    }

    if (collision === 2) {
      return false;
    }

    if (collision >= 20) {
      const unblockedDirection = collision - 20;
      if (!this.shareADirection(d, unblockedDirection)) {
        return false;
      }
    } else if (collision > 10) {
      const blockedDirection = collision - 10;

      if (this.shareADirection(d, blockedDirection)) {
        return false;
      }
    } else if (collision === 4) {
      if (DirectionHelper.goesUp(d) || DirectionHelper.goesDown(d)) {
        return false;
      }
    } else if (collision === 5) {
      if (DirectionHelper.goesLeft(d) || DirectionHelper.goesRight(d)) {
        return false;
      }
    }

    return true;
  }

  static applyTileCollision(x, y, down, left, right, up) {
    if (down === left && down === right && down === up) {
      this.applyFullTileCollision(x, y, down ? 1 : 2);
      return;
    }

    if (CycloneMovement$1.collisionStepCount === 1) {
      this.applySingleTileCollision(x, y, !up, !down, !left, !right);
      return;
    }

    this.applyFullTileCollision(x, y, 1);

    if (!left) {
      this.applyTileDirectionCollision(x, y, 4, 14);
    }

    if (!right) {
      this.applyTileDirectionCollision(x, y, 6, 16);
    }

    if (!down) {
      this.applyTileDirectionCollision(x, y, 2, 12);

      if (!left) {
        this.applyTileCornerCollision(x, y, 4, 2, 11);
      }
      if (!right) {
        this.applyTileCornerCollision(x, y, 6, 2, 13);
      }
    }

    if (!up) {
      this.applyTileDirectionCollision(x, y, 8, 18);

      if (!left) {
        this.applyTileCornerCollision(x, y, 4, 8, 17);
      }
      if (!right) {
        this.applyTileCornerCollision(x, y, 6, 8, 19);
      }
    }
  }

  static tileIdx(x, y) {
    const width = $dataMap.width;
    return y * width + x || 0;
  }

  static addPixelMovementToClass(classObj) {
    addPixelMovementToClass(classObj);
  }
}

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

CycloneMovement.patchClass(Game_Map, $super => class {
  isValid(x, y) {
    return x >= 0 && y >= 0 && Math.floor(x) < this.width() && Math.floor(y) < this.height();
  }

  setup(mapId) {
    $super.setup.call(this, mapId);
    this._loaded = true;
    CycloneMovement.setupCollision();
  }

  isTileClear(x, y) {
    if (!this.checkPassage(x, y, 2)) {
      return false;
    }

    if (!this.checkPassage(x, y, 4)) {
      return false;
    }

    if (!this.checkPassage(x, y, 6)) {
      return false;
    }

    if (!this.checkPassage(x, y, 8)) {
      return false;
    }

    return true;
  }

  distance(x1, y1, x2, y2) {
    if (!CycloneMovement.diagonalPathfinding) {
      return $super.distance.call(this, x1, y1, x2, y2);
    }

    // good old Pythagoras
    const b = Math.abs(this.deltaY(y1, y2));
    const c = Math.abs(this.deltaX(x1, x2));
    const a2 = Math.pow(b, 2) + Math.pow(c, 2);
    const a = Math.sqrt(a2);

    return a;
  }

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

let tryToLeaveVehicleDelay = 0;

CycloneMovement.patchClass(Game_Player, $super => class {
  get defaultWidth() {
    return 0.75;
  }
  get defaultHeight() {
    return 0.375;
  }
  get defaultHitboxX() {
    return 0.125;
  }
  get defaultHitboxY() {
    return 0.5;
  }

  getWidth() {
    if (this.isInAnyVehicle()) {
      return 1;
    }

    return this.defaultWidth;
  }
  getHeight() {
    if (this.isInAnyVehicle()) {
      return 1;
    }

    return this.defaultHeight;
  }

  getHitboxX() {
    if (this.isInAnyVehicle()) {
      return 0;
    }

    return this.defaultHitboxX;
  }

  getHitboxY() {
    if (this.isInAnyVehicle()) {
      return 0;
    }

    return this.defaultHitboxY;
  }

  isInAnyVehicle() {
    if (this._ignoreVehicle) {
      return false;
    }

    return this._vehicleType !== 'walk';
  }

  moveByInput() {
    if (this.isMoving() || !this.canMove()) {
      return;
    }

    let direction = Input.dir4;
    let diagonalDirection = Input.dir8;
    let alternativeD = direction;

    if (direction > 0) {
      $gameTemp.clearDestination();
    } else if ($gameTemp.isDestinationValid()) {
      diagonalDirection = this.determineDirectionToDestination();
      direction = CycloneMovement.getFirstDirection(diagonalDirection);
    }

    alternativeD = CycloneMovement.getAlternativeDirection(direction, diagonalDirection);

    if (direction === 0) {
      return;
    }

    this.tryMoving(direction, alternativeD, diagonalDirection);

    if (!this.isMoving()) {
      if (this.tryOtherMovementOptions(direction)) {
        return;
      }

      if (this._direction !== direction) {
        this.setDirection(direction);
        this.checkEventTriggerTouchFront();
      }
    }
  }

  tryMoving(direction, alternativeD, diagonalDirection) {
    if (this.canPass(this._x, this._y, direction) || (direction !== alternativeD && this.canPass(this._x, this._y, alternativeD))) {
      this.onBeforeMove();

      const oldDirection = this._direction;
      this.executeMove(diagonalDirection);
      if (this.isMovementSucceeded()) {
        return;
      }

      this.executeMove(direction);
      if (!this.isMovementSucceeded()) {
        this.executeMove(alternativeD);

        // If none of the directions were clear and we were already facing one of them before, then revert back to it
        if (!this.isMovementSucceeded()) {
          if (oldDirection === direction || oldDirection === alternativeD) {
            this._direction = oldDirection;
          }
        }
      }
    }
  }

  onBeforeMove() {
    tryToLeaveVehicleDelay = 20;
  }

  tryOtherMovementOptions(direction) {
    if (this.tryToLeaveVehicle(direction)) {
      return true;
    }

    if (this.isInAnyVehicle()) {
      return false;
    }

    if (this.tryToAvoid(direction, CycloneMovement.params.maxOffset)) {
      return true;
    }

    return false;
  }

  tryToLeaveVehicle(direction) {
    if (!CycloneMovement.autoLeaveVehicles) {
      return false;
    }

    if (tryToLeaveVehicleDelay > 0) {
      tryToLeaveVehicleDelay--;
      return false;
    }

    if (!this.isInBoat() && !this.isInShip()) {
      return false;
    }

    return this.getOffVehicle(direction);
  }

  tryToAvoid(direction, maxOffset) {
    if (!CycloneMovement.params.sidestepEvents) {
      if (this._blockingReason === 'characters') {
        return false;
      }
    }

    if (direction === 4 || direction === 6) {
      if (this.tryToAvoidVertically(direction, maxOffset)) {
        return true;
      }
    }

    if (direction === 2 || direction === 8) {
      if (this.tryToAvoidHorizontally(direction, maxOffset)) {
        return true;
      }
    }

    return false;
  }

  tryToAvoidDirection(xOffset, yOffset, movementDirection, faceDirection) {
    if (this.canPass(this._x + xOffset, this._y + yOffset, faceDirection)) {
      this.executeMove(movementDirection);
      this.setDirection(faceDirection);
      return true;
    }

    return false;
  }

  tryToAvoidVertically(direction, maxOffset) {
    let previousOffset = 0;
    let offset = CycloneMovement.stepSize;

    let downEnabled = true;
    let upEnabled = true;

    while (offset <= maxOffset) {
      if (downEnabled) {
        if (!this.canPass(this._x, this._y + previousOffset, 2)) {
          downEnabled = false;
        }
      }

      if (upEnabled) {
        if (!this.canPass(this._x, this._y - previousOffset, 8)) {
          upEnabled = false;
        }
      }

      if (downEnabled && this.tryToAvoidDirection(0, offset, 2, direction)) {
        return true;
      }

      if (upEnabled && this.tryToAvoidDirection(0, -offset, 8, direction)) {
        return true;
      }

      previousOffset = offset;
      offset += CycloneMovement.stepSize;
    }
  }

  tryToAvoidHorizontally(direction, maxOffset) {
    let previousOffset = 0;
    let offset = CycloneMovement.stepSize;
    let leftEnabled = true;
    let rightEnabled = true;

    while (offset <= maxOffset) {
      if (leftEnabled) {
        if (!this.canPass(this._x - previousOffset, 4)) {
          leftEnabled = false;
        }
      }

      if (rightEnabled) {
        if (!this.canPass(this._x + previousOffset, 6)) {
          rightEnabled = false;
        }
      }

      if (rightEnabled && this.tryToAvoidDirection(offset, 0, 6, direction)) {
        return true;
      }

      if (leftEnabled && this.tryToAvoidDirection(-offset, 0, 4, direction)) {
        return true;
      }

      previousOffset = offset;
      offset += CycloneMovement.stepSize;
    }

    return false;
  }

  executeMove(direction) {
    switch (direction) {
      case 8:
      case 2:
      case 4:
      case 6:
        this.moveStraight(direction);
        break;

      case 7:
        this.moveDiagonally(4, 8);
        break;
      case 9:
        this.moveDiagonally(6, 8);
        break;
      case 1:
        this.moveDiagonally(4, 2);
        break;
      case 3:
        this.moveDiagonally(6, 2);
        break;
    }
  }

  updateDashing() {
    this.updateIsMoving();
    $super.updateDashing.call(this);
  }

  moveStraight(d) {
    if (this.isMovementSucceeded()) {
      this._followers.updateMove();
    }

    this._moveStraight(d);
  }

  moveDiagonally(horz, vert) {
    if (this.isMovementSucceeded()) {
      this._followers.updateMove();
    }

    this._moveDiagonally(horz, vert);
  }

  checkEventTriggerThere(triggers) {
    if (!this.canStartLocalEvents()) {
      return;
    }

    const direction = this.direction();
    const x1 = this.left;
    const y1 = this.top;

    const x2 = CycloneMovement.roundXWithDirection(x1, direction);
    const y2 = CycloneMovement.roundYWithDirection(y1, direction);

    this.startMapEvent(x2, y2, triggers, true);

    if (!$gameMap.isAnyEventStarting() && $gameMap.isCounter(x2, y2)) {
      const x3 = $gameMap.roundXWithDirection(x2, direction);
      const y3 = $gameMap.roundYWithDirection(y2, direction);

      this.startMapEvent(x3, y3, triggers, true);
    }
  }

  shouldTriggerEvent(event, triggers, normal) {
    if (!event) {
      return false;
    }

    if (!event.isTriggerIn(triggers)) {
      return false;
    }

    if (event.isNormalPriority() !== normal) {
      return false;
    }

    if (!event.hasAnythingToRun()) {
      return false;
    }

    return true;
  }

  startMapTileEvent(tileX, tileY, triggers, normal) {
    if (!CycloneMovement.triggerAllEvents && $gameMap.isEventRunning()) {
      return;
    }

    let anyStarted = false;

    const events = $gameMap.eventsXy(tileX, tileY);
    for (const event of events) {
      if (!this.shouldTriggerEvent(event, triggers, normal)) {
        continue;
      }

      event.start();
      anyStarted = true;

      if (!CycloneMovement.triggerAllEvents) {
        return true;
      }
    }

    return anyStarted;
  }

  checkEventTriggerHere(triggers) {
    if (!this.canStartLocalEvents()) {
      return;
    }

    // Remove "Player Touch" and "Event Touch" from possible trigers
    const newTriggers = [];
    for (const t of triggers) {
      if (t !== 1 && t !== 2) {
        newTriggers.push(t);
      }
    }

    if (newTriggers.length) {
      this.startMapEvent(this.left, this.top, newTriggers, false);
    }
  }

  startMapEvent(x, y, triggers, normal) {
    if ($gameMap.isEventRunning()) {
      return;
    }

    const left = x;
    const right = x + this.width;
    const top = y;
    const bottom = y + this.height;

    const firstX = Math.floor(left);
    const lastX = CycloneMovement.isRoundNumber(right) ? right - 1 : Math.floor(right);
    const firstY = Math.floor(top);
    const lastY = CycloneMovement.isRoundNumber(bottom) ? bottom -1 : Math.floor(bottom);

    for (let newX = firstX; newX <= lastX; newX++) {
      for (let newY = firstY; newY <= lastY; newY++) {
        if (this.startMapTileEvent(newX, newY, triggers, normal) === true) {
          return true;
        }
      }
    }

    return false;
  }

  isOnDamageFloor() {
    if (this.isInAirship()) {
      return false;
    }

    if (this._newMaxX < 0 || this._newMaxY < 0) {
      return false;
    }

    for (let x = this._newMinX; x <= this._newMaxX; x++) {
      for (let y = this._newMinY; y <= this._newMaxY; y++) {
        if ($gameMap.isDamageFloor(x, y)) {
          return true;
        }
      }
    }

    return false;
  }

  encounterProgressValue() {
    const old = $super.encounterProgressValue.call(this);

    return old / CycloneMovement.stepCount;
  }

  updateNonmoving(wasMoving, sceneActive) {
    try {
      if ($gameMap.isEventRunning()) {
        return;
      }

      const enteredNewTile = this._newMaxX >= 0 && this._newMaxY >= 0;
      if (enteredNewTile) {
        $gameParty.onPlayerWalk();
      }

      if (enteredNewTile) {
        for (let x = this._newMinX; x <= this._newMaxX; x++) {
          for (let y = this._newMinY; y <= this._newMaxY; y++) {
            this.startMapEvent(x, y, [1, 2], false);

            if ($gameMap.setupStartingEvent()) {
              return;
            }
          }
        }
      }

      if (sceneActive && this.triggerAction()) {
        return;
      }

      if (wasMoving) {
        this.updateEncounterCount();
      } else {
        $gameTemp.clearDestination();
      }

      if (wasMoving || Input.dir4 !== 0) {
        this.checkEventTriggerThere([1, 2]);
        $gameMap.setupStartingEvent();
      }
    } finally {
      this._newMinX = Infinity;
      this._newMinY = Infinity;
      this._newMaxX = -Infinity;
      this._newMaxY = -Infinity;
    }
  }

  updateMove() {
    this.iterateNewTiles((x, y) => {
      this._newMinX = Math.min(x, this._newMinX ?? -Infinity);
      this._newMinY = Math.min(y, this._newMinY ?? -Infinity);
      this._newMaxX = Math.max(x, this._newMaxX ?? Infinity);
      this._newMaxY = Math.max(y, this._newMaxY ?? Infinity);
    });

    $super.updateMove.call(this);
  }

  _isSamePos(x1, y1, destX, destY) {
    if (Math.floor(x1) !== destX && Math.ceil(x1) !== destX) {
      return false;
    }

    if (Math.floor(y1) !== destY && Math.ceil(y1) !== destY) {
      return false;
    }

    return true;
  }

  triggerTouchAction() {
    if (!$gameTemp.isDestinationValid()) {
      return false;
    }

    const direction = this.direction();
    const x1 = this.x;
    const y1 = this.y;
    const destX = $gameTemp.destinationX();
    const destY = $gameTemp.destinationY();

    if (this._isSamePos(x1, y1, destX, destY)) {
      const result = this.triggerTouchActionD1(x1, y1);
      if (result) {
        return result;
      }
    }

    const x2 = CycloneMovement.roundXWithDirection(x1, direction);
    const y2 = CycloneMovement.roundYWithDirection(y1, direction);

    if (this._isSamePos(x2, y2, destX, destY)) {
      const result = this.triggerTouchActionD2(x2, y2);
      if (result) {
        return result;
      }
    }

    const x3 = CycloneMovement.roundXWithDirection(x2, direction);
    const y3 = CycloneMovement.roundYWithDirection(y2, direction);

    if (this._isSamePos(x3, y3, destX, destY)) {
      return this.triggerTouchActionD3(x3, y3);
    }

    return false;
  }

  isTouchingAirship() {
    const airship = $gameMap.airship();
    if (!airship) {
      return false;
    }

    return this.isTouchingCharacter(airship);
  }

  isFacingVehicle(vehicle) {
    if (!vehicle) {
      return false;
    }

    let { x, y } = this;
    switch (this._direction) {
      case 2:
        y++;
        break;
      case 4:
        x--;
        break;
      case 6:
        x++;
        break;
      case 8:
        y--;
        break;
    }

    return this.wouldTouchCharacterAt(vehicle, x, y);
  }

  getOnVehicle() {
    if (this.isTouchingAirship()) {
      this._vehicleType = 'airship';
    } else if (this.isFacingVehicle($gameMap.ship())) {
      this._vehicleType = 'ship';
    } else if (this.isFacingVehicle($gameMap.boat())) {
      this._vehicleType = 'boat';
    }

    if (this.isInAnyVehicle()) {
      this._vehicleGettingOn = true;

      if (!this.isInAirship()) {
        const vehicle = this.vehicle();
        if (vehicle) {
          this._x = vehicle._x;
          this._y = vehicle._y;

          this.updateAnimationCount();
        }
      }

      this.gatherFollowers();
    }
    return this._vehicleGettingOn;
  }

  checkDistanceToLand(direction, targetX, targetY) {
    switch (direction) {
      case 2:
        if (Math.abs(targetY - this.bottom) > 0.5) {
          return false;
        }
        break;
      case 4:
        if (Math.abs(targetX - this.left) > 1) {
          return false;
        }
        break;
      case 6:
        if (Math.abs(targetX - this.right) > 0.5) {
          return false;
        }
        break;
      case 8:
        if (Math.abs(targetY - this.top) > 1) {
          return false;
        }
        break;
    }

    return true;
  }

  isValidLandingPosition(vehicle, x, y, d) {
    if (!this.canLandOn(x, y, d)) {
      return false;
    }

    if (this.isCollidedWithCharacters(x, y)) {
      return false;
    }

    if (!vehicle.isLandOk(x, y, d)) {
      return false;
    }

    return true;
  }

  getLandingXOffset(vehicle, x, y, direction) {
    const maxOffset = this.isInAirship() ? Math.ceil(CycloneMovement.stepCount / 2) : CycloneMovement.stepCount;

    for (let i = 1; i < maxOffset; i++) {
      const offset = CycloneMovement.stepSize * i;
      if (this.isValidLandingPosition(vehicle, x - offset, y, direction)) {
        return -offset;
      }

      if (this.isValidLandingPosition(vehicle, x + offset, y, direction)) {
        return offset;
      }
    }

    return 0;
  }

  getLandingYOffset(vehicle, x, y, direction) {
    const maxOffset = this.isInAirship() ? Math.ceil(CycloneMovement.stepCount / 2) : CycloneMovement.stepCount;

    for (let i = 1; i < maxOffset; i++) {
      const offset = CycloneMovement.stepSize * i;
      if (this.isValidLandingPosition(vehicle, x, y - offset, direction)) {
        return -offset;
      }

      if (this.isValidLandingPosition(vehicle, x, y + offset, direction)) {
        return offset;
      }
    }

    return 0;
  }

  getBestLandingPosition(vehicle, direction) {
    let x;
    let y;
    let vehicleX = this.x;
    let vehicleY = this.y;
    const { stepCount } = CycloneMovement;

    if (this.isInAirship()) {
      x = Math.round(this.x * stepCount) / stepCount;
      y = Math.round(this.y * stepCount) / stepCount;
    } else {
      switch(direction) {
        case 2:
          x = Math.round(this.x * stepCount) / stepCount;
          y = Math.ceil((this.y + this.hitboxY + this.height) * stepCount) / stepCount;
          break;
        case 4:
          x = Math.floor((this.x - this.defaultHitboxX - this.defaultWidth) * stepCount) / stepCount;
          y = Math.round(this.y * stepCount) / stepCount;
          break;
        case 6:
          x = Math.ceil((this.x + this.hitboxX + this.width) * stepCount) / stepCount;
          y = Math.round(this.y * stepCount) / stepCount;
          break;
        case 8:
          x = Math.round(this.x * stepCount) / stepCount;
          y = Math.floor((this.y - this.defaultHitboxY - this.defaultHeight) * stepCount) / stepCount;
          break;
      }
    }

    if (this.isValidLandingPosition(vehicle, x, y, direction)) {
      return {
        x,
        y,
        vehicleX,
        vehicleY,
      };
    }

    if (CycloneMovement.isVertical(direction) || this.isInAirship()) {
      const xOffset = this.getLandingXOffset(vehicle, x, y, direction);
      if (xOffset !== 0) {
        return {
          x: x + xOffset,
          y,
          vehicleX: vehicleX + xOffset,
          vehicleY,
        };
      }

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

    const yOffset = this.getLandingYOffset(vehicle, x, y, direction);
    if (yOffset !== 0) {
      return {
        x,
        y: y + yOffset,
        vehicleX,
        vehicleY: vehicleY + yOffset
      };
    }

    return false;
  }

  getOffVehicle(direction = undefined) {
    direction = direction || this.direction();
    const vehicle = this.vehicle();
    if (!vehicle) {
      return this._vehicleGettingOff;
    }

    const target = this.getBestLandingPosition(vehicle, direction);
    if (!target) {
      return this._vehicleGettingOff;
    }

    if (this.isInAirship()) {
      this.setDirection(2);
    }

    this._followers.synchronize(this.x, this.y, direction);
    vehicle.getOff();

    const oldVehicleY = vehicle._y;
    const oldVehicleX = vehicle._x;

    vehicle._x = target.vehicleX;
    vehicle._y = target.vehicleY;
    this._x = target.x;
    this._y = target.y;
    this._positionHistory = [];

    if (!this.isInAirship()) {
      if (oldVehicleX < target.vehicleX) {
        vehicle.setDirection(6);
      } else if (oldVehicleX > target.vehicleX) {
        vehicle.setDirection(4);
      } else if (oldVehicleY < target.vehicleY) {
        vehicle.setDirection(2);
      } else if (oldVehicleY > target.vehicleY) {
        vehicle.setDirection(8);
      }

      this.updateAnimationCount();
      this.setTransparent(false);
    }

    this._vehicleGettingOff = true;
    this.setMoveSpeed(4);
    this.setThrough(false);
    this.makeEncounterCount();
    this.gatherFollowers();
  }

  // Stop airship from setting the movement as through
  updateVehicleGetOn() {
    const oldThrough = this._through;
    $super.updateVehicleGetOn.call(this);
    this._through = oldThrough;
  }

  isThrough() {
    if (!this._ignoreVehicle && this.isInAirship()) {
      return true;
    }

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

  isPositionPassable(x, y, d) {
    const vehicle = this.vehicle();
    if (vehicle && !this._ignoreVehicle) {
      return vehicle.checkPassage(Math.floor(x), Math.floor(y));
    }

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

  shouldSkipExtraPassabilityTests() {
    const vehicle = this.vehicle();

    if (vehicle && !this._ignoreVehicle) {
      return true;
    }

    return false;
  }

  isInVehicle() {
    if (this._ignoreVehicle) {
      return false;
    }

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

  // Check if there's enough room for the player on that position
  canLandOn(x, y, direction) {
    this._ignoreVehicle = true;
    this.updateHitbox();
    try {
      if (this.canPass(x, y, 2)) {
        return true;
      }
      if (this.canPass(x, y, 4)) {
        return true;
      }
      if (this.canPass(x, y, 6)) {
        return true;
      }
      if (this.canPass(x, y, 8)) {
        return true;
      }

      return false;
    } finally {
      this._ignoreVehicle = false;
      this.updateHitbox();
    }
  }

  determineDirectionToDestination() {
    const x = $gameTemp.destinationX();
    const y = $gameTemp.destinationY();

    return this.findDirectionTo(x, y);
  }

  searchLimit() {
    const limit = $super.searchLimit.call(this);

    if (TouchInput.isLongPressed()) {
      return Math.floor(limit / CycloneMovement.stepCount);
    }

    return limit;
  }
});

CycloneMovement.patchClass(Game_Follower, $super => class {
  getWidth() {
    return 0.75;
  }
  getHeight() {
    return 0.375;
  }
  getHitboxX() {
    return 0.125;
  }
  getHibtoxY() {
    return 0.5;
  }

  chaseCharacter(character) {
    if (this.isMoving()) {
      return;
    }

    const position = character.getPositionToFollow();
    if (!position) {
      return;
    }

    const { x, y } = position;

    this.chasePosition(x, y);
  }
});

CycloneMovement.patchClass(Game_Vehicle, $super => class {
  checkPassage(x, y) {
    if (this.isBoat()) {
      return $gameMap.isBoatPassable(x, y);
    }

    if (this.isShip()) {
      return $gameMap.isShipPassable(x, y);
    }

    return this.isAirship();
  }

  shouldPassThrough() {
    if (this.isAirship()) {
      return true;
    }

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

  isAirshipLandOk(x, y) {
    if (!$gamePlayer.canLandOn(x, y)) {
      return false;
    }

    const floorX = Math.floor(x);
    const floorY = Math.floor(y);

    if (!$gameMap.isAirshipLandOk(floorX, floorY)) {
      return false;
    }

    if ($gameMap.eventsXy(floorX, floorY).length > 0) {
      return false;
    }

    return true;
  }

  isLandOk(x, y, d) {
    if (this.isAirship()) {
      return this.isAirshipLandOk(x, y);
    }

    return true;
  }

  getOff() {
    this._driving = false;
    this.setWalkAnime(false);
    this.setStepAnime(false);
    $gameSystem.replayWalkingBgm();
  }
});

const uselessCommands = Object.freeze([
  // comments
  108,
  408,
  // label
  118,
  // end of list
  0,
]);

CycloneMovement.patchClass(Game_Event, $super => class {
  turnTowardPlayer() {
    const sx = this.deltaXFrom($gamePlayer.x);
    const sy = this.deltaYFrom($gamePlayer.y);

    const asx = Math.abs(sx);
    const asy = Math.abs(sy);

    if (asx < 1 && asy < 1) {
      this.setDirection(10 - $gamePlayer._direction);
      return;
    }

    if (asx > asy) {
      this.setDirection(sx > 0 ? 4 : 6);
      return;
    }

    if (sy !== 0) {
      this.setDirection(sy > 0 ? 8 : 2);
    }
  }

  turnAwayFromPlayer() {
    const sx = this.deltaXFrom($gamePlayer.x);
    const sy = this.deltaYFrom($gamePlayer.y);
    const asx = Math.abs(sx);
    const asy = Math.abs(sy);

    if (asx < 1 && asy < 1) {
      this.setDirection($gamePlayer._direction);
      return;
    }

    if (asx > asy) {
      this.setDirection(sx > 0 ? 6 : 4);
      return;
    }

    if (sy !== 0) {
      this.setDirection(sy > 0 ? 2 : 8);
    }
  }

  hasAnythingToRun() {
    if (!CycloneMovement.ignoreEmptyEvents) {
      return true;
    }

    for (const command of this.list()) {
      if (uselessCommands.includes(Number(command.code))) {
        continue;
      }

      return true;
    }

    return false;
  }
});

let timeout;
let latestX;
let latestY;
let needsCalling = false;

CycloneMovement.patchClass(Game_Temp, $super => class {
  _setDestination(x, y) {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      timeout = false;
      if (needsCalling) {
        this._setDestination(latestX, latestY);
      }
    }, 50 * CycloneMovement.stepCount);

    $super.setDestination.call(this, x, y);
    needsCalling = false;
    latestX = x;
    latestY = y;
  }

  setDestination(x, y) {
    if (!TouchInput.isLongPressed()) {
      return this._setDestination(x, y);
    }

    if (!timeout) {
      return this._setDestination(x, y);
    }

    const delta = $gameMap.distance(x, y, latestX, latestY);
    if (delta > 3) {
      return this._setDestination(x, y);
    }

    latestX = x;
    latestY = y;

    needsCalling = true;
  }

  clearDestination(...args) {
    $super.clearDestination.call(this, ...args);

    needsCalling = false;
    latestX = undefined;
    latestY = undefined;

    if (timeout) {
      clearTimeout(timeout);
      timeout = false;
    }
  }
});

CycloneMovement.patchClass(Game_Party, $super => class {
  steps() {
    return Math.floor(this._steps);
  }

  increaseSteps() {
    this._steps += CycloneMovement.stepSize;
  }
});

CycloneMovement.patchClass(Scene_Map, $super => class {
  onMapTouch(...args) {
    if (CycloneMovement.disableMouseMovement) {
      return;
    }

    $super.onMapTouch.call(this, ...args);
  }
});

CycloneMovement.patchClass(DataManager, $super => class {
  static onLoad(object) {
    $super.onLoad.call(this, object);

    if (this.isMapObject(object)) {
      CycloneMovement.setupCollision();
    }
  }
});
})();