code:
//=============================================================================
// rmmz_core.js v1.8.0
//=============================================================================

//-----------------------------------------------------------------------------
/**
 * This section contains some methods that will be added to the standard
 * Javascript objects.
 *
 * @namespace JsExtensions
 */

/**
 * Makes a shallow copy of the array.
 *
 * @memberof JsExtensions
 * @returns {array} A shallow copy of the array.
 */
Array.prototype.clone = function () {
  return this.slice(0);
};

Object.defineProperty(Array.prototype, "clone", {
  enumerable: false,
});

/**
 * Checks whether the array contains a given element.
 *
 * @memberof JsExtensions
 * @param {any} element - The element to search for.
 * @returns {boolean} True if the array contains a given element.
 * @deprecated includes() should be used instead.
 */
Array.prototype.contains = function (element) {
  return this.includes(element);
};

Object.defineProperty(Array.prototype, "contains", {
  enumerable: false,
});

/**
 * Checks whether the two arrays are the same.
 *
 * @memberof JsExtensions
 * @param {array} array - The array to compare to.
 * @returns {boolean} True if the two arrays are the same.
 */
Array.prototype.equals = function (array) {
  if (!array || this.length !== array.length) {
    return false;
  }
  for (let i = 0; i < this.length; i++) {
    if (this[i] instanceof Array && array[i] instanceof Array) {
      if (!this[i].equals(array[i])) {
        return false;
      }
    } else if (this[i] !== array[i]) {
      return false;
    }
  }
  return true;
};

Object.defineProperty(Array.prototype, "equals", {
  enumerable: false,
});

/**
 * Removes a given element from the array (in place).
 *
 * @memberof JsExtensions
 * @param {any} element - The element to remove.
 * @returns {array} The array after remove.
 */
Array.prototype.remove = function (element) {
  for (;;) {
    const index = this.indexOf(element);
    if (index >= 0) {
      this.splice(index, 1);
    } else {
      return this;
    }
  }
};

Object.defineProperty(Array.prototype, "remove", {
  enumerable: false,
});

/**
 * Generates a random integer in the range (0, max-1).
 *
 * @memberof JsExtensions
 * @param {number} max - The upper boundary (excluded).
 * @returns {number} A random integer.
 */
Math.randomInt = function (max) {
  return Math.floor(max * Math.random());
};

/**
 * Returns a number whose value is limited to the given range.
 *
 * @memberof JsExtensions
 * @param {number} min - The lower boundary.
 * @param {number} max - The upper boundary.
 * @returns {number} A number in the range (min, max).
 */
Number.prototype.clamp = function (min, max) {
  return Math.min(Math.max(this, min), max);
};

/**
 * Returns a modulo value which is always positive.
 *
 * @memberof JsExtensions
 * @param {number} n - The divisor.
 * @returns {number} A modulo value.
 */
Number.prototype.mod = function (n) {
  return ((this % n) + n) % n;
};

/**
 * Makes a number string with leading zeros.
 *
 * @memberof JsExtensions
 * @param {number} length - The length of the output string.
 * @returns {string} A string with leading zeros.
 */
Number.prototype.padZero = function (length) {
  return String(this).padZero(length);
};

/**
 * Checks whether the string contains a given string.
 *
 * @memberof JsExtensions
 * @param {string} string - The string to search for.
 * @returns {boolean} True if the string contains a given string.
 * @deprecated includes() should be used instead.
 */
String.prototype.contains = function (string) {
  return this.includes(string);
};

/**
 * Replaces %1, %2 and so on in the string to the arguments.
 *
 * @memberof JsExtensions
 * @param {any} ...args The objects to format.
 * @returns {string} A formatted string.
 */
String.prototype.format = function () {
  return this.replace(/%([0-9]+)/g, (s, n) => arguments[Number(n) - 1]);
};

/**
 * Makes a number string with leading zeros.
 *
 * @memberof JsExtensions
 * @param {number} length - The length of the output string.
 * @returns {string} A string with leading zeros.
 */
String.prototype.padZero = function (length) {
  return this.padStart(length, "0");
};

//-----------------------------------------------------------------------------
/**
 * The static class that defines utility methods.
 *
 * @namespace
 */
function Utils() {
  throw new Error("This is a static class");
}

/**
 * The name of the RPG Maker. "MZ" in the current version.
 *
 * @type string
 * @constant
 */
Utils.RPGMAKER_NAME = "MZ";

/**
 * The version of the RPG Maker.
 *
 * @type string
 * @constant
 */
Utils.RPGMAKER_VERSION = "1.8.0";

/**
 * Checks whether the current RPG Maker version is greater than or equal to
 * the given version.
 *
 * @param {string} version - The "x.x.x" format string to compare.
 * @returns {boolean} True if the current version is greater than or equal
 *                    to the given version.
 */
Utils.checkRMVersion = function (version) {
  const array1 = this.RPGMAKER_VERSION.split(".");
  const array2 = String(version).split(".");
  for (let i = 0; i < array1.length; i++) {
    const v1 = parseInt(array1[i]);
    const v2 = parseInt(array2[i]);
    if (v1 > v2) {
      return true;
    } else if (v1 < v2) {
      return false;
    }
  }
  return true;
};

/**
 * Checks whether the option is in the query string.
 *
 * @param {string} name - The option name.
 * @returns {boolean} True if the option is in the query string.
 */
Utils.isOptionValid = function (name) {
  const args = location.search.slice(1);
  if (args.split("&").includes(name)) {
    return true;
  }
  if (this.isNwjs() && nw.App.argv.length > 0) {
    return nw.App.argv[0].split("&").includes(name);
  }
  return false;
};

/**
 * Checks whether the platform is NW.js.
 *
 * @returns {boolean} True if the platform is NW.js.
 */
Utils.isNwjs = function () {
  return typeof require === "function" && typeof process === "object";
};

/**
 * Checks whether the platform is a mobile device.
 *
 * @returns {boolean} True if the platform is a mobile device.
 */
Utils.isMobileDevice = function () {
  const r = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/i;
  return !!navigator.userAgent.match(r);
};

/**
 * Checks whether the browser is Mobile Safari.
 *
 * @returns {boolean} True if the browser is Mobile Safari.
 */
Utils.isMobileSafari = function () {
  const agent = navigator.userAgent;
  return !!(
    agent.match(/iPhone|iPad|iPod/) &&
    agent.match(/AppleWebKit/) &&
    !agent.match("CriOS")
  );
};

/**
 * Checks whether the browser is Android Chrome.
 *
 * @returns {boolean} True if the browser is Android Chrome.
 */
Utils.isAndroidChrome = function () {
  const agent = navigator.userAgent;
  return !!(agent.match(/Android/) && agent.match(/Chrome/));
};

/**
 * Checks whether the browser is accessing local files.
 *
 * @returns {boolean} True if the browser is accessing local files.
 */
Utils.isLocal = function () {
  return window.location.href.startsWith("file:");
};

/**
 * Checks whether the browser supports WebGL.
 *
 * @returns {boolean} True if the browser supports WebGL.
 */
Utils.canUseWebGL = function () {
  try {
    const canvas = document.createElement("canvas");
    return !!canvas.getContext("webgl");
  } catch (e) {
    return false;
  }
};

/**
 * Checks whether the browser supports Web Audio API.
 *
 * @returns {boolean} True if the browser supports Web Audio API.
 */
Utils.canUseWebAudioAPI = function () {
  return !!(window.AudioContext || window.webkitAudioContext);
};

/**
 * Checks whether the browser supports CSS Font Loading.
 *
 * @returns {boolean} True if the browser supports CSS Font Loading.
 */
Utils.canUseCssFontLoading = function () {
  return !!(document.fonts && document.fonts.ready);
};

/**
 * Checks whether the browser supports IndexedDB.
 *
 * @returns {boolean} True if the browser supports IndexedDB.
 */
Utils.canUseIndexedDB = function () {
  return !!(window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB);
};

/**
 * Checks whether the browser can play ogg files.
 *
 * @returns {boolean} True if the browser can play ogg files.
 */
Utils.canPlayOgg = function () {
  if (!Utils._audioElement) {
    Utils._audioElement = document.createElement("audio");
  }
  return !!(
    Utils._audioElement &&
    Utils._audioElement.canPlayType('audio/ogg; codecs="vorbis"')
  );
};

/**
 * Checks whether the browser can play webm files.
 *
 * @returns {boolean} True if the browser can play webm files.
 */
Utils.canPlayWebm = function () {
  if (!Utils._videoElement) {
    Utils._videoElement = document.createElement("video");
  }
  return !!(
    Utils._videoElement &&
    Utils._videoElement.canPlayType('video/webm; codecs="vp8, vorbis"')
  );
};

/**
 * Encodes a URI component without escaping slash characters.
 *
 * @param {string} str - The input string.
 * @returns {string} Encoded string.
 */
Utils.encodeURI = function (str) {
  return encodeURIComponent(str).replace(/%2F/g, "/");
};

/**
 * Gets the filename that does not include subfolders.
 *
 * @param {string} filename - The filename with subfolders.
 * @returns {string} The filename without subfolders.
 */
Utils.extractFileName = function (filename) {
  return filename.split("/").pop();
};

/**
 * Escapes special characters for HTML.
 *
 * @param {string} str - The input string.
 * @returns {string} Escaped string.
 */
Utils.escapeHtml = function (str) {
  const entityMap = {
    "&": "&",
    "<": "<",
    ">": ">",
    '"': """,
    "'": "'",
    "/": "/",
  };
  return String(str).replace(/[&<>"'/]/g, (s) => entityMap[s]);
};

/**
 * Checks whether the string contains any Arabic characters.
 *
 * @returns {boolean} True if the string contains any Arabic characters.
 */
Utils.containsArabic = function (str) {
  const regExp = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/;
  return regExp.test(str);
};

/**
 * Sets information related to encryption.
 *
 * @param {boolean} hasImages - Whether the image files are encrypted.
 * @param {boolean} hasAudio - Whether the audio files are encrypted.
 * @param {string} key - The encryption key.
 */
Utils.setEncryptionInfo = function (hasImages, hasAudio, key) {
  // [Note] This function is implemented for module independence.
  this._hasEncryptedImages = hasImages;
  this._hasEncryptedAudio = hasAudio;
  this._encryptionKey = key;
};

/**
 * Checks whether the image files in the game are encrypted.
 *
 * @returns {boolean} True if the image files are encrypted.
 */
Utils.hasEncryptedImages = function () {
  return this._hasEncryptedImages;
};

/**
 * Checks whether the audio files in the game are encrypted.
 *
 * @returns {boolean} True if the audio files are encrypted.
 */
Utils.hasEncryptedAudio = function () {
  return this._hasEncryptedAudio;
};

/**
 * Decrypts encrypted data.
 *
 * @param {ArrayBuffer} source - The data to be decrypted.
 * @returns {ArrayBuffer} The decrypted data.
 */
Utils.decryptArrayBuffer = function (source) {
  const header = new Uint8Array(source, 0, 16);
  const headerHex = Array.from(header, (x) => x.toString(16)).join(",");
  if (headerHex !== "52,50,47,4d,56,0,0,0,0,3,1,0,0,0,0,0") {
    throw new Error("Decryption error");
  }
  const body = source.slice(16);
  const view = new DataView(body);
  const key = this._encryptionKey.match(/.{2}/g);
  for (let i = 0; i < 16; i++) {
    view.setUint8(i, view.getUint8(i) ^ parseInt(key[i], 16));
  }
  return body;
};

//-----------------------------------------------------------------------------
/**
 * The static class that carries out graphics processing.
 *
 * @namespace
 */
function Graphics() {
  throw new Error("This is a static class");
}

/**
 * Initializes the graphics system.
 *
 * @returns {boolean} True if the graphics system is available.
 */
Graphics.initialize = function () {
  this._width = 0;
  this._height = 0;
  this._defaultScale = 1;
  this._realScale = 1;
  this._errorPrinter = null;
  this._tickHandler = null;
  this._canvas = null;
  this._fpsCounter = null;
  this._loadingSpinner = null;
  this._stretchEnabled = this._defaultStretchMode();
  this._app = null;
  this._effekseer = null;
  this._wasLoading = false;

  /**
   * The total frame count of the game screen.
   *
   * @type number
   * @name Graphics.frameCount
   */
  this.frameCount = 0;

  /**
   * The width of the window display area.
   *
   * @type number
   * @name Graphics.boxWidth
   */
  this.boxWidth = this._width;

  /**
   * The height of the window display area.
   *
   * @type number
   * @name Graphics.boxHeight
   */
  this.boxHeight = this._height;

  this._updateRealScale();
  this._createAllElements();
  this._disableContextMenu();
  this._setupEventHandlers();
  this._createPixiApp();
  this._createEffekseerContext();

  return !!this._app;
};

/**
 * The PIXI.Application object.
 *
 * @readonly
 * @type PIXI.Application
 * @name Graphics.app
 */
Object.defineProperty(Graphics, "app", {
  get: function () {
    return this._app;
  },
  configurable: true,
});

/**
 * The context object of Effekseer.
 *
 * @readonly
 * @type EffekseerContext
 * @name Graphics.effekseer
 */
Object.defineProperty(Graphics, "effekseer", {
  get: function () {
    return this._effekseer;
  },
  configurable: true,
});

/**
 * Register a handler for tick events.
 *
 * @param {function} handler - The listener function to be added for updates.
 */
Graphics.setTickHandler = function (handler) {
  this._tickHandler = handler;
};

/**
 * Starts the game loop.
 */
Graphics.startGameLoop = function () {
  if (this._app) {
    this._app.start();
  }
};

/**
 * Stops the game loop.
 */
Graphics.stopGameLoop = function () {
  if (this._app) {
    this._app.stop();
  }
};

/**
 * Sets the stage to be rendered.
 *
 * @param {Stage} stage - The stage object to be rendered.
 */
Graphics.setStage = function (stage) {
  if (this._app) {
    this._app.stage = stage;
  }
};

/**
 * Shows the loading spinner.
 */
Graphics.startLoading = function () {
  if (!document.getElementById("loadingSpinner")) {
    document.body.appendChild(this._loadingSpinner);
  }
};

/**
 * Erases the loading spinner.
 *
 * @returns {boolean} True if the loading spinner was active.
 */
Graphics.endLoading = function () {
  if (document.getElementById("loadingSpinner")) {
    document.body.removeChild(this._loadingSpinner);
    return true;
  } else {
    return false;
  }
};

/**
 * Displays the error text to the screen.
 *
 * @param {string} name - The name of the error.
 * @param {string} message - The message of the error.
 * @param {Error} [error] - The error object.
 */
Graphics.printError = function (name, message, error = null) {
  if (!this._errorPrinter) {
    this._createErrorPrinter();
  }
  this._errorPrinter.innerHTML = this._makeErrorHtml(name, message, error);
  this._wasLoading = this.endLoading();
  this._applyCanvasFilter();
};

/**
 * Displays a button to try to reload resources.
 *
 * @param {function} retry - The callback function to be called when the button
 *                           is pressed.
 */
Graphics.showRetryButton = function (retry) {
  const button = document.createElement("button");
  button.id = "retryButton";
  button.innerHTML = "Retry";
  // [Note] stopPropagation() is required for iOS Safari.
  button.ontouchstart = (e) => e.stopPropagation();
  button.onclick = () => {
    Graphics.eraseError();
    retry();
  };
  this._errorPrinter.appendChild(button);
  button.focus();
};

/**
 * Erases the loading error text.
 */
Graphics.eraseError = function () {
  if (this._errorPrinter) {
    this._errorPrinter.innerHTML = this._makeErrorHtml();
    if (this._wasLoading) {
      this.startLoading();
    }
  }
  this._clearCanvasFilter();
};

/**
 * Converts an x coordinate on the page to the corresponding
 * x coordinate on the canvas area.
 *
 * @param {number} x - The x coordinate on the page to be converted.
 * @returns {number} The x coordinate on the canvas area.
 */
Graphics.pageToCanvasX = function (x) {
  if (this._canvas) {
    const left = this._canvas.offsetLeft;
    return Math.round((x - left) / this._realScale);
  } else {
    return 0;
  }
};

/**
 * Converts a y coordinate on the page to the corresponding
 * y coordinate on the canvas area.
 *
 * @param {number} y - The y coordinate on the page to be converted.
 * @returns {number} The y coordinate on the canvas area.
 */
Graphics.pageToCanvasY = function (y) {
  if (this._canvas) {
    const top = this._canvas.offsetTop;
    return Math.round((y - top) / this._realScale);
  } else {
    return 0;
  }
};

/**
 * Checks whether the specified point is inside the game canvas area.
 *
 * @param {number} x - The x coordinate on the canvas area.
 * @param {number} y - The y coordinate on the canvas area.
 * @returns {boolean} True if the specified point is inside the game canvas area.
 */
Graphics.isInsideCanvas = function (x, y) {
  return x >= 0 && x < this._width && y >= 0 && y < this._height;
};

/**
 * Shows the game screen.
 */
Graphics.showScreen = function () {
  this._canvas.style.opacity = 1;
};

/**
 * Hides the game screen.
 */
Graphics.hideScreen = function () {
  this._canvas.style.opacity = 0;
};

/**
 * Changes the size of the game screen.
 *
 * @param {number} width - The width of the game screen.
 * @param {number} height - The height of the game screen.
 */
Graphics.resize = function (width, height) {
  this._width = width;
  this._height = height;
  this._app.renderer.resize(width, height);
  this._updateAllElements();
};

/**
 * The width of the game screen.
 *
 * @type number
 * @name Graphics.width
 */
Object.defineProperty(Graphics, "width", {
  get: function () {
    return this._width;
  },
  set: function (value) {
    if (this._width !== value) {
      this._width = value;
      this._updateAllElements();
    }
  },
  configurable: true,
});

/**
 * The height of the game screen.
 *
 * @type number
 * @name Graphics.height
 */
Object.defineProperty(Graphics, "height", {
  get: function () {
    return this._height;
  },
  set: function (value) {
    if (this._height !== value) {
      this._height = value;
      this._updateAllElements();
    }
  },
  configurable: true,
});

/**
 * The default zoom scale of the game screen.
 *
 * @type number
 * @name Graphics.defaultScale
 */
Object.defineProperty(Graphics, "defaultScale", {
  get: function () {
    return this._defaultScale;
  },
  set: function (value) {
    if (this._defaultScale !== value) {
      this._defaultScale = value;
      this._updateAllElements();
    }
  },
  configurable: true,
});

Graphics._createAllElements = function () {
  this._createErrorPrinter();
  this._createCanvas();
  this._createLoadingSpinner();
  this._createFPSCounter();
};

Graphics._updateAllElements = function () {
  this._updateRealScale();
  this._updateErrorPrinter();
  this._updateCanvas();
  this._updateVideo();
};

Graphics._onTick = function (deltaTime) {
  this._fpsCounter.startTick();
  if (this._tickHandler) {
    this._tickHandler(deltaTime);
  }
  if (this._canRender()) {
    this._app.render();
  }
  this._fpsCounter.endTick();
};

Graphics._canRender = function () {
  return !!this._app.stage;
};

Graphics._updateRealScale = function () {
  if (this._stretchEnabled && this._width > 0 && this._height > 0) {
    const h = this._stretchWidth() / this._width;
    const v = this._stretchHeight() / this._height;
    this._realScale = Math.min(h, v);
    window.scrollTo(0, 0);
  } else {
    this._realScale = this._defaultScale;
  }
};

Graphics._stretchWidth = function () {
  if (Utils.isMobileDevice()) {
    return document.documentElement.clientWidth;
  } else {
    return window.innerWidth;
  }
};

Graphics._stretchHeight = function () {
  if (Utils.isMobileDevice()) {
    // [Note] Mobile browsers often have special operations at the top and
    //   bottom of the screen.
    const rate = Utils.isLocal() ? 1.0 : 0.9;
    return document.documentElement.clientHeight * rate;
  } else {
    return window.innerHeight;
  }
};

Graphics._makeErrorHtml = function (name, message /*, error*/) {
  const nameDiv = document.createElement("div");
  const messageDiv = document.createElement("div");
  nameDiv.id = "errorName";
  messageDiv.id = "errorMessage";
  nameDiv.innerHTML = Utils.escapeHtml(name || "");
  messageDiv.innerHTML = Utils.escapeHtml(message || "");
  return nameDiv.outerHTML + messageDiv.outerHTML;
};

Graphics._defaultStretchMode = function () {
  return Utils.isNwjs() || Utils.isMobileDevice();
};

Graphics._createErrorPrinter = function () {
  this._errorPrinter = document.createElement("div");
  this._errorPrinter.id = "errorPrinter";
  this._errorPrinter.innerHTML = this._makeErrorHtml();
  document.body.appendChild(this._errorPrinter);
};

Graphics._updateErrorPrinter = function () {
  const width = this._width * 0.8 * this._realScale;
  const height = 100 * this._realScale;
  this._errorPrinter.style.width = width + "px";
  this._errorPrinter.style.height = height + "px";
};

Graphics._createCanvas = function () {
  this._canvas = document.createElement("canvas");
  this._canvas.id = "gameCanvas";
  this._updateCanvas();
  document.body.appendChild(this._canvas);
};

Graphics._updateCanvas = function () {
  this._canvas.width = this._width;
  this._canvas.height = this._height;
  this._canvas.style.zIndex = 1;
  this._centerElement(this._canvas);
};

Graphics._updateVideo = function () {
  const width = this._width * this._realScale;
  const height = this._height * this._realScale;
  Video.resize(width, height);
};

Graphics._createLoadingSpinner = function () {
  const loadingSpinner = document.createElement("div");
  const loadingSpinnerImage = document.createElement("div");
  loadingSpinner.id = "loadingSpinner";
  loadingSpinnerImage.id = "loadingSpinnerImage";
  loadingSpinner.appendChild(loadingSpinnerImage);
  this._loadingSpinner = loadingSpinner;
};

Graphics._createFPSCounter = function () {
  this._fpsCounter = new Graphics.FPSCounter();
};

Graphics._centerElement = function (element) {
  const width = element.width * this._realScale;
  const height = element.height * this._realScale;
  element.style.position = "absolute";
  element.style.margin = "auto";
  element.style.top = 0;
  element.style.left = 0;
  element.style.right = 0;
  element.style.bottom = 0;
  element.style.width = width + "px";
  element.style.height = height + "px";
};

Graphics._disableContextMenu = function () {
  const elements = document.body.getElementsByTagName("*");
  const oncontextmenu = () => false;
  for (const element of elements) {
    element.oncontextmenu = oncontextmenu;
  }
};

Graphics._applyCanvasFilter = function () {
  if (this._canvas) {
    this._canvas.style.opacity = 0.5;
    this._canvas.style.filter = "blur(8px)";
    this._canvas.style.webkitFilter = "blur(8px)";
  }
};

Graphics._clearCanvasFilter = function () {
  if (this._canvas) {
    this._canvas.style.opacity = 1;
    this._canvas.style.filter = "";
    this._canvas.style.webkitFilter = "";
  }
};

Graphics._setupEventHandlers = function () {
  window.addEventListener("resize", this._onWindowResize.bind(this));
  document.addEventListener("keydown", this._onKeyDown.bind(this));
};

Graphics._onWindowResize = function () {
  this._updateAllElements();
};

Graphics._onKeyDown = function (event) {
  if (!event.ctrlKey && !event.altKey) {
    switch (event.keyCode) {
      case 113: // F2
        event.preventDefault();
        this._switchFPSCounter();
        break;
      case 114: // F3
        event.preventDefault();
        this._switchStretchMode();
        break;
      case 115: // F4
        event.preventDefault();
        this._switchFullScreen();
        break;
    }
  }
};

Graphics._switchFPSCounter = function () {
  this._fpsCounter.switchMode();
};

Graphics._switchStretchMode = function () {
  this._stretchEnabled = !this._stretchEnabled;
  this._updateAllElements();
};

Graphics._switchFullScreen = function () {
  if (this._isFullScreen()) {
    this._cancelFullScreen();
  } else {
    this._requestFullScreen();
  }
};

Graphics._isFullScreen = function () {
  return (
    document.fullScreenElement ||
    document.mozFullScreen ||
    document.webkitFullscreenElement
  );
};

Graphics._requestFullScreen = function () {
  const element = document.body;
  if (element.requestFullScreen) {
    element.requestFullScreen();
  } else if (element.mozRequestFullScreen) {
    element.mozRequestFullScreen();
  } else if (element.webkitRequestFullScreen) {
    element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  }
};

Graphics._cancelFullScreen = function () {
  if (document.cancelFullScreen) {
    document.cancelFullScreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.webkitCancelFullScreen) {
    document.webkitCancelFullScreen();
  }
};

Graphics._createPixiApp = function () {
  try {
    this._setupPixi();
    this._app = new PIXI.Application({
      view: this._canvas,
      autoStart: false,
    });
    this._app.ticker.remove(this._app.render, this._app);
    this._app.ticker.add(this._onTick, this);
  } catch (e) {
    this._app = null;
  }
};

Graphics._setupPixi = function () {
  PIXI.utils.skipHello();
  PIXI.settings.GC_MAX_IDLE = 600;
};

Graphics._createEffekseerContext = function () {
  if (this._app && window.effekseer) {
    try {
      this._effekseer = effekseer.createContext();
      if (this._effekseer) {
        this._effekseer.init(this._app.renderer.gl);
        this._effekseer.setRestorationOfStatesFlag(false);
      }
    } catch (e) {
      this._app = null;
    }
  }
};

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// FPSCounter
//
// This is based on Darsain's FPSMeter which is under the MIT license.
// The original can be found at https://github.com/Darsain/fpsmeter.

Graphics.FPSCounter = function () {
  this.initialize(...arguments);
};

Graphics.FPSCounter.prototype.initialize = function () {
  this._tickCount = 0;
  this._frameTime = 100;
  this._frameStart = 0;
  this._lastLoop = performance.now() - 100;
  this._showFps = true;
  this.fps = 0;
  this.duration = 0;
  this._createElements();
  this._update();
};

Graphics.FPSCounter.prototype.startTick = function () {
  this._frameStart = performance.now();
};

Graphics.FPSCounter.prototype.endTick = function () {
  const time = performance.now();
  const thisFrameTime = time - this._lastLoop;
  this._frameTime += (thisFrameTime - this._frameTime) / 12;
  this.fps = 1000 / this._frameTime;
  this.duration = Math.max(0, time - this._frameStart);
  this._lastLoop = time;
  if (this._tickCount++ % 15 === 0) {
    this._update();
  }
};

Graphics.FPSCounter.prototype.switchMode = function () {
  if (this._boxDiv.style.display === "none") {
    this._boxDiv.style.display = "block";
    this._showFps = true;
  } else if (this._showFps) {
    this._showFps = false;
  } else {
    this._boxDiv.style.display = "none";
  }
  this._update();
};

Graphics.FPSCounter.prototype._createElements = function () {
  this._boxDiv = document.createElement("div");
  this._labelDiv = document.createElement("div");
  this._numberDiv = document.createElement("div");
  this._boxDiv.id = "fpsCounterBox";
  this._labelDiv.id = "fpsCounterLabel";
  this._numberDiv.id = "fpsCounterNumber";
  this._boxDiv.style.display = "none";
  this._boxDiv.appendChild(this._labelDiv);
  this._boxDiv.appendChild(this._numberDiv);
  document.body.appendChild(this._boxDiv);
};

Graphics.FPSCounter.prototype._update = function () {
  const count = this._showFps ? this.fps : this.duration;
  this._labelDiv.textContent = this._showFps ? "FPS" : "ms";
  this._numberDiv.textContent = count.toFixed(0);
};

//-----------------------------------------------------------------------------
/**
 * The point class.
 *
 * @class
 * @extends PIXI.Point
 * @param {number} x - The x coordinate.
 * @param {number} y - The y coordinate.
 */
function Point() {
  this.initialize(...arguments);
}

Point.prototype = Object.create(PIXI.Point.prototype);
Point.prototype.constructor = Point;

Point.prototype.initialize = function (x, y) {
  PIXI.Point.call(this, x, y);
};

//-----------------------------------------------------------------------------
/**
 * The rectangle class.
 *
 * @class
 * @extends PIXI.Rectangle
 * @param {number} x - The x coordinate for the upper-left corner.
 * @param {number} y - The y coordinate for the upper-left corner.
 * @param {number} width - The width of the rectangle.
 * @param {number} height - The height of the rectangle.
 */
function Rectangle() {
  this.initialize(...arguments);
}

Rectangle.prototype = Object.create(PIXI.Rectangle.prototype);
Rectangle.prototype.constructor = Rectangle;

Rectangle.prototype.initialize = function (x, y, width, height) {
  PIXI.Rectangle.call(this, x, y, width, height);
};

//-----------------------------------------------------------------------------
/**
 * The basic object that represents an image.
 *
 * @class
 * @param {number} width - The width of the bitmap.
 * @param {number} height - The height of the bitmap.
 */
function Bitmap() {
  this.initialize(...arguments);
}

Bitmap.prototype.initialize = function (width, height) {
  this._canvas = null;
  this._context = null;
  this._baseTexture = null;
  this._image = null;
  this._url = "";
  this._paintOpacity = 255;
  this._smooth = true;
  this._loadListeners = [];

  // "none", "loading", "loaded", or "error"
  this._loadingState = "none";

  if (width > 0 && height > 0) {
    this._createCanvas(width, height);
  }

  /**
   * The face name of the font.
   *
   * @type string
   */
  this.fontFace = "sans-serif";

  /**
   * The size of the font in pixels.
   *
   * @type number
   */
  this.fontSize = 16;

  /**
   * Whether the font is bold.
   *
   * @type boolean
   */
  this.fontBold = false;

  /**
   * Whether the font is italic.
   *
   * @type boolean
   */
  this.fontItalic = false;

  /**
   * The color of the text in CSS format.
   *
   * @type string
   */
  this.textColor = "#ffffff";

  /**
   * The color of the outline of the text in CSS format.
   *
   * @type string
   */
  this.outlineColor = "rgba(0, 0, 0, 0.5)";

  /**
   * The width of the outline of the text.
   *
   * @type number
   */
  this.outlineWidth = 3;
};

/**
 * Loads a image file.
 *
 * @param {string} url - The image url of the texture.
 * @returns {Bitmap} The new bitmap object.
 */
Bitmap.load = function (url) {
  const bitmap = Object.create(Bitmap.prototype);
  bitmap.initialize();
  bitmap._url = url;
  bitmap._startLoading();
  return bitmap;
};

/**
 * Takes a snapshot of the game screen.
 *
 * @param {Stage} stage - The stage object.
 * @returns {Bitmap} The new bitmap object.
 */
Bitmap.snap = function (stage) {
  const width = Graphics.width;
  const height = Graphics.height;
  const bitmap = new Bitmap(width, height);
  const renderTexture = PIXI.RenderTexture.create(width, height);
  if (stage) {
    const renderer = Graphics.app.renderer;
    renderer.render(stage, renderTexture);
    stage.worldTransform.identity();
    const canvas = renderer.extract.canvas(renderTexture);
    bitmap.context.drawImage(canvas, 0, 0);
    canvas.width = 0;
    canvas.height = 0;
  }
  renderTexture.destroy({ destroyBase: true });
  bitmap.baseTexture.update();
  return bitmap;
};

/**
 * Checks whether the bitmap is ready to render.
 *
 * @returns {boolean} True if the bitmap is ready to render.
 */
Bitmap.prototype.isReady = function () {
  return this._loadingState === "loaded" || this._loadingState === "none";
};

/**
 * Checks whether a loading error has occurred.
 *
 * @returns {boolean} True if a loading error has occurred.
 */
Bitmap.prototype.isError = function () {
  return this._loadingState === "error";
};

/**
 * The url of the image file.
 *
 * @readonly
 * @type string
 * @name Bitmap#url
 */
Object.defineProperty(Bitmap.prototype, "url", {
  get: function () {
    return this._url;
  },
  configurable: true,
});

/**
 * The base texture that holds the image.
 *
 * @readonly
 * @type PIXI.BaseTexture
 * @name Bitmap#baseTexture
 */
Object.defineProperty(Bitmap.prototype, "baseTexture", {
  get: function () {
    return this._baseTexture;
  },
  configurable: true,
});

/**
 * The bitmap image.
 *
 * @readonly
 * @type HTMLImageElement
 * @name Bitmap#image
 */
Object.defineProperty(Bitmap.prototype, "image", {
  get: function () {
    return this._image;
  },
  configurable: true,
});

/**
 * The bitmap canvas.
 *
 * @readonly
 * @type HTMLCanvasElement
 * @name Bitmap#canvas
 */
Object.defineProperty(Bitmap.prototype, "canvas", {
  get: function () {
    this._ensureCanvas();
    return this._canvas;
  },
  configurable: true,
});

/**
 * The 2d context of the bitmap canvas.
 *
 * @readonly
 * @type CanvasRenderingContext2D
 * @name Bitmap#context
 */
Object.defineProperty(Bitmap.prototype, "context", {
  get: function () {
    this._ensureCanvas();
    return this._context;
  },
  configurable: true,
});

/**
 * The width of the bitmap.
 *
 * @readonly
 * @type number
 * @name Bitmap#width
 */
Object.defineProperty(Bitmap.prototype, "width", {
  get: function () {
    const image = this._canvas || this._image;
    return image ? image.width : 0;
  },
  configurable: true,
});

/**
 * The height of the bitmap.
 *
 * @readonly
 * @type number
 * @name Bitmap#height
 */
Object.defineProperty(Bitmap.prototype, "height", {
  get: function () {
    const image = this._canvas || this._image;
    return image ? image.height : 0;
  },
  configurable: true,
});

/**
 * The rectangle of the bitmap.
 *
 * @readonly
 * @type Rectangle
 * @name Bitmap#rect
 */
Object.defineProperty(Bitmap.prototype, "rect", {
  get: function () {
    return new Rectangle(0, 0, this.width, this.height);
  },
  configurable: true,
});

/**
 * Whether the smooth scaling is applied.
 *
 * @type boolean
 * @name Bitmap#smooth
 */
Object.defineProperty(Bitmap.prototype, "smooth", {
  get: function () {
    return this._smooth;
  },
  set: function (value) {
    if (this._smooth !== value) {
      this._smooth = value;
      this._updateScaleMode();
    }
  },
  configurable: true,
});

/**
 * The opacity of the drawing object in the range (0, 255).
 *
 * @type number
 * @name Bitmap#paintOpacity
 */
Object.defineProperty(Bitmap.prototype, "paintOpacity", {
  get: function () {
    return this._paintOpacity;
  },
  set: function (value) {
    if (this._paintOpacity !== value) {
      this._paintOpacity = value;
      this.context.globalAlpha = this._paintOpacity / 255;
    }
  },
  configurable: true,
});

/**
 * Destroys the bitmap.
 */
Bitmap.prototype.destroy = function () {
  if (this._baseTexture) {
    this._baseTexture.destroy();
    this._baseTexture = null;
  }
  this._destroyCanvas();
};

/**
 * Resizes the bitmap.
 *
 * @param {number} width - The new width of the bitmap.
 * @param {number} height - The new height of the bitmap.
 */
Bitmap.prototype.resize = function (width, height) {
  width = Math.max(width || 0, 1);
  height = Math.max(height || 0, 1);
  this.canvas.width = width;
  this.canvas.height = height;
  this.baseTexture.width = width;
  this.baseTexture.height = height;
};

/**
 * Performs a block transfer.
 *
 * @param {Bitmap} source - The bitmap to draw.
 * @param {number} sx - The x coordinate in the source.
 * @param {number} sy - The y coordinate in the source.
 * @param {number} sw - The width of the source image.
 * @param {number} sh - The height of the source image.
 * @param {number} dx - The x coordinate in the destination.
 * @param {number} dy - The y coordinate in the destination.
 * @param {number} [dw=sw] The width to draw the image in the destination.
 * @param {number} [dh=sh] The height to draw the image in the destination.
 */
Bitmap.prototype.blt = function (source, sx, sy, sw, sh, dx, dy, dw, dh) {
  dw = dw || sw;
  dh = dh || sh;
  try {
    const image = source._canvas || source._image;
    this.context.globalCompositeOperation = "source-over";
    this.context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
    this._baseTexture.update();
  } catch (e) {
    //
  }
};

/**
 * Returns pixel color at the specified point.
 *
 * @param {number} x - The x coordinate of the pixel in the bitmap.
 * @param {number} y - The y coordinate of the pixel in the bitmap.
 * @returns {string} The pixel color (hex format).
 */
Bitmap.prototype.getPixel = function (x, y) {
  const data = this.context.getImageData(x, y, 1, 1).data;
  let result = "#";
  for (let i = 0; i < 3; i++) {
    result += data[i].toString(16).padZero(2);
  }
  return result;
};

/**
 * Returns alpha pixel value at the specified point.
 *
 * @param {number} x - The x coordinate of the pixel in the bitmap.
 * @param {number} y - The y coordinate of the pixel in the bitmap.
 * @returns {string} The alpha value.
 */
Bitmap.prototype.getAlphaPixel = function (x, y) {
  const data = this.context.getImageData(x, y, 1, 1).data;
  return data[3];
};

/**
 * Clears the specified rectangle.
 *
 * @param {number} x - The x coordinate for the upper-left corner.
 * @param {number} y - The y coordinate for the upper-left corner.
 * @param {number} width - The width of the rectangle to clear.
 * @param {number} height - The height of the rectangle to clear.
 */
Bitmap.prototype.clearRect = function (x, y, width, height) {
  this.context.clearRect(x, y, width, height);
  this._baseTexture.update();
};

/**
 * Clears the entire bitmap.
 */
Bitmap.prototype.clear = function () {
  this.clearRect(0, 0, this.width, this.height);
};

/**
 * Fills the specified rectangle.
 *
 * @param {number} x - The x coordinate for the upper-left corner.
 * @param {number} y - The y coordinate for the upper-left corner.
 * @param {number} width - The width of the rectangle to fill.
 * @param {number} height - The height of the rectangle to fill.
 * @param {string} color - The color of the rectangle in CSS format.
 */
Bitmap.prototype.fillRect = function (x, y, width, height, color) {
  const context = this.context;
  context.save();
  context.fillStyle = color;
  context.fillRect(x, y, width, height);
  context.restore();
  this._baseTexture.update();
};

/**
 * Fills the entire bitmap.
 *
 * @param {string} color - The color of the rectangle in CSS format.
 */
Bitmap.prototype.fillAll = function (color) {
  this.fillRect(0, 0, this.width, this.height, color);
};

/**
 * Draws the specified rectangular frame.
 *
 * @param {number} x - The x coordinate for the upper-left corner.
 * @param {number} y - The y coordinate for the upper-left corner.
 * @param {number} width - The width of the rectangle to fill.
 * @param {number} height - The height of the rectangle to fill.
 * @param {string} color - The color of the rectangle in CSS format.
 */
Bitmap.prototype.strokeRect = function (x, y, width, height, color) {
  const context = this.context;
  context.save();
  context.strokeStyle = color;
  context.strokeRect(x, y, width, height);
  context.restore();
  this._baseTexture.update();
};

// prettier-ignore
/**
 * Draws the rectangle with a gradation.
 *
 * @param {number} x - The x coordinate for the upper-left corner.
 * @param {number} y - The y coordinate for the upper-left corner.
 * @param {number} width - The width of the rectangle to fill.
 * @param {number} height - The height of the rectangle to fill.
 * @param {string} color1 - The gradient starting color.
 * @param {string} color2 - The gradient ending color.
 * @param {boolean} vertical - Whether the gradient should be draw as vertical or not.
 */
Bitmap.prototype.gradientFillRect = function(
    x, y, width, height, color1, color2, vertical
) {
    const context = this.context;
    const x1 = vertical ? x : x + width;
    const y1 = vertical ? y + height : y;
    const grad = context.createLinearGradient(x, y, x1, y1);
    grad.addColorStop(0, color1);
    grad.addColorStop(1, color2);
    context.save();
    context.fillStyle = grad;
    context.fillRect(x, y, width, height);
    context.restore();
    this._baseTexture.update();
};

/**
 * Draws a bitmap in the shape of a circle.
 *
 * @param {number} x - The x coordinate based on the circle center.
 * @param {number} y - The y coordinate based on the circle center.
 * @param {number} radius - The radius of the circle.
 * @param {string} color - The color of the circle in CSS format.
 */
Bitmap.prototype.drawCircle = function (x, y, radius, color) {
  const context = this.context;
  context.save();
  context.fillStyle = color;
  context.beginPath();
  context.arc(x, y, radius, 0, Math.PI * 2, false);
  context.fill();
  context.restore();
  this._baseTexture.update();
};

/**
 * Draws the outline text to the bitmap.
 *
 * @param {string} text - The text that will be drawn.
 * @param {number} x - The x coordinate for the left of the text.
 * @param {number} y - The y coordinate for the top of the text.
 * @param {number} maxWidth - The maximum allowed width of the text.
 * @param {number} lineHeight - The height of the text line.
 * @param {string} align - The alignment of the text.
 */
Bitmap.prototype.drawText = function (text, x, y, maxWidth, lineHeight, align) {
  // [Note] Different browser makes different rendering with
  //   textBaseline == 'top'. So we use 'alphabetic' here.
  const context = this.context;
  const alpha = context.globalAlpha;
  maxWidth = maxWidth || 0xffffffff;
  let tx = x;
  let ty = Math.round(y + lineHeight / 2 + this.fontSize * 0.35);
  if (align === "center") {
    tx += maxWidth / 2;
  }
  if (align === "right") {
    tx += maxWidth;
  }
  context.save();
  context.font = this._makeFontNameText();
  context.textAlign = align;
  context.textBaseline = "alphabetic";
  context.globalAlpha = 1;
  this._drawTextOutline(text, tx, ty, maxWidth);
  context.globalAlpha = alpha;
  this._drawTextBody(text, tx, ty, maxWidth);
  context.restore();
  this._baseTexture.update();
};

Bitmap.prototype.drawTextRotate = function (
  text,
  x,
  y,
  maxWidth,
  lineHeight,
  align
) {
  // [Note] Different browser makes different rendering with
  //   textBaseline == 'top'. So we use 'alphabetic' here.
  const context = this.context;
  const alpha = context.globalAlpha;
  maxWidth = maxWidth || 0xffffffff;
  let tx = x;
  let ty = Math.round(y + lineHeight / 2 + this.fontSize * 0.35);
  if (align === "center") {
    tx += maxWidth / 2;
  }
  if (align === "right") {
    tx += maxWidth;
  }
  context.save();
  context.font = this._makeFontNameText();
  context.textAlign = align;
  context.textBaseline = "alphabetic";
  context.globalAlpha = 1;
  context.translate(tx - 7, ty - 7);
  context.rotate(Math.PI / 2);

  this._drawTextOutline(text, 0, 0, maxWidth);
  context.globalAlpha = alpha;
  this._drawTextBody(text, 0, 0, maxWidth);
  context.restore();
  this._baseTexture.update();
};
/**
 * Returns the width of the specified text.
 *
 * @param {string} text - The text to be measured.
 * @returns {number} The width of the text in pixels.
 */
Bitmap.prototype.measureTextWidth = function (text) {
  const context = this.context;
  context.save();
  context.font = this._makeFontNameText();
  const width = context.measureText(text).width;
  context.restore();
  return width;
};

/**
 * Adds a callback function that will be called when the bitmap is loaded.
 *
 * @param {function} listner - The callback function.
 */
Bitmap.prototype.addLoadListener = function (listner) {
  if (!this.isReady()) {
    this._loadListeners.push(listner);
  } else {
    listner(this);
  }
};

/**
 * Tries to load the image again.
 */
Bitmap.prototype.retry = function () {
  this._startLoading();
};

Bitmap.prototype._makeFontNameText = function () {
  const italic = this.fontItalic ? "Italic " : "";
  const bold = this.fontBold ? "Bold " : "";
  return italic + bold + this.fontSize + "px " + this.fontFace;
};

Bitmap.prototype._drawTextOutline = function (text, tx, ty, maxWidth) {
  const context = this.context;
  context.strokeStyle = this.outlineColor;
  context.lineWidth = this.outlineWidth;

  context.lineJoin = "round";
  context.strokeText(text, tx, ty, maxWidth);
};

Bitmap.prototype._drawTextBody = function (text, tx, ty, maxWidth) {
  const context = this.context;
  context.fillStyle = this.textColor;
  context.fillText(text, tx, ty, maxWidth);
};

Bitmap.prototype._createCanvas = function (width, height) {
  this._canvas = document.createElement("canvas");
  this._context = this._canvas.getContext("2d");
  this._canvas.width = width;
  this._canvas.height = height;
  this._createBaseTexture(this._canvas);
};

Bitmap.prototype._ensureCanvas = function () {
  if (!this._canvas) {
    if (this._image) {
      this._createCanvas(this._image.width, this._image.height);
      this._context.drawImage(this._image, 0, 0);
    } else {
      this._createCanvas(0, 0);
    }
  }
};

Bitmap.prototype._destroyCanvas = function () {
  if (this._canvas) {
    this._canvas.width = 0;
    this._canvas.height = 0;
    this._canvas = null;
  }
};

Bitmap.prototype._createBaseTexture = function (source) {
  this._baseTexture = new PIXI.BaseTexture(source);
  this._baseTexture.mipmap = false;
  this._baseTexture.width = source.width;
  this._baseTexture.height = source.height;
  this._updateScaleMode();
};

Bitmap.prototype._updateScaleMode = function () {
  if (this._baseTexture) {
    if (this._smooth) {
      this._baseTexture.scaleMode = PIXI.SCALE_MODES.LINEAR;
    } else {
      this._baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
    }
  }
};

Bitmap.prototype._startLoading = function () {
  this._image = new Image();
  this._image.onload = this._onLoad.bind(this);
  this._image.onerror = this._onError.bind(this);
  this._destroyCanvas();
  this._loadingState = "loading";
  if (Utils.hasEncryptedImages()) {
    this._startDecrypting();
  } else {
    this._image.src = this._url;
    if (this._image.width > 0) {
      this._image.onload = null;
      this._onLoad();
    }
  }
};

Bitmap.prototype._startDecrypting = function () {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", this._url + "_");
  xhr.responseType = "arraybuffer";
  xhr.onload = () => this._onXhrLoad(xhr);
  xhr.onerror = this._onError.bind(this);
  xhr.send();
};

Bitmap.prototype._onXhrLoad = function (xhr) {
  if (xhr.status < 400) {
    const arrayBuffer = Utils.decryptArrayBuffer(xhr.response);
    const blob = new Blob([arrayBuffer]);
    this._image.src = URL.createObjectURL(blob);
  } else {
    this._onError();
  }
};

Bitmap.prototype._onLoad = function () {
  if (Utils.hasEncryptedImages()) {
    URL.revokeObjectURL(this._image.src);
  }
  this._loadingState = "loaded";
  this._createBaseTexture(this._image);
  this._callLoadListeners();
};

Bitmap.prototype._callLoadListeners = function () {
  while (this._loadListeners.length > 0) {
    const listener = this._loadListeners.shift();
    listener(this);
  }
};

Bitmap.prototype._onError = function () {
  this._loadingState = "error";
};

//-----------------------------------------------------------------------------
/**
 * The basic object that is rendered to the game screen.
 *
 * @class
 * @extends PIXI.Sprite
 * @param {Bitmap} bitmap - The image for the sprite.
 */
function Sprite() {
  this.initialize(...arguments);
}

Sprite.prototype = Object.create(PIXI.Sprite.prototype);
Sprite.prototype.constructor = Sprite;

Sprite.prototype.initialize = function (bitmap) {
  if (!Sprite._emptyBaseTexture) {
    Sprite._emptyBaseTexture = new PIXI.BaseTexture();
    Sprite._emptyBaseTexture.setSize(1, 1);
  }
  const frame = new Rectangle();
  const texture = new PIXI.Texture(Sprite._emptyBaseTexture, frame);
  PIXI.Sprite.call(this, texture);
  this.spriteId = Sprite._counter++;
  this._bitmap = bitmap;
  this._frame = frame;
  this._hue = 0;
  this._blendColor = [0, 0, 0, 0];
  this._colorTone = [0, 0, 0, 0];
  this._colorFilter = null;
  this._blendMode = PIXI.BLEND_MODES.NORMAL;
  this._hidden = false;
  this._onBitmapChange();
};

Sprite._emptyBaseTexture = null;
Sprite._counter = 0;

/**
 * The image for the sprite.
 *
 * @type Bitmap
 * @name Sprite#bitmap
 */
Object.defineProperty(Sprite.prototype, "bitmap", {
  get: function () {
    return this._bitmap;
  },
  set: function (value) {
    if (this._bitmap !== value) {
      this._bitmap = value;
      this._onBitmapChange();
    }
  },
  configurable: true,
});

/**
 * The width of the sprite without the scale.
 *
 * @type number
 * @name Sprite#width
 */
Object.defineProperty(Sprite.prototype, "width", {
  get: function () {
    return this._frame.width;
  },
  set: function (value) {
    this._frame.width = value;
    this._refresh();
  },
  configurable: true,
});

/**
 * The height of the sprite without the scale.
 *
 * @type number
 * @name Sprite#height
 */
Object.defineProperty(Sprite.prototype, "height", {
  get: function () {
    return this._frame.height;
  },
  set: function (value) {
    this._frame.height = value;
    this._refresh();
  },
  configurable: true,
});

/**
 * The opacity of the sprite (0 to 255).
 *
 * @type number
 * @name Sprite#opacity
 */
Object.defineProperty(Sprite.prototype, "opacity", {
  get: function () {
    return this.alpha * 255;
  },
  set: function (value) {
    this.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * The blend mode to be applied to the sprite.
 *
 * @type number
 * @name Sprite#blendMode
 */
Object.defineProperty(Sprite.prototype, "blendMode", {
  get: function () {
    if (this._colorFilter) {
      return this._colorFilter.blendMode;
    } else {
      return this._blendMode;
    }
  },
  set: function (value) {
    this._blendMode = value;
    if (this._colorFilter) {
      this._colorFilter.blendMode = value;
    }
  },
  configurable: true,
});

/**
 * Destroys the sprite.
 */
Sprite.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Sprite.prototype.destroy.call(this, options);
};

/**
 * Updates the sprite for each frame.
 */
Sprite.prototype.update = function () {
  for (const child of this.children) {
    if (child.update) {
      child.update();
    }
  }
};

/**
 * Makes the sprite "hidden".
 */
Sprite.prototype.hide = function () {
  this._hidden = true;
  this.updateVisibility();
};

/**
 * Releases the "hidden" state of the sprite.
 */
Sprite.prototype.show = function () {
  this._hidden = false;
  this.updateVisibility();
};

/**
 * Reflects the "hidden" state of the sprite to the visible state.
 */
Sprite.prototype.updateVisibility = function () {
  this.visible = !this._hidden;
};

/**
 * Sets the x and y at once.
 *
 * @param {number} x - The x coordinate of the sprite.
 * @param {number} y - The y coordinate of the sprite.
 */
Sprite.prototype.move = function (x, y) {
  this.x = x;
  this.y = y;
};

/**
 * Sets the rectagle of the bitmap that the sprite displays.
 *
 * @param {number} x - The x coordinate of the frame.
 * @param {number} y - The y coordinate of the frame.
 * @param {number} width - The width of the frame.
 * @param {number} height - The height of the frame.
 */
Sprite.prototype.setFrame = function (x, y, width, height) {
  this._refreshFrame = false;
  const frame = this._frame;
  if (
    x !== frame.x ||
    y !== frame.y ||
    width !== frame.width ||
    height !== frame.height
  ) {
    frame.x = x;
    frame.y = y;
    frame.width = width;
    frame.height = height;
    this._refresh();
  }
};

/**
 * Sets the hue rotation value.
 *
 * @param {number} hue - The hue value (-360, 360).
 */
Sprite.prototype.setHue = function (hue) {
  if (this._hue !== Number(hue)) {
    this._hue = Number(hue);
    this._updateColorFilter();
  }
};

/**
 * Gets the blend color for the sprite.
 *
 * @returns {array} The blend color [r, g, b, a].
 */
Sprite.prototype.getBlendColor = function () {
  return this._blendColor.clone();
};

/**
 * Sets the blend color for the sprite.
 *
 * @param {array} color - The blend color [r, g, b, a].
 */
Sprite.prototype.setBlendColor = function (color) {
  if (!(color instanceof Array)) {
    throw new Error("Argument must be an array");
  }
  if (!this._blendColor.equals(color)) {
    this._blendColor = color.clone();
    this._updateColorFilter();
  }
};

/**
 * Gets the color tone for the sprite.
 *
 * @returns {array} The color tone [r, g, b, gray].
 */
Sprite.prototype.getColorTone = function () {
  return this._colorTone.clone();
};

/**
 * Sets the color tone for the sprite.
 *
 * @param {array} tone - The color tone [r, g, b, gray].
 */
Sprite.prototype.setColorTone = function (tone) {
  if (!(tone instanceof Array)) {
    throw new Error("Argument must be an array");
  }
  if (!this._colorTone.equals(tone)) {
    this._colorTone = tone.clone();
    this._updateColorFilter();
  }
};

Sprite.prototype._onBitmapChange = function () {
  if (this._bitmap) {
    this._refreshFrame = true;
    this._bitmap.addLoadListener(this._onBitmapLoad.bind(this));
  } else {
    this._refreshFrame = false;
    this.texture.frame = new Rectangle();
  }
};

Sprite.prototype._onBitmapLoad = function (bitmapLoaded) {
  if (bitmapLoaded === this._bitmap) {
    if (this._refreshFrame && this._bitmap) {
      this._refreshFrame = false;
      this._frame.width = this._bitmap.width;
      this._frame.height = this._bitmap.height;
    }
  }
  this._refresh();
};

Sprite.prototype._refresh = function () {
  const texture = this.texture;
  const frameX = Math.floor(this._frame.x);
  const frameY = Math.floor(this._frame.y);
  const frameW = Math.floor(this._frame.width);
  const frameH = Math.floor(this._frame.height);
  const baseTexture = this._bitmap ? this._bitmap.baseTexture : null;
  const baseTextureW = baseTexture ? baseTexture.width : 0;
  const baseTextureH = baseTexture ? baseTexture.height : 0;
  const realX = frameX.clamp(0, baseTextureW);
  const realY = frameY.clamp(0, baseTextureH);
  const realW = (frameW - realX + frameX).clamp(0, baseTextureW - realX);
  const realH = (frameH - realY + frameY).clamp(0, baseTextureH - realY);
  const frame = new Rectangle(realX, realY, realW, realH);
  if (texture) {
    this.pivot.x = frameX - realX;
    this.pivot.y = frameY - realY;
    if (baseTexture) {
      texture.baseTexture = baseTexture;
      try {
        texture.frame = frame;
      } catch (e) {
        texture.frame = new Rectangle();
      }
    }
    texture._updateID++;
  }
};

Sprite.prototype._createColorFilter = function () {
  this._colorFilter = new ColorFilter();
  if (!this.filters) {
    this.filters = [];
  }
  this.filters.push(this._colorFilter);
};

Sprite.prototype._updateColorFilter = function () {
  if (!this._colorFilter) {
    this._createColorFilter();
  }
  this._colorFilter.setHue(this._hue);
  this._colorFilter.setBlendColor(this._blendColor);
  this._colorFilter.setColorTone(this._colorTone);
};

//-----------------------------------------------------------------------------
/**
 * The tilemap which displays 2D tile-based game map.
 *
 * @class
 * @extends PIXI.Container
 */
function Tilemap() {
  this.initialize(...arguments);
}

Tilemap.prototype = Object.create(PIXI.Container.prototype);
Tilemap.prototype.constructor = Tilemap;

Tilemap.prototype.initialize = function () {
  PIXI.Container.call(this);

  this._width = Graphics.width;
  this._height = Graphics.height;
  this._margin = 20;
  this._mapWidth = 0;
  this._mapHeight = 0;
  this._mapData = null;
  this._bitmaps = [];

  /**
   * The width of each tile.
   *
   * @type number
   */
  this.tileWidth = 48;

  /**
   * The height of each tile.
   *
   * @type number
   */
  this.tileHeight = 48;

  /**
   * The origin point of the tilemap for scrolling.
   *
   * @type Point
   */
  this.origin = new Point();

  /**
   * The tileset flags.
   *
   * @type array
   */
  this.flags = [];

  /**
   * The animation count for autotiles.
   *
   * @type number
   */
  this.animationCount = 0;

  /**
   * Whether the tilemap loops horizontal.
   *
   * @type boolean
   */
  this.horizontalWrap = false;

  /**
   * Whether the tilemap loops vertical.
   *
   * @type boolean
   */
  this.verticalWrap = false;

  this._createLayers();
  this.refresh();
};

/**
 * The width of the tilemap.
 *
 * @type number
 * @name Tilemap#width
 */
Object.defineProperty(Tilemap.prototype, "width", {
  get: function () {
    return this._width;
  },
  set: function (value) {
    this._width = value;
  },
  configurable: true,
});

/**
 * The height of the tilemap.
 *
 * @type number
 * @name Tilemap#height
 */
Object.defineProperty(Tilemap.prototype, "height", {
  get: function () {
    return this._height;
  },
  set: function (value) {
    this._height = value;
  },
  configurable: true,
});

/**
 * Destroys the tilemap.
 */
Tilemap.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
};

/**
 * Sets the tilemap data.
 *
 * @param {number} width - The width of the map in number of tiles.
 * @param {number} height - The height of the map in number of tiles.
 * @param {array} data - The one dimensional array for the map data.
 */
Tilemap.prototype.setData = function (width, height, data) {
  this._mapWidth = width;
  this._mapHeight = height;
  this._mapData = data;
};

/**
 * Checks whether the tileset is ready to render.
 *
 * @type boolean
 * @returns {boolean} True if the tilemap is ready.
 */
Tilemap.prototype.isReady = function () {
  for (const bitmap of this._bitmaps) {
    if (bitmap && !bitmap.isReady()) {
      return false;
    }
  }
  return true;
};

/**
 * Updates the tilemap for each frame.
 */
Tilemap.prototype.update = function () {
  this.animationCount++;
  this.animationFrame = Math.floor(this.animationCount / 30);
  for (const child of this.children) {
    if (child.update) {
      child.update();
    }
  }
};

/**
 * Sets the bitmaps used as a tileset.
 *
 * @param {array} bitmaps - The array of the tileset bitmaps.
 */
Tilemap.prototype.setBitmaps = function (bitmaps) {
  // [Note] We wait for the images to finish loading. Creating textures
  //   from bitmaps that are not yet loaded here brings some maintenance
  //   difficulties. e.g. PIXI overwrites img.onload internally.
  this._bitmaps = bitmaps;
  const listener = this._updateBitmaps.bind(this);
  for (const bitmap of this._bitmaps) {
    if (!bitmap.isReady()) {
      bitmap.addLoadListener(listener);
    }
  }
  this._needsBitmapsUpdate = true;
  this._updateBitmaps();
};

/**
 * Forces to repaint the entire tilemap.
 */
Tilemap.prototype.refresh = function () {
  this._needsRepaint = true;
};

/**
 * Updates the transform on all children of this container for rendering.
 */
Tilemap.prototype.updateTransform = function () {
  const ox = Math.ceil(this.origin.x);
  const oy = Math.ceil(this.origin.y);
  const startX = Math.floor((ox - this._margin) / this.tileWidth);
  const startY = Math.floor((oy - this._margin) / this.tileHeight);
  this._lowerLayer.x = startX * this.tileWidth - ox;
  this._lowerLayer.y = startY * this.tileHeight - oy;
  this._upperLayer.x = startX * this.tileWidth - ox;
  this._upperLayer.y = startY * this.tileHeight - oy;
  if (
    this._needsRepaint ||
    this._lastAnimationFrame !== this.animationFrame ||
    this._lastStartX !== startX ||
    this._lastStartY !== startY
  ) {
    this._lastAnimationFrame = this.animationFrame;
    this._lastStartX = startX;
    this._lastStartY = startY;
    this._addAllSpots(startX, startY);
    this._needsRepaint = false;
  }
  this._sortChildren();
  PIXI.Container.prototype.updateTransform.call(this);
};

Tilemap.prototype._createLayers = function () {
  /*
   * [Z coordinate]
   *  0 : Lower tiles
   *  1 : Lower characters
   *  3 : Normal characters
   *  4 : Upper tiles
   *  5 : Upper characters
   *  6 : Airship shadow
   *  7 : Balloon
   *  8 : Animation
   *  9 : Destination
   */
  this._lowerLayer = new Tilemap.CombinedLayer();
  this._lowerLayer.z = 0;
  this._upperLayer = new Tilemap.CombinedLayer();
  this._upperLayer.z = 4;
  this.addChild(this._lowerLayer);
  this.addChild(this._upperLayer);
  this._needsRepaint = true;
};

Tilemap.prototype._updateBitmaps = function () {
  if (this._needsBitmapsUpdate && this.isReady()) {
    this._lowerLayer.setBitmaps(this._bitmaps);
    this._needsBitmapsUpdate = false;
    this._needsRepaint = true;
  }
};

Tilemap.prototype._addAllSpots = function (startX, startY) {
  this._lowerLayer.clear();
  this._upperLayer.clear();
  const widthWithMatgin = this.width + this._margin * 2;
  const heightWithMatgin = this.height + this._margin * 2;
  const tileCols = Math.ceil(widthWithMatgin / this.tileWidth) + 1;
  const tileRows = Math.ceil(heightWithMatgin / this.tileHeight) + 1;
  for (let y = 0; y < tileRows; y++) {
    for (let x = 0; x < tileCols; x++) {
      //p(startX + ' ' + startY + ' ' + x + ' ' + y)
      this._addSpot(startX, startY, x, y);
    }
  }
};

Tilemap.prototype._addSpot = function (startX, startY, x, y) {
  const mx = startX + x;
  const my = startY + y;
  const dx = x * this.tileWidth;
  const dy = y * this.tileHeight;
  const tileId0 = this._readMapData(mx, my, 0);
  //p(tileId0)
  const tileId1 = this._readMapData(mx, my, 1);
  const tileId2 = this._readMapData(mx, my, 2);
  const tileId3 = this._readMapData(mx, my, 3);
  const shadowBits = this._readMapData(mx, my, 4);
  const upperTileId1 = this._readMapData(mx, my - 1, 1);

  this._addSpotTile(tileId0, dx, dy);
  this._addSpotTile(tileId1, dx, dy);
  this._addShadow(this._lowerLayer, shadowBits, dx, dy);
  if (this._isTableTile(upperTileId1) && !this._isTableTile(tileId1)) {
    if (!Tilemap.isShadowingTile(tileId0)) {
      this._addTableEdge(this._lowerLayer, upperTileId1, dx, dy);
    }
  }
  if (this._isOverpassPosition(mx, my)) {
    this._addTile(this._upperLayer, tileId2, dx, dy);
    this._addTile(this._upperLayer, tileId3, dx, dy);
  } else {
    this._addSpotTile(tileId2, dx, dy);
    this._addSpotTile(tileId3, dx, dy);
  }
};

Tilemap.prototype._addSpotTile = function (tileId, dx, dy) {
  if (this._isHigherTile(tileId)) {
    this._addTile(this._upperLayer, tileId, dx, dy);
  } else {
    this._addTile(this._lowerLayer, tileId, dx, dy);
  }
};

Tilemap.prototype._addTile = function (layer, tileId, dx, dy) {
  if (Tilemap.isVisibleTile(tileId)) {
    if (Tilemap.isAutotile(tileId)) {
      this._addAutotile(layer, tileId, dx, dy);
    } else {
      this._addNormalTile(layer, tileId, dx, dy);
    }
  }
};

Tilemap.prototype._addNormalTile = function (layer, tileId, dx, dy) {
  let setNumber = 0;

  if (Tilemap.isTileA5(tileId)) {
    setNumber = 4;
  } else {
    setNumber = 5 + Math.floor(tileId / 256);
  }

  const w = this.tileWidth;
  const h = this.tileHeight;
  const sx = ((Math.floor(tileId / 128) % 2) * 8 + (tileId % 8)) * w;
  const sy = (Math.floor((tileId % 256) / 8) % 16) * h;

  layer.addRect(setNumber, sx, sy, dx, dy, w, h);
};

Tilemap.prototype._addAutotile = function (layer, tileId, dx, dy) {
  const kind = Tilemap.getAutotileKind(tileId);
  const shape = Tilemap.getAutotileShape(tileId);
  const tx = kind % 8;
  const ty = Math.floor(kind / 8);
  let setNumber = 0;
  let bx = 0;
  let by = 0;
  let autotileTable = Tilemap.FLOOR_AUTOTILE_TABLE;
  let isTable = false;

  if (Tilemap.isTileA1(tileId)) {
    const waterSurfaceIndex = [0, 1, 2, 1][this.animationFrame % 4];
    setNumber = 0;
    if (kind === 0) {
      bx = waterSurfaceIndex * 2;
      by = 0;
    } else if (kind === 1) {
      bx = waterSurfaceIndex * 2;
      by = 3;
    } else if (kind === 2) {
      bx = 6;
      by = 0;
    } else if (kind === 3) {
      bx = 6;
      by = 3;
    } else {
      bx = Math.floor(tx / 4) * 8;
      by = ty * 6 + (Math.floor(tx / 2) % 2) * 3;
      if (kind % 2 === 0) {
        bx += waterSurfaceIndex * 2;
      } else {
        bx += 6;
        autotileTable = Tilemap.WATERFALL_AUTOTILE_TABLE;
        by += this.animationFrame % 3;
      }
    }
  } else if (Tilemap.isTileA2(tileId)) {
    setNumber = 1;
    bx = tx * 2;
    by = (ty - 2) * 3;
    isTable = this._isTableTile(tileId);
  } else if (Tilemap.isTileA3(tileId)) {
    setNumber = 2;
    bx = tx * 2;
    by = (ty - 6) * 2;
    autotileTable = Tilemap.WALL_AUTOTILE_TABLE;
  } else if (Tilemap.isTileA4(tileId)) {
    setNumber = 3;
    bx = tx * 2;
    by = Math.floor((ty - 10) * 2.5 + (ty % 2 === 1 ? 0.5 : 0));
    if (ty % 2 === 1) {
      autotileTable = Tilemap.WALL_AUTOTILE_TABLE;
    }
  }

  const table = autotileTable[shape];
  const w1 = this.tileWidth / 2;
  const h1 = this.tileHeight / 2;
  for (let i = 0; i < 4; i++) {
    const qsx = table[i][0];
    const qsy = table[i][1];
    const sx1 = (bx * 2 + qsx) * w1;
    const sy1 = (by * 2 + qsy) * h1;
    const dx1 = dx + (i % 2) * w1;
    const dy1 = dy + Math.floor(i / 2) * h1;
    if (isTable && (qsy === 1 || qsy === 5)) {
      const qsx2 = qsy === 1 ? (4 - qsx) % 4 : qsx;
      const qsy2 = 3;
      const sx2 = (bx * 2 + qsx2) * w1;
      const sy2 = (by * 2 + qsy2) * h1;
      layer.addRect(setNumber, sx2, sy2, dx1, dy1, w1, h1);
      layer.addRect(setNumber, sx1, sy1, dx1, dy1 + h1 / 2, w1, h1 / 2);
    } else {
      layer.addRect(setNumber, sx1, sy1, dx1, dy1, w1, h1);
    }
  }
};

Tilemap.prototype._addTableEdge = function (layer, tileId, dx, dy) {
  if (Tilemap.isTileA2(tileId)) {
    const autotileTable = Tilemap.FLOOR_AUTOTILE_TABLE;
    const kind = Tilemap.getAutotileKind(tileId);
    const shape = Tilemap.getAutotileShape(tileId);
    const tx = kind % 8;
    const ty = Math.floor(kind / 8);
    const setNumber = 1;
    const bx = tx * 2;
    const by = (ty - 2) * 3;
    const table = autotileTable[shape];
    const w1 = this.tileWidth / 2;
    const h1 = this.tileHeight / 2;
    for (let i = 0; i < 2; i++) {
      const qsx = table[2 + i][0];
      const qsy = table[2 + i][1];
      const sx1 = (bx * 2 + qsx) * w1;
      const sy1 = (by * 2 + qsy) * h1 + h1 / 2;
      const dx1 = dx + (i % 2) * w1;
      const dy1 = dy + Math.floor(i / 2) * h1;
      layer.addRect(setNumber, sx1, sy1, dx1, dy1, w1, h1 / 2);
    }
  }
};

Tilemap.prototype._addShadow = function (layer, shadowBits, dx, dy) {
  if (shadowBits & 0x0f) {
    const w1 = this.tileWidth / 2;
    const h1 = this.tileHeight / 2;
    for (let i = 0; i < 4; i++) {
      if (shadowBits & (1 << i)) {
        const dx1 = dx + (i % 2) * w1;
        const dy1 = dy + Math.floor(i / 2) * h1;
        layer.addRect(-1, 0, 0, dx1, dy1, w1, h1);
      }
    }
  }
};

Tilemap.prototype._readMapData = function (x, y, z) {
  if (this._mapData) {
    const width = this._mapWidth;
    const height = this._mapHeight;
    if (this.horizontalWrap) {
      x = x.mod(width);
    }
    if (this.verticalWrap) {
      y = y.mod(height);
    }
    if (x >= 0 && x < width && y >= 0 && y < height) {
      return this._mapData[(z * height + y) * width + x] || 0;
    } else {
      return 0;
    }
  } else {
    return 0;
  }
};

Tilemap.prototype._isHigherTile = function (tileId) {
  return this.flags[tileId] & 0x10;
};

Tilemap.prototype._isTableTile = function (tileId) {
  return Tilemap.isTileA2(tileId) && this.flags[tileId] & 0x80;
};

Tilemap.prototype._isOverpassPosition = function (/*mx, my*/) {
  return false;
};

Tilemap.prototype._sortChildren = function () {
  this.children.sort(this._compareChildOrder.bind(this));
};

Tilemap.prototype._compareChildOrder = function (a, b) {
  if (a.z !== b.z) {
    return a.z - b.z;
  } else if (a.y !== b.y) {
    return a.y - b.y;
  } else {
    return a.spriteId - b.spriteId;
  }
};

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Tile type checkers

Tilemap.TILE_ID_B = 0;
Tilemap.TILE_ID_C = 256;
Tilemap.TILE_ID_D = 512;
Tilemap.TILE_ID_E = 768;
Tilemap.TILE_ID_A5 = 1536;
Tilemap.TILE_ID_A1 = 2048;
Tilemap.TILE_ID_A2 = 2816;
Tilemap.TILE_ID_A3 = 4352;
Tilemap.TILE_ID_A4 = 5888;
Tilemap.TILE_ID_MAX = 8192;

Tilemap.isVisibleTile = function (tileId) {
  return tileId > 0 && tileId < this.TILE_ID_MAX;
};

Tilemap.isAutotile = function (tileId) {
  return tileId >= this.TILE_ID_A1;
};

Tilemap.getAutotileKind = function (tileId) {
  return Math.floor((tileId - this.TILE_ID_A1) / 48);
};

Tilemap.getAutotileShape = function (tileId) {
  return (tileId - this.TILE_ID_A1) % 48;
};

Tilemap.makeAutotileId = function (kind, shape) {
  return this.TILE_ID_A1 + kind * 48 + shape;
};

Tilemap.isSameKindTile = function (tileID1, tileID2) {
  if (this.isAutotile(tileID1) && this.isAutotile(tileID2)) {
    return this.getAutotileKind(tileID1) === this.getAutotileKind(tileID2);
  } else {
    return tileID1 === tileID2;
  }
};

Tilemap.isTileA1 = function (tileId) {
  return tileId >= this.TILE_ID_A1 && tileId < this.TILE_ID_A2;
};

Tilemap.isTileA2 = function (tileId) {
  return tileId >= this.TILE_ID_A2 && tileId < this.TILE_ID_A3;
};

Tilemap.isTileA3 = function (tileId) {
  return tileId >= this.TILE_ID_A3 && tileId < this.TILE_ID_A4;
};

Tilemap.isTileA4 = function (tileId) {
  return tileId >= this.TILE_ID_A4 && tileId < this.TILE_ID_MAX;
};

Tilemap.isTileA5 = function (tileId) {
  return tileId >= this.TILE_ID_A5 && tileId < this.TILE_ID_A1;
};

Tilemap.isWaterTile = function (tileId) {
  if (this.isTileA1(tileId)) {
    return !(tileId >= this.TILE_ID_A1 + 96 && tileId < this.TILE_ID_A1 + 192);
  } else {
    return false;
  }
};

Tilemap.isWaterfallTile = function (tileId) {
  if (tileId >= this.TILE_ID_A1 + 192 && tileId < this.TILE_ID_A2) {
    return this.getAutotileKind(tileId) % 2 === 1;
  } else {
    return false;
  }
};

Tilemap.isGroundTile = function (tileId) {
  return (
    this.isTileA1(tileId) || this.isTileA2(tileId) || this.isTileA5(tileId)
  );
};

Tilemap.isShadowingTile = function (tileId) {
  return this.isTileA3(tileId) || this.isTileA4(tileId);
};

Tilemap.isRoofTile = function (tileId) {
  return this.isTileA3(tileId) && this.getAutotileKind(tileId) % 16 < 8;
};

Tilemap.isWallTopTile = function (tileId) {
  return this.isTileA4(tileId) && this.getAutotileKind(tileId) % 16 < 8;
};

Tilemap.isWallSideTile = function (tileId) {
  return (
    (this.isTileA3(tileId) || this.isTileA4(tileId)) &&
    this.getAutotileKind(tileId) % 16 >= 8
  );
};

Tilemap.isWallTile = function (tileId) {
  return this.isWallTopTile(tileId) || this.isWallSideTile(tileId);
};

Tilemap.isFloorTypeAutotile = function (tileId) {
  return (
    (this.isTileA1(tileId) && !this.isWaterfallTile(tileId)) ||
    this.isTileA2(tileId) ||
    this.isWallTopTile(tileId)
  );
};

Tilemap.isWallTypeAutotile = function (tileId) {
  return this.isRoofTile(tileId) || this.isWallSideTile(tileId);
};

Tilemap.isWaterfallTypeAutotile = function (tileId) {
  return this.isWaterfallTile(tileId);
};

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Autotile shape number to coordinates of tileset images

// prettier-ignore
Tilemap.FLOOR_AUTOTILE_TABLE = [
    [[2, 4], [1, 4], [2, 3], [1, 3]],
    [[2, 0], [1, 4], [2, 3], [1, 3]],
    [[2, 4], [3, 0], [2, 3], [1, 3]],
    [[2, 0], [3, 0], [2, 3], [1, 3]],
    [[2, 4], [1, 4], [2, 3], [3, 1]],
    [[2, 0], [1, 4], [2, 3], [3, 1]],
    [[2, 4], [3, 0], [2, 3], [3, 1]],
    [[2, 0], [3, 0], [2, 3], [3, 1]],
    [[2, 4], [1, 4], [2, 1], [1, 3]],
    [[2, 0], [1, 4], [2, 1], [1, 3]],
    [[2, 4], [3, 0], [2, 1], [1, 3]],
    [[2, 0], [3, 0], [2, 1], [1, 3]],
    [[2, 4], [1, 4], [2, 1], [3, 1]],
    [[2, 0], [1, 4], [2, 1], [3, 1]],
    [[2, 4], [3, 0], [2, 1], [3, 1]],
    [[2, 0], [3, 0], [2, 1], [3, 1]],
    [[0, 4], [1, 4], [0, 3], [1, 3]],
    [[0, 4], [3, 0], [0, 3], [1, 3]],
    [[0, 4], [1, 4], [0, 3], [3, 1]],
    [[0, 4], [3, 0], [0, 3], [3, 1]],
    [[2, 2], [1, 2], [2, 3], [1, 3]],
    [[2, 2], [1, 2], [2, 3], [3, 1]],
    [[2, 2], [1, 2], [2, 1], [1, 3]],
    [[2, 2], [1, 2], [2, 1], [3, 1]],
    [[2, 4], [3, 4], [2, 3], [3, 3]],
    [[2, 4], [3, 4], [2, 1], [3, 3]],
    [[2, 0], [3, 4], [2, 3], [3, 3]],
    [[2, 0], [3, 4], [2, 1], [3, 3]],
    [[2, 4], [1, 4], [2, 5], [1, 5]],
    [[2, 0], [1, 4], [2, 5], [1, 5]],
    [[2, 4], [3, 0], [2, 5], [1, 5]],
    [[2, 0], [3, 0], [2, 5], [1, 5]],
    [[0, 4], [3, 4], [0, 3], [3, 3]],
    [[2, 2], [1, 2], [2, 5], [1, 5]],
    [[0, 2], [1, 2], [0, 3], [1, 3]],
    [[0, 2], [1, 2], [0, 3], [3, 1]],
    [[2, 2], [3, 2], [2, 3], [3, 3]],
    [[2, 2], [3, 2], [2, 1], [3, 3]],
    [[2, 4], [3, 4], [2, 5], [3, 5]],
    [[2, 0], [3, 4], [2, 5], [3, 5]],
    [[0, 4], [1, 4], [0, 5], [1, 5]],
    [[0, 4], [3, 0], [0, 5], [1, 5]],
    [[0, 2], [3, 2], [0, 3], [3, 3]],
    [[0, 2], [1, 2], [0, 5], [1, 5]],
    [[0, 4], [3, 4], [0, 5], [3, 5]],
    [[2, 2], [3, 2], [2, 5], [3, 5]],
    [[0, 2], [3, 2], [0, 5], [3, 5]],
    [[0, 0], [1, 0], [0, 1], [1, 1]]
];

// prettier-ignore
Tilemap.WALL_AUTOTILE_TABLE = [
    [[2, 2], [1, 2], [2, 1], [1, 1]],
    [[0, 2], [1, 2], [0, 1], [1, 1]],
    [[2, 0], [1, 0], [2, 1], [1, 1]],
    [[0, 0], [1, 0], [0, 1], [1, 1]],
    [[2, 2], [3, 2], [2, 1], [3, 1]],
    [[0, 2], [3, 2], [0, 1], [3, 1]],
    [[2, 0], [3, 0], [2, 1], [3, 1]],
    [[0, 0], [3, 0], [0, 1], [3, 1]],
    [[2, 2], [1, 2], [2, 3], [1, 3]],
    [[0, 2], [1, 2], [0, 3], [1, 3]],
    [[2, 0], [1, 0], [2, 3], [1, 3]],
    [[0, 0], [1, 0], [0, 3], [1, 3]],
    [[2, 2], [3, 2], [2, 3], [3, 3]],
    [[0, 2], [3, 2], [0, 3], [3, 3]],
    [[2, 0], [3, 0], [2, 3], [3, 3]],
    [[0, 0], [3, 0], [0, 3], [3, 3]]
];

// prettier-ignore
Tilemap.WATERFALL_AUTOTILE_TABLE = [
    [[2, 0], [1, 0], [2, 1], [1, 1]],
    [[0, 0], [1, 0], [0, 1], [1, 1]],
    [[2, 0], [3, 0], [2, 1], [3, 1]],
    [[0, 0], [3, 0], [0, 1], [3, 1]]
];

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Internal classes

Tilemap.Layer = function () {
  this.initialize(...arguments);
};

Tilemap.Layer.prototype = Object.create(PIXI.Container.prototype);
Tilemap.Layer.prototype.constructor = Tilemap.Layer;

Tilemap.Layer.prototype.initialize = function () {
  PIXI.Container.call(this);
  this._elements = [];
  this._indexBuffer = null;
  this._indexArray = new Float32Array(0);
  this._vertexBuffer = null;
  this._vertexArray = new Float32Array(0);
  this._vao = null;
  this._needsTexturesUpdate = false;
  this._needsVertexUpdate = false;
  this._images = [];
  this._state = PIXI.State.for2d();
  this._createVao();
};

Tilemap.Layer.MAX_GL_TEXTURES = 3;
Tilemap.Layer.VERTEX_STRIDE = 9 * 4;
Tilemap.Layer.MAX_SIZE = 16000;

Tilemap.Layer.prototype.destroy = function () {
  if (this._vao) {
    this._vao.destroy();
    this._indexBuffer.destroy();
    this._vertexBuffer.destroy();
  }
  this._indexBuffer = null;
  this._vertexBuffer = null;
  this._vao = null;
};

Tilemap.Layer.prototype.setBitmaps = function (bitmaps) {
  this._images = bitmaps.map((bitmap) => bitmap.image || bitmap.canvas);
  this._needsTexturesUpdate = true;
};

Tilemap.Layer.prototype.clear = function () {
  this._elements.length = 0;
  this._needsVertexUpdate = true;
};

Tilemap.Layer.prototype.size = function () {
  return this._elements.length;
};

Tilemap.Layer.prototype.addRect = function (setNumber, sx, sy, dx, dy, w, h) {
  this._elements.push([setNumber, sx, sy, dx, dy, w, h]);
};

Tilemap.Layer.prototype.render = function (renderer) {
  const gl = renderer.gl;
  const tilemapRenderer = renderer.plugins.rpgtilemap;
  const shader = tilemapRenderer.getShader();
  const matrix = shader.uniforms.uProjectionMatrix;

  renderer.batch.setObjectRenderer(tilemapRenderer);
  renderer.projection.projectionMatrix.copyTo(matrix);
  matrix.append(this.worldTransform);
  renderer.shader.bind(shader);

  if (this._needsTexturesUpdate) {
    tilemapRenderer.updateTextures(renderer, this._images);
    this._needsTexturesUpdate = false;
  }
  tilemapRenderer.bindTextures(renderer);
  renderer.geometry.bind(this._vao, shader);
  this._updateIndexBuffer();
  if (this._needsVertexUpdate) {
    this._updateVertexBuffer();
    this._needsVertexUpdate = false;
  }
  renderer.geometry.updateBuffers();

  const numElements = this._elements.length;
  if (numElements > 0) {
    renderer.state.set(this._state);
    renderer.geometry.draw(gl.TRIANGLES, numElements * 6, 0);
  }
};

Tilemap.Layer.prototype.isReady = function () {
  if (this._images.length === 0) {
    return false;
  }
  for (const texture of this._images) {
    if (!texture || !texture.valid) {
      return false;
    }
  }
  return true;
};

Tilemap.Layer.prototype._createVao = function () {
  const ib = new PIXI.Buffer(null, true, true);
  const vb = new PIXI.Buffer(null, true, false);
  const stride = Tilemap.Layer.VERTEX_STRIDE;
  const type = PIXI.TYPES.FLOAT;
  const geometry = new PIXI.Geometry();
  this._indexBuffer = ib;
  this._vertexBuffer = vb;
  this._vao = geometry
    .addIndex(this._indexBuffer)
    .addAttribute("aTextureId", vb, 1, false, type, stride, 0)
    .addAttribute("aFrame", vb, 4, false, type, stride, 1 * 4)
    .addAttribute("aSource", vb, 2, false, type, stride, 5 * 4)
    .addAttribute("aDest", vb, 2, false, type, stride, 7 * 4);
};

Tilemap.Layer.prototype._updateIndexBuffer = function () {
  const numElements = this._elements.length;
  if (this._indexArray.length < numElements * 6 * 2) {
    this._indexArray = PIXI.utils.createIndicesForQuads(numElements * 2);
    this._indexBuffer.update(this._indexArray);
  }
};

Tilemap.Layer.prototype._updateVertexBuffer = function () {
  const numElements = this._elements.length;
  const required = numElements * Tilemap.Layer.VERTEX_STRIDE;
  if (this._vertexArray.length < required) {
    this._vertexArray = new Float32Array(required * 2);
  }
  const vertexArray = this._vertexArray;
  let index = 0;
  for (const item of this._elements) {
    const setNumber = item[0];
    const tid = setNumber >> 2;
    const sxOffset = 1024 * (setNumber & 1);
    const syOffset = 1024 * ((setNumber >> 1) & 1);
    const sx = item[1] + sxOffset;
    const sy = item[2] + syOffset;
    const dx = item[3];
    const dy = item[4];
    const w = item[5];
    const h = item[6];
    const frameLeft = sx + 0.5;
    const frameTop = sy + 0.5;
    const frameRight = sx + w - 0.5;
    const frameBottom = sy + h - 0.5;
    vertexArray[index++] = tid;
    vertexArray[index++] = frameLeft;
    vertexArray[index++] = frameTop;
    vertexArray[index++] = frameRight;
    vertexArray[index++] = frameBottom;
    vertexArray[index++] = sx;
    vertexArray[index++] = sy;
    vertexArray[index++] = dx;
    vertexArray[index++] = dy;
    vertexArray[index++] = tid;
    vertexArray[index++] = frameLeft;
    vertexArray[index++] = frameTop;
    vertexArray[index++] = frameRight;
    vertexArray[index++] = frameBottom;
    vertexArray[index++] = sx + w;
    vertexArray[index++] = sy;
    vertexArray[index++] = dx + w;
    vertexArray[index++] = dy;
    vertexArray[index++] = tid;
    vertexArray[index++] = frameLeft;
    vertexArray[index++] = frameTop;
    vertexArray[index++] = frameRight;
    vertexArray[index++] = frameBottom;
    vertexArray[index++] = sx + w;
    vertexArray[index++] = sy + h;
    vertexArray[index++] = dx + w;
    vertexArray[index++] = dy + h;
    vertexArray[index++] = tid;
    vertexArray[index++] = frameLeft;
    vertexArray[index++] = frameTop;
    vertexArray[index++] = frameRight;
    vertexArray[index++] = frameBottom;
    vertexArray[index++] = sx;
    vertexArray[index++] = sy + h;
    vertexArray[index++] = dx;
    vertexArray[index++] = dy + h;
  }
  this._vertexBuffer.update(vertexArray);
};

Tilemap.CombinedLayer = function () {
  this.initialize(...arguments);
};

Tilemap.CombinedLayer.prototype = Object.create(PIXI.Container.prototype);
Tilemap.CombinedLayer.prototype.constructor = Tilemap.CombinedLayer;

Tilemap.CombinedLayer.prototype.initialize = function () {
  PIXI.Container.call(this);
  for (let i = 0; i < 2; i++) {
    this.addChild(new Tilemap.Layer());
  }
};

Tilemap.CombinedLayer.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
};

Tilemap.CombinedLayer.prototype.setBitmaps = function (bitmaps) {
  for (const child of this.children) {
    child.setBitmaps(bitmaps);
  }
};

Tilemap.CombinedLayer.prototype.clear = function () {
  for (const child of this.children) {
    child.clear();
  }
};

Tilemap.CombinedLayer.prototype.size = function () {
  return this.children.reduce((r, child) => r + child.size(), 0);
};

// prettier-ignore
Tilemap.CombinedLayer.prototype.addRect = function(
    setNumber, sx, sy, dx, dy, w, h
) {
    for (const child of this.children) {
        if (child.size() < Tilemap.Layer.MAX_SIZE) {
            child.addRect(setNumber, sx, sy, dx, dy, w, h);
            break;
        }
    }
};

Tilemap.CombinedLayer.prototype.isReady = function () {
  return this.children.every((child) => child.isReady());
};

Tilemap.Renderer = function () {
  this.initialize(...arguments);
};

Tilemap.Renderer.prototype = Object.create(PIXI.ObjectRenderer.prototype);
Tilemap.Renderer.prototype.constructor = Tilemap.Renderer;

Tilemap.Renderer.prototype.initialize = function (renderer) {
  PIXI.ObjectRenderer.call(this, renderer);
  this._shader = null;
  this._images = [];
  this._internalTextures = [];
  this._clearBuffer = new Uint8Array(1024 * 1024 * 4);
  this.contextChange();
};

Tilemap.Renderer.prototype.destroy = function () {
  PIXI.ObjectRenderer.prototype.destroy.call(this);
  this._destroyInternalTextures();
  this._shader.destroy();
  this._shader = null;
};

Tilemap.Renderer.prototype.getShader = function () {
  return this._shader;
};

Tilemap.Renderer.prototype.contextChange = function () {
  this._shader = this._createShader();
  this._images = [];
  this._createInternalTextures();
};

Tilemap.Renderer.prototype._createShader = function () {
  const vertexSrc =
    "attribute float aTextureId;" +
    "attribute vec4 aFrame;" +
    "attribute vec2 aSource;" +
    "attribute vec2 aDest;" +
    "uniform mat3 uProjectionMatrix;" +
    "varying vec4 vFrame;" +
    "varying vec2 vTextureCoord;" +
    "varying float vTextureId;" +
    "void main(void) {" +
    "  vec3 position = uProjectionMatrix * vec3(aDest, 1.0);" +
    "  gl_Position = vec4(position, 1.0);" +
    "  vFrame = aFrame;" +
    "  vTextureCoord = aSource;" +
    "  vTextureId = aTextureId;" +
    "}";
  const fragmentSrc =
    "varying vec4 vFrame;" +
    "varying vec2 vTextureCoord;" +
    "varying float vTextureId;" +
    "uniform sampler2D uSampler0;" +
    "uniform sampler2D uSampler1;" +
    "uniform sampler2D uSampler2;" +
    "void main(void) {" +
    "  vec2 textureCoord = clamp(vTextureCoord, vFrame.xy, vFrame.zw);" +
    "  int textureId = int(vTextureId);" +
    "  vec4 color;" +
    "  if (textureId < 0) {" +
    "    color = vec4(0.0, 0.0, 0.0, 0.5);" +
    "  } else if (textureId == 0) {" +
    "    color = texture2D(uSampler0, textureCoord / 2048.0);" +
    "  } else if (textureId == 1) {" +
    "    color = texture2D(uSampler1, textureCoord / 2048.0);" +
    "  } else if (textureId == 2) {" +
    "    color = texture2D(uSampler2, textureCoord / 2048.0);" +
    "  }" +
    "  gl_FragColor = color;" +
    "}";

  return new PIXI.Shader(PIXI.Program.from(vertexSrc, fragmentSrc), {
    uSampler0: 0,
    uSampler1: 0,
    uSampler2: 0,
    uProjectionMatrix: new PIXI.Matrix(),
  });
};

Tilemap.Renderer.prototype._createInternalTextures = function () {
  this._destroyInternalTextures();
  for (let i = 0; i < Tilemap.Layer.MAX_GL_TEXTURES; i++) {
    const baseTexture = new PIXI.BaseRenderTexture();
    baseTexture.resize(2048, 2048);
    baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
    this._internalTextures.push(baseTexture);
  }
};

Tilemap.Renderer.prototype._destroyInternalTextures = function () {
  for (const internalTexture of this._internalTextures) {
    internalTexture.destroy();
  }
  this._internalTextures = [];
};

Tilemap.Renderer.prototype.updateTextures = function (renderer, images) {
  for (let i = 0; i < images.length; i++) {
    const internalTexture = this._internalTextures[i >> 2];
    renderer.texture.bind(internalTexture, 0);
    const gl = renderer.gl;
    const x = 1024 * (i % 2);
    const y = 1024 * ((i >> 1) % 2);
    const format = gl.RGBA;
    const type = gl.UNSIGNED_BYTE;
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
    // prettier-ignore
    gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, 1024, 1024, format, type,
                         this._clearBuffer);
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
    gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, format, type, images[i]);
  }
};

Tilemap.Renderer.prototype.bindTextures = function (renderer) {
  for (let ti = 0; ti < Tilemap.Layer.MAX_GL_TEXTURES; ti++) {
    renderer.texture.bind(this._internalTextures[ti], ti);
  }
};

PIXI.Renderer.registerPlugin("rpgtilemap", Tilemap.Renderer);

//-----------------------------------------------------------------------------
/**
 * The sprite object for a tiling image.
 *
 * @class
 * @extends PIXI.TilingSprite
 * @param {Bitmap} bitmap - The image for the tiling sprite.
 */
function TilingSprite() {
  this.initialize(...arguments);
}

TilingSprite.prototype = Object.create(PIXI.TilingSprite.prototype);
TilingSprite.prototype.constructor = TilingSprite;

TilingSprite.prototype.initialize = function (bitmap) {
  if (!TilingSprite._emptyBaseTexture) {
    TilingSprite._emptyBaseTexture = new PIXI.BaseTexture();
    TilingSprite._emptyBaseTexture.setSize(1, 1);
  }
  const frame = new Rectangle();
  const texture = new PIXI.Texture(TilingSprite._emptyBaseTexture, frame);
  PIXI.TilingSprite.call(this, texture);
  this._bitmap = bitmap;
  this._width = 0;
  this._height = 0;
  this._frame = frame;

  /**
   * The origin point of the tiling sprite for scrolling.
   *
   * @type Point
   */
  this.origin = new Point();

  this._onBitmapChange();
};

TilingSprite._emptyBaseTexture = null;

/**
 * The image for the tiling sprite.
 *
 * @type Bitmap
 * @name TilingSprite#bitmap
 */
Object.defineProperty(TilingSprite.prototype, "bitmap", {
  get: function () {
    return this._bitmap;
  },
  set: function (value) {
    if (this._bitmap !== value) {
      this._bitmap = value;
      this._onBitmapChange();
    }
  },
  configurable: true,
});

/**
 * The opacity of the tiling sprite (0 to 255).
 *
 * @type number
 * @name TilingSprite#opacity
 */
Object.defineProperty(TilingSprite.prototype, "opacity", {
  get: function () {
    return this.alpha * 255;
  },
  set: function (value) {
    this.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * Destroys the tiling sprite.
 */
TilingSprite.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.TilingSprite.prototype.destroy.call(this, options);
};

/**
 * Updates the tiling sprite for each frame.
 */
TilingSprite.prototype.update = function () {
  for (const child of this.children) {
    if (child.update) {
      child.update();
    }
  }
};

/**
 * Sets the x, y, width, and height all at once.
 *
 * @param {number} x - The x coordinate of the tiling sprite.
 * @param {number} y - The y coordinate of the tiling sprite.
 * @param {number} width - The width of the tiling sprite.
 * @param {number} height - The height of the tiling sprite.
 */
TilingSprite.prototype.move = function (x, y, width, height) {
  this.x = x || 0;
  this.y = y || 0;
  this._width = width || 0;
  this._height = height || 0;
};

/**
 * Specifies the region of the image that the tiling sprite will use.
 *
 * @param {number} x - The x coordinate of the frame.
 * @param {number} y - The y coordinate of the frame.
 * @param {number} width - The width of the frame.
 * @param {number} height - The height of the frame.
 */
TilingSprite.prototype.setFrame = function (x, y, width, height) {
  this._frame.x = x;
  this._frame.y = y;
  this._frame.width = width;
  this._frame.height = height;
  this._refresh();
};

/**
 * Updates the transform on all children of this container for rendering.
 */
TilingSprite.prototype.updateTransform = function () {
  this.tilePosition.x = Math.round(-this.origin.x);
  this.tilePosition.y = Math.round(-this.origin.y);
  PIXI.TilingSprite.prototype.updateTransform.call(this);
};

TilingSprite.prototype._onBitmapChange = function () {
  if (this._bitmap) {
    this._bitmap.addLoadListener(this._onBitmapLoad.bind(this));
  } else {
    this.texture.frame = new Rectangle();
  }
};

TilingSprite.prototype._onBitmapLoad = function () {
  this.texture.baseTexture = this._bitmap.baseTexture;
  this._refresh();
};

TilingSprite.prototype._refresh = function () {
  const texture = this.texture;
  const frame = this._frame.clone();
  if (frame.width === 0 && frame.height === 0 && this._bitmap) {
    frame.width = this._bitmap.width;
    frame.height = this._bitmap.height;
  }
  if (texture) {
    if (texture.baseTexture) {
      try {
        texture.frame = frame;
      } catch (e) {
        texture.frame = new Rectangle();
      }
    }
    texture._updateID++;
  }
};

//-----------------------------------------------------------------------------
/**
 * The sprite which covers the entire game screen.
 *
 * @class
 * @extends PIXI.Container
 */
function ScreenSprite() {
  this.initialize(...arguments);
}

ScreenSprite.prototype = Object.create(PIXI.Container.prototype);
ScreenSprite.prototype.constructor = ScreenSprite;

ScreenSprite.prototype.initialize = function () {
  PIXI.Container.call(this);
  this._graphics = new PIXI.Graphics();
  this.addChild(this._graphics);
  this.opacity = 0;
  this._red = -1;
  this._green = -1;
  this._blue = -1;
  this.setBlack();
};

/**
 * The opacity of the sprite (0 to 255).
 *
 * @type number
 * @name ScreenSprite#opacity
 */
Object.defineProperty(ScreenSprite.prototype, "opacity", {
  get: function () {
    return this.alpha * 255;
  },
  set: function (value) {
    this.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * Destroys the screen sprite.
 */
ScreenSprite.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
};

/**
 * Sets black to the color of the screen sprite.
 */
ScreenSprite.prototype.setBlack = function () {
  this.setColor(0, 0, 0);
};

/**
 * Sets white to the color of the screen sprite.
 */
ScreenSprite.prototype.setWhite = function () {
  this.setColor(255, 255, 255);
};

/**
 * Sets the color of the screen sprite by values.
 *
 * @param {number} r - The red value in the range (0, 255).
 * @param {number} g - The green value in the range (0, 255).
 * @param {number} b - The blue value in the range (0, 255).
 */
ScreenSprite.prototype.setColor = function (r, g, b) {
  if (this._red !== r || this._green !== g || this._blue !== b) {
    r = Math.round(r || 0).clamp(0, 255);
    g = Math.round(g || 0).clamp(0, 255);
    b = Math.round(b || 0).clamp(0, 255);
    this._red = r;
    this._green = g;
    this._blue = b;
    const graphics = this._graphics;
    graphics.clear();
    graphics.beginFill((r << 16) | (g << 8) | b, 1);
    graphics.drawRect(-50000, -50000, 100000, 100000);
  }
};

//-----------------------------------------------------------------------------
/**
 * The window in the game.
 *
 * @class
 * @extends PIXI.Container
 */
function Window() {
  this.initialize(...arguments);
}

Window.prototype = Object.create(PIXI.Container.prototype);
Window.prototype.constructor = Window;

Window.prototype.initialize = function () {
  PIXI.Container.call(this);

  this._isWindow = true;
  this._windowskin = null;
  this._width = 0;
  this._height = 0;
  this._cursorRect = new Rectangle();
  this._openness = 255;
  this._animationCount = 0;

  this._padding = 12;
  this._margin = 4;
  this._colorTone = [0, 0, 0, 0];
  this._innerChildren = [];

  this._container = null;
  this._backSprite = null;
  this._frameSprite = null;
  this._contentsBackSprite = null;
  this._cursorSprite = null;
  this._contentsSprite = null;
  this._downArrowSprite = null;
  this._upArrowSprite = null;
  this._pauseSignSprite = null;

  this._createAllParts();

  /**
   * The origin point of the window for scrolling.
   *
   * @type Point
   */
  this.origin = new Point();

  /**
   * The active state for the window.
   *
   * @type boolean
   */
  this.active = true;

  /**
   * The visibility of the frame.
   *
   * @type boolean
   */
  this.frameVisible = true;

  /**
   * The visibility of the cursor.
   *
   * @type boolean
   */
  this.cursorVisible = true;

  /**
   * The visibility of the down scroll arrow.
   *
   * @type boolean
   */
  this.downArrowVisible = false;

  /**
   * The visibility of the up scroll arrow.
   *
   * @type boolean
   */
  this.upArrowVisible = false;

  /**
   * The visibility of the pause sign.
   *
   * @type boolean
   */
  this.pause = false;
};

/**
 * The image used as a window skin.
 *
 * @type Bitmap
 * @name Window#windowskin
 */
Object.defineProperty(Window.prototype, "windowskin", {
  get: function () {
    return this._windowskin;
  },
  set: function (value) {
    if (this._windowskin !== value) {
      this._windowskin = value;
      this._windowskin.addLoadListener(this._onWindowskinLoad.bind(this));
    }
  },
  configurable: true,
});

/**
 * The bitmap used for the window contents.
 *
 * @type Bitmap
 * @name Window#contents
 */
Object.defineProperty(Window.prototype, "contents", {
  get: function () {
    return this._contentsSprite.bitmap;
  },
  set: function (value) {
    this._contentsSprite.bitmap = value;
  },
  configurable: true,
});

/**
 * The bitmap used for the window contents background.
 *
 * @type Bitmap
 * @name Window#contentsBack
 */
Object.defineProperty(Window.prototype, "contentsBack", {
  get: function () {
    return this._contentsBackSprite.bitmap;
  },
  set: function (value) {
    this._contentsBackSprite.bitmap = value;
  },
  configurable: true,
});

/**
 * The width of the window in pixels.
 *
 * @type number
 * @name Window#width
 */
Object.defineProperty(Window.prototype, "width", {
  get: function () {
    return this._width;
  },
  set: function (value) {
    this._width = value;
    this._refreshAllParts();
  },
  configurable: true,
});

/**
 * The height of the window in pixels.
 *
 * @type number
 * @name Window#height
 */
Object.defineProperty(Window.prototype, "height", {
  get: function () {
    return this._height;
  },
  set: function (value) {
    this._height = value;
    this._refreshAllParts();
  },
  configurable: true,
});

/**
 * The size of the padding between the frame and contents.
 *
 * @type number
 * @name Window#padding
 */
Object.defineProperty(Window.prototype, "padding", {
  get: function () {
    return this._padding;
  },
  set: function (value) {
    this._padding = value;
    this._refreshAllParts();
  },
  configurable: true,
});

/**
 * The size of the margin for the window background.
 *
 * @type number
 * @name Window#margin
 */
Object.defineProperty(Window.prototype, "margin", {
  get: function () {
    return this._margin;
  },
  set: function (value) {
    this._margin = value;
    this._refreshAllParts();
  },
  configurable: true,
});

/**
 * The opacity of the window without contents (0 to 255).
 *
 * @type number
 * @name Window#opacity
 */
Object.defineProperty(Window.prototype, "opacity", {
  get: function () {
    return this._container.alpha * 255;
  },
  set: function (value) {
    this._container.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * The opacity of the window background (0 to 255).
 *
 * @type number
 * @name Window#backOpacity
 */
Object.defineProperty(Window.prototype, "backOpacity", {
  get: function () {
    return this._backSprite.alpha * 255;
  },
  set: function (value) {
    this._backSprite.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * The opacity of the window contents (0 to 255).
 *
 * @type number
 * @name Window#contentsOpacity
 */
Object.defineProperty(Window.prototype, "contentsOpacity", {
  get: function () {
    return this._contentsSprite.alpha * 255;
  },
  set: function (value) {
    this._contentsSprite.alpha = value.clamp(0, 255) / 255;
  },
  configurable: true,
});

/**
 * The openness of the window (0 to 255).
 *
 * @type number
 * @name Window#openness
 */
Object.defineProperty(Window.prototype, "openness", {
  get: function () {
    return this._openness;
  },
  set: function (value) {
    if (this._openness !== value) {
      this._openness = value.clamp(0, 255);
      this._container.scale.y = this._openness / 255;
      this._container.y = (this.height / 2) * (1 - this._openness / 255);
    }
  },
  configurable: true,
});

/**
 * The width of the content area in pixels.
 *
 * @readonly
 * @type number
 * @name Window#innerWidth
 */
Object.defineProperty(Window.prototype, "innerWidth", {
  get: function () {
    return Math.max(0, this._width - this._padding * 2);
  },
  configurable: true,
});

/**
 * The height of the content area in pixels.
 *
 * @readonly
 * @type number
 * @name Window#innerHeight
 */
Object.defineProperty(Window.prototype, "innerHeight", {
  get: function () {
    return Math.max(0, this._height - this._padding * 2);
  },
  configurable: true,
});

/**
 * The rectangle of the content area.
 *
 * @readonly
 * @type Rectangle
 * @name Window#innerRect
 */
Object.defineProperty(Window.prototype, "innerRect", {
  get: function () {
    return new Rectangle(
      this._padding,
      this._padding,
      this.innerWidth,
      this.innerHeight
    );
  },
  configurable: true,
});

/**
 * Destroys the window.
 */
Window.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
};

/**
 * Updates the window for each frame.
 */
Window.prototype.update = function () {
  if (this.active) {
    this._animationCount++;
  }
  for (const child of this.children) {
    if (child.update) {
      child.update();
    }
  }
};

/**
 * Sets the x, y, width, and height all at once.
 *
 * @param {number} x - The x coordinate of the window.
 * @param {number} y - The y coordinate of the window.
 * @param {number} width - The width of the window.
 * @param {number} height - The height of the window.
 */
Window.prototype.move = function (x, y, width, height) {
  this.x = x || 0;
  this.y = y || 0;
  if (this._width !== width || this._height !== height) {
    this._width = width || 0;
    this._height = height || 0;
    this._refreshAllParts();
  }
};

/**
 * Checks whether the window is completely open (openness == 255).
 *
 * @returns {boolean} True if the window is open.
 */
Window.prototype.isOpen = function () {
  return this._openness >= 255;
};

/**
 * Checks whether the window is completely closed (openness == 0).
 *
 * @returns {boolean} True if the window is closed.
 */
Window.prototype.isClosed = function () {
  return this._openness <= 0;
};

/**
 * Sets the position of the command cursor.
 *
 * @param {number} x - The x coordinate of the cursor.
 * @param {number} y - The y coordinate of the cursor.
 * @param {number} width - The width of the cursor.
 * @param {number} height - The height of the cursor.
 */
Window.prototype.setCursorRect = function (x, y, width, height) {
  const cw = Math.floor(width || 0);
  const ch = Math.floor(height || 0);
  this._cursorRect.x = Math.floor(x || 0);
  this._cursorRect.y = Math.floor(y || 0);
  if (this._cursorRect.width !== cw || this._cursorRect.height !== ch) {
    this._cursorRect.width = cw;
    this._cursorRect.height = ch;
    this._refreshCursor();
  }
};

/**
 * Moves the cursor position by the given amount.
 *
 * @param {number} x - The amount of horizontal movement.
 * @param {number} y - The amount of vertical movement.
 */
Window.prototype.moveCursorBy = function (x, y) {
  this._cursorRect.x += x;
  this._cursorRect.y += y;
};

/**
 * Moves the inner children by the given amount.
 *
 * @param {number} x - The amount of horizontal movement.
 * @param {number} y - The amount of vertical movement.
 */
Window.prototype.moveInnerChildrenBy = function (x, y) {
  for (const child of this._innerChildren) {
    child.x += x;
    child.y += y;
  }
};

/**
 * Changes the color of the background.
 *
 * @param {number} r - The red value in the range (-255, 255).
 * @param {number} g - The green value in the range (-255, 255).
 * @param {number} b - The blue value in the range (-255, 255).
 */
Window.prototype.setTone = function (r, g, b) {
  const tone = this._colorTone;
  if (r !== tone[0] || g !== tone[1] || b !== tone[2]) {
    this._colorTone = [r, g, b, 0];
    this._refreshBack();
  }
};

/**
 * Adds a child between the background and contents.
 *
 * @param {object} child - The child to add.
 * @returns {object} The child that was added.
 */
Window.prototype.addChildToBack = function (child) {
  const containerIndex = this.children.indexOf(this._container);
  return this.addChildAt(child, containerIndex + 1);
};

/**
 * Adds a child to the client area.
 *
 * @param {object} child - The child to add.
 * @returns {object} The child that was added.
 */
Window.prototype.addInnerChild = function (child) {
  this._innerChildren.push(child);
  return this._clientArea.addChild(child);
};

/**
 * Updates the transform on all children of this container for rendering.
 */
Window.prototype.updateTransform = function () {
  this._updateClientArea();
  this._updateFrame();
  this._updateContentsBack();
  this._updateCursor();
  this._updateContents();
  this._updateArrows();
  this._updatePauseSign();
  PIXI.Container.prototype.updateTransform.call(this);
  this._updateFilterArea();
};

/**
 * Draws the window shape into PIXI.Graphics object. Used by WindowLayer.
 */
Window.prototype.drawShape = function (graphics) {
  if (graphics) {
    const width = this.width;
    const height = (this.height * this._openness) / 255;
    const x = this.x;
    const y = this.y + (this.height - height) / 2;
    graphics.beginFill(0xffffff);
    graphics.drawRoundedRect(x, y, width, height, 0);
    graphics.endFill();
  }
};

Window.prototype._createAllParts = function () {
  this._createContainer();
  this._createBackSprite();
  this._createFrameSprite();
  this._createClientArea();
  this._createContentsBackSprite();
  this._createCursorSprite();
  this._createContentsSprite();
  this._createArrowSprites();
  this._createPauseSignSprites();
};

Window.prototype._createContainer = function () {
  this._container = new PIXI.Container();
  this.addChild(this._container);
};

Window.prototype._createBackSprite = function () {
  this._backSprite = new Sprite();
  this._backSprite.addChild(new TilingSprite());
  this._container.addChild(this._backSprite);
};

Window.prototype._createFrameSprite = function () {
  this._frameSprite = new Sprite();
  for (let i = 0; i < 8; i++) {
    this._frameSprite.addChild(new Sprite());
  }
  this._container.addChild(this._frameSprite);
};

Window.prototype._createClientArea = function () {
  this._clientArea = new Sprite();
  this._clientArea.filters = [new PIXI.filters.AlphaFilter()];
  this._clientArea.filterArea = new Rectangle();
  this._clientArea.move(this._padding, this._padding);
  this.addChild(this._clientArea);
};

Window.prototype._createContentsBackSprite = function () {
  this._contentsBackSprite = new Sprite();
  this._clientArea.addChild(this._contentsBackSprite);
};

Window.prototype._createCursorSprite = function () {
  this._cursorSprite = new Sprite();
  for (let i = 0; i < 9; i++) {
    this._cursorSprite.addChild(new Sprite());
  }
  this._clientArea.addChild(this._cursorSprite);
};

Window.prototype._createContentsSprite = function () {
  this._contentsSprite = new Sprite();
  this._clientArea.addChild(this._contentsSprite);
};

Window.prototype._createArrowSprites = function () {
  this._downArrowSprite = new Sprite();
  this.addChild(this._downArrowSprite);
  this._upArrowSprite = new Sprite();
  this.addChild(this._upArrowSprite);
};

Window.prototype._createPauseSignSprites = function () {
  this._pauseSignSprite = new Sprite();
  this.addChild(this._pauseSignSprite);
};

Window.prototype._onWindowskinLoad = function () {
  this._refreshAllParts();
};

Window.prototype._refreshAllParts = function () {
  this._refreshBack();
  this._refreshFrame();
  this._refreshCursor();
  this._refreshArrows();
  this._refreshPauseSign();
};

Window.prototype._refreshBack = function () {
  const m = this._margin;
  const w = Math.max(0, this._width - m * 2);
  const h = Math.max(0, this._height - m * 2);
  const sprite = this._backSprite;
  const tilingSprite = sprite.children[0];
  // [Note] We use 95 instead of 96 here to avoid blurring edges.
  sprite.bitmap = this._windowskin;
  sprite.setFrame(0, 0, 95, 95);
  sprite.move(m, m);
  sprite.scale.x = w / 95;
  sprite.scale.y = h / 95;
  tilingSprite.bitmap = this._windowskin;
  tilingSprite.setFrame(0, 96, 96, 96);
  tilingSprite.move(0, 0, w, h);
  tilingSprite.scale.x = 1 / sprite.scale.x;
  tilingSprite.scale.y = 1 / sprite.scale.y;
  sprite.setColorTone(this._colorTone);
};

Window.prototype._refreshFrame = function () {
  const drect = { x: 0, y: 0, width: this._width, height: this._height };
  const srect = { x: 96, y: 0, width: 96, height: 96 };
  const m = 24;
  for (const child of this._frameSprite.children) {
    child.bitmap = this._windowskin;
  }
  this._setRectPartsGeometry(this._frameSprite, srect, drect, m);
};

Window.prototype._refreshCursor = function () {
  const drect = this._cursorRect.clone();
  const srect = { x: 96, y: 96, width: 48, height: 48 };
  const m = 4;
  for (const child of this._cursorSprite.children) {
    child.bitmap = this._windowskin;
  }
  this._setRectPartsGeometry(this._cursorSprite, srect, drect, m);
};

Window.prototype._setRectPartsGeometry = function (sprite, srect, drect, m) {
  const sx = srect.x;
  const sy = srect.y;
  const sw = srect.width;
  const sh = srect.height;
  const dx = drect.x;
  const dy = drect.y;
  const dw = drect.width;
  const dh = drect.height;
  const smw = sw - m * 2;
  const smh = sh - m * 2;
  const dmw = dw - m * 2;
  const dmh = dh - m * 2;
  const children = sprite.children;
  sprite.setFrame(0, 0, dw, dh);
  sprite.move(dx, dy);
  // corner
  children[0].setFrame(sx, sy, m, m);
  children[1].setFrame(sx + sw - m, sy, m, m);
  children[2].setFrame(sx, sy + sw - m, m, m);
  children[3].setFrame(sx + sw - m, sy + sw - m, m, m);
  children[0].move(0, 0);
  children[1].move(dw - m, 0);
  children[2].move(0, dh - m);
  children[3].move(dw - m, dh - m);
  // edge
  children[4].move(m, 0);
  children[5].move(m, dh - m);
  children[6].move(0, m);
  children[7].move(dw - m, m);
  children[4].setFrame(sx + m, sy, smw, m);
  children[5].setFrame(sx + m, sy + sw - m, smw, m);
  children[6].setFrame(sx, sy + m, m, smh);
  children[7].setFrame(sx + sw - m, sy + m, m, smh);
  children[4].scale.x = dmw / smw;
  children[5].scale.x = dmw / smw;
  children[6].scale.y = dmh / smh;
  children[7].scale.y = dmh / smh;
  // center
  if (children[8]) {
    children[8].setFrame(sx + m, sy + m, smw, smh);
    children[8].move(m, m);
    children[8].scale.x = dmw / smw;
    children[8].scale.y = dmh / smh;
  }
  for (const child of children) {
    child.visible = dw > 0 && dh > 0;
  }
};

Window.prototype._refreshArrows = function () {
  const w = this._width;
  const h = this._height;
  const p = 24;
  const q = p / 2;
  const sx = 96 + p;
  const sy = 0 + p;
  this._downArrowSprite.bitmap = this._windowskin;
  this._downArrowSprite.anchor.x = 0.5;
  this._downArrowSprite.anchor.y = 0.5;
  this._downArrowSprite.setFrame(sx + q, sy + q + p, p, q);
  this._downArrowSprite.move(w / 2, h - q);
  this._upArrowSprite.bitmap = this._windowskin;
  this._upArrowSprite.anchor.x = 0.5;
  this._upArrowSprite.anchor.y = 0.5;
  this._upArrowSprite.setFrame(sx + q, sy, p, q);
  this._upArrowSprite.move(w / 2, q);
};

Window.prototype._refreshPauseSign = function () {
  const sx = 144;
  const sy = 96;
  const p = 24;
  this._pauseSignSprite.bitmap = this._windowskin;
  this._pauseSignSprite.anchor.x = 0.5;
  this._pauseSignSprite.anchor.y = 1;
  this._pauseSignSprite.move(this._width / 2, this._height);
  this._pauseSignSprite.setFrame(sx, sy, p, p);
  this._pauseSignSprite.alpha = 0;
};

Window.prototype._updateClientArea = function () {
  const pad = this._padding;
  this._clientArea.move(pad, pad);
  this._clientArea.x = pad - this.origin.x;
  this._clientArea.y = pad - this.origin.y;
  if (this.innerWidth > 0 && this.innerHeight > 0) {
    this._clientArea.visible = this.isOpen();
  } else {
    this._clientArea.visible = false;
  }
};

Window.prototype._updateFrame = function () {
  this._frameSprite.visible = this.frameVisible;
};

Window.prototype._updateContentsBack = function () {
  const bitmap = this._contentsBackSprite.bitmap;
  if (bitmap) {
    this._contentsBackSprite.setFrame(0, 0, bitmap.width, bitmap.height);
  }
};

Window.prototype._updateCursor = function () {
  this._cursorSprite.alpha = this._makeCursorAlpha();
  this._cursorSprite.visible = this.isOpen() && this.cursorVisible;
  this._cursorSprite.x = this._cursorRect.x;
  this._cursorSprite.y = this._cursorRect.y;
};

Window.prototype._makeCursorAlpha = function () {
  const blinkCount = this._animationCount % 40;
  const baseAlpha = this.contentsOpacity / 255;
  if (this.active) {
    if (blinkCount < 20) {
      return baseAlpha - blinkCount / 32;
    } else {
      return baseAlpha - (40 - blinkCount) / 32;
    }
  }
  return baseAlpha;
};

Window.prototype._updateContents = function () {
  const bitmap = this._contentsSprite.bitmap;
  if (bitmap) {
    this._contentsSprite.setFrame(0, 0, bitmap.width, bitmap.height);
  }
};

Window.prototype._updateArrows = function () {
  this._downArrowSprite.visible = this.isOpen() && this.downArrowVisible;
  this._upArrowSprite.visible = this.isOpen() && this.upArrowVisible;
};

Window.prototype._updatePauseSign = function () {
  const sprite = this._pauseSignSprite;
  const x = Math.floor(this._animationCount / 16) % 2;
  const y = Math.floor(this._animationCount / 16 / 2) % 2;
  const sx = 144;
  const sy = 96;
  const p = 24;
  if (!this.pause) {
    sprite.alpha = 0;
  } else if (sprite.alpha < 1) {
    sprite.alpha = Math.min(sprite.alpha + 0.1, 1);
  }
  sprite.setFrame(sx + x * p, sy + y * p, p, p);
  sprite.visible = this.isOpen();
};

Window.prototype._updateFilterArea = function () {
  const pos = this._clientArea.worldTransform.apply(new Point(0, 0));
  const filterArea = this._clientArea.filterArea;
  filterArea.x = pos.x + this.origin.x;
  filterArea.y = pos.y + this.origin.y;
  filterArea.width = this.innerWidth;
  filterArea.height = this.innerHeight;
};

//-----------------------------------------------------------------------------
/**
 * The layer which contains game windows.
 *
 * @class
 * @extends PIXI.Container
 */
function WindowLayer() {
  this.initialize(...arguments);
}

WindowLayer.prototype = Object.create(PIXI.Container.prototype);
WindowLayer.prototype.constructor = WindowLayer;

WindowLayer.prototype.initialize = function () {
  PIXI.Container.call(this);
};

/**
 * Updates the window layer for each frame.
 */
WindowLayer.prototype.update = function () {
  for (const child of this.children) {
    if (child.update) {
      child.update();
    }
  }
};

/**
 * Renders the object using the WebGL renderer.
 *
 * @param {PIXI.Renderer} renderer - The renderer.
 */
WindowLayer.prototype.render = function render(renderer) {
  if (!this.visible) {
    return;
  }

  const graphics = new PIXI.Graphics();
  const gl = renderer.gl;
  const children = this.children.clone();

  renderer.framebuffer.forceStencil();
  graphics.transform = this.transform;
  renderer.batch.flush();
  gl.enable(gl.STENCIL_TEST);

  while (children.length > 0) {
    const win = children.pop();
    if (win._isWindow && win.visible && win.openness > 0) {
      gl.stencilFunc(gl.EQUAL, 0, ~0);
      gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
      win.render(renderer);
      renderer.batch.flush();
      graphics.clear();
      win.drawShape(graphics);
      gl.stencilFunc(gl.ALWAYS, 1, ~0);
      gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
      gl.blendFunc(gl.ZERO, gl.ONE);
      graphics.render(renderer);
      renderer.batch.flush();
      gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    }
  }

  gl.disable(gl.STENCIL_TEST);
  gl.clear(gl.STENCIL_BUFFER_BIT);
  gl.clearStencil(0);
  renderer.batch.flush();

  for (const child of this.children) {
    if (!child._isWindow && child.visible) {
      child.render(renderer);
    }
  }

  renderer.batch.flush();
};

//-----------------------------------------------------------------------------
/**
 * The weather effect which displays rain, storm, or snow.
 *
 * @class
 * @extends PIXI.Container
 */
function Weather() {
  this.initialize(...arguments);
}

Weather.prototype = Object.create(PIXI.Container.prototype);
Weather.prototype.constructor = Weather;

Weather.prototype.initialize = function () {
  PIXI.Container.call(this);

  this._width = Graphics.width;
  this._height = Graphics.height;
  this._sprites = [];

  this._createBitmaps();
  this._createDimmer();

  /**
   * The type of the weather in ["none", "rain", "storm", "snow"].
   *
   * @type string
   */
  this.type = "none";

  /**
   * The power of the weather in the range (0, 9).
   *
   * @type number
   */
  this.power = 0;

  /**
   * The origin point of the weather for scrolling.
   *
   * @type Point
   */
  this.origin = new Point();
};

/**
 * Destroys the weather.
 */
Weather.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
  this._rainBitmap.destroy();
  this._stormBitmap.destroy();
  this._snowBitmap.destroy();
};

/**
 * Updates the weather for each frame.
 */
Weather.prototype.update = function () {
  this._updateDimmer();
  this._updateAllSprites();
};

Weather.prototype._createBitmaps = function () {
  this._rainBitmap = new Bitmap(1, 60);
  this._rainBitmap.fillAll("white");
  this._stormBitmap = new Bitmap(2, 100);
  this._stormBitmap.fillAll("white");
  this._snowBitmap = new Bitmap(9, 9);
  this._snowBitmap.drawCircle(4, 4, 4, "white");
};

Weather.prototype._createDimmer = function () {
  this._dimmerSprite = new ScreenSprite();
  this._dimmerSprite.setColor(80, 80, 80);
  this.addChild(this._dimmerSprite);
};

Weather.prototype._updateDimmer = function () {
  this._dimmerSprite.opacity = Math.floor(this.power * 6);
};

Weather.prototype._updateAllSprites = function () {
  const maxSprites = Math.floor(this.power * 10);
  while (this._sprites.length < maxSprites) {
    this._addSprite();
  }
  while (this._sprites.length > maxSprites) {
    this._removeSprite();
  }
  for (const sprite of this._sprites) {
    this._updateSprite(sprite);
    sprite.x = sprite.ax - this.origin.x;
    sprite.y = sprite.ay - this.origin.y;
  }
};

Weather.prototype._addSprite = function () {
  const sprite = new Sprite(this.viewport);
  sprite.opacity = 0;
  this._sprites.push(sprite);
  this.addChild(sprite);
};

Weather.prototype._removeSprite = function () {
  this.removeChild(this._sprites.pop());
};

Weather.prototype._updateSprite = function (sprite) {
  switch (this.type) {
    case "rain":
      this._updateRainSprite(sprite);
      break;
    case "storm":
      this._updateStormSprite(sprite);
      break;
    case "snow":
      this._updateSnowSprite(sprite);
      break;
  }
  if (sprite.opacity < 40) {
    this._rebornSprite(sprite);
  }
};

Weather.prototype._updateRainSprite = function (sprite) {
  sprite.bitmap = this._rainBitmap;
  sprite.rotation = Math.PI / 16;
  sprite.ax -= 6 * Math.sin(sprite.rotation);
  sprite.ay += 6 * Math.cos(sprite.rotation);
  sprite.opacity -= 6;
};

Weather.prototype._updateStormSprite = function (sprite) {
  sprite.bitmap = this._stormBitmap;
  sprite.rotation = Math.PI / 8;
  sprite.ax -= 8 * Math.sin(sprite.rotation);
  sprite.ay += 8 * Math.cos(sprite.rotation);
  sprite.opacity -= 8;
};

Weather.prototype._updateSnowSprite = function (sprite) {
  sprite.bitmap = this._snowBitmap;
  sprite.rotation = Math.PI / 16;
  sprite.ax -= 3 * Math.sin(sprite.rotation);
  sprite.ay += 3 * Math.cos(sprite.rotation);
  sprite.opacity -= 3;
};

Weather.prototype._rebornSprite = function (sprite) {
  sprite.ax = Math.randomInt(Graphics.width + 100) - 100 + this.origin.x;
  sprite.ay = Math.randomInt(Graphics.height + 200) - 200 + this.origin.y;
  sprite.opacity = 160 + Math.randomInt(60);
};

//-----------------------------------------------------------------------------
/**
 * The color filter for WebGL.
 *
 * @class
 * @extends PIXI.Filter
 */
function ColorFilter() {
  this.initialize(...arguments);
}

ColorFilter.prototype = Object.create(PIXI.Filter.prototype);
ColorFilter.prototype.constructor = ColorFilter;

ColorFilter.prototype.initialize = function () {
  PIXI.Filter.call(this, null, this._fragmentSrc());
  this.uniforms.hue = 0;
  this.uniforms.colorTone = [0, 0, 0, 0];
  this.uniforms.blendColor = [0, 0, 0, 0];
  this.uniforms.brightness = 255;
};

/**
 * Sets the hue rotation value.
 *
 * @param {number} hue - The hue value (-360, 360).
 */
ColorFilter.prototype.setHue = function (hue) {
  this.uniforms.hue = Number(hue);
};

/**
 * Sets the color tone.
 *
 * @param {array} tone - The color tone [r, g, b, gray].
 */
ColorFilter.prototype.setColorTone = function (tone) {
  if (!(tone instanceof Array)) {
    throw new Error("Argument must be an array");
  }
  this.uniforms.colorTone = tone.clone();
};

/**
 * Sets the blend color.
 *
 * @param {array} color - The blend color [r, g, b, a].
 */
ColorFilter.prototype.setBlendColor = function (color) {
  if (!(color instanceof Array)) {
    throw new Error("Argument must be an array");
  }
  this.uniforms.blendColor = color.clone();
};

/**
 * Sets the brightness.
 *
 * @param {number} brightness - The brightness (0 to 255).
 */
ColorFilter.prototype.setBrightness = function (brightness) {
  this.uniforms.brightness = Number(brightness);
};

ColorFilter.prototype._fragmentSrc = function () {
  const src =
    "varying vec2 vTextureCoord;" +
    "uniform sampler2D uSampler;" +
    "uniform float hue;" +
    "uniform vec4 colorTone;" +
    "uniform vec4 blendColor;" +
    "uniform float brightness;" +
    "vec3 rgbToHsl(vec3 rgb) {" +
    "  float r = rgb.r;" +
    "  float g = rgb.g;" +
    "  float b = rgb.b;" +
    "  float cmin = min(r, min(g, b));" +
    "  float cmax = max(r, max(g, b));" +
    "  float h = 0.0;" +
    "  float s = 0.0;" +
    "  float l = (cmin + cmax) / 2.0;" +
    "  float delta = cmax - cmin;" +
    "  if (delta > 0.0) {" +
    "    if (r == cmax) {" +
    "      h = mod((g - b) / delta + 6.0, 6.0) / 6.0;" +
    "    } else if (g == cmax) {" +
    "      h = ((b - r) / delta + 2.0) / 6.0;" +
    "    } else {" +
    "      h = ((r - g) / delta + 4.0) / 6.0;" +
    "    }" +
    "    if (l < 1.0) {" +
    "      s = delta / (1.0 - abs(2.0 * l - 1.0));" +
    "    }" +
    "  }" +
    "  return vec3(h, s, l);" +
    "}" +
    "vec3 hslToRgb(vec3 hsl) {" +
    "  float h = hsl.x;" +
    "  float s = hsl.y;" +
    "  float l = hsl.z;" +
    "  float c = (1.0 - abs(2.0 * l - 1.0)) * s;" +
    "  float x = c * (1.0 - abs((mod(h * 6.0, 2.0)) - 1.0));" +
    "  float m = l - c / 2.0;" +
    "  float cm = c + m;" +
    "  float xm = x + m;" +
    "  if (h < 1.0 / 6.0) {" +
    "    return vec3(cm, xm, m);" +
    "  } else if (h < 2.0 / 6.0) {" +
    "    return vec3(xm, cm, m);" +
    "  } else if (h < 3.0 / 6.0) {" +
    "    return vec3(m, cm, xm);" +
    "  } else if (h < 4.0 / 6.0) {" +
    "    return vec3(m, xm, cm);" +
    "  } else if (h < 5.0 / 6.0) {" +
    "    return vec3(xm, m, cm);" +
    "  } else {" +
    "    return vec3(cm, m, xm);" +
    "  }" +
    "}" +
    "void main() {" +
    "  vec4 sample = texture2D(uSampler, vTextureCoord);" +
    "  float a = sample.a;" +
    "  vec3 hsl = rgbToHsl(sample.rgb);" +
    "  hsl.x = mod(hsl.x + hue / 360.0, 1.0);" +
    "  hsl.y = hsl.y * (1.0 - colorTone.a / 255.0);" +
    "  vec3 rgb = hslToRgb(hsl);" +
    "  float r = rgb.r;" +
    "  float g = rgb.g;" +
    "  float b = rgb.b;" +
    "  float r2 = colorTone.r / 255.0;" +
    "  float g2 = colorTone.g / 255.0;" +
    "  float b2 = colorTone.b / 255.0;" +
    "  float r3 = blendColor.r / 255.0;" +
    "  float g3 = blendColor.g / 255.0;" +
    "  float b3 = blendColor.b / 255.0;" +
    "  float i3 = blendColor.a / 255.0;" +
    "  float i1 = 1.0 - i3;" +
    "  r = clamp((r / a + r2) * a, 0.0, 1.0);" +
    "  g = clamp((g / a + g2) * a, 0.0, 1.0);" +
    "  b = clamp((b / a + b2) * a, 0.0, 1.0);" +
    "  r = clamp(r * i1 + r3 * i3 * a, 0.0, 1.0);" +
    "  g = clamp(g * i1 + g3 * i3 * a, 0.0, 1.0);" +
    "  b = clamp(b * i1 + b3 * i3 * a, 0.0, 1.0);" +
    "  r = r * brightness / 255.0;" +
    "  g = g * brightness / 255.0;" +
    "  b = b * brightness / 255.0;" +
    "  gl_FragColor = vec4(r, g, b, a);" +
    "}";
  return src;
};

//-----------------------------------------------------------------------------
/**
 * The root object of the display tree.
 *
 * @class
 * @extends PIXI.Container
 */
function Stage() {
  this.initialize(...arguments);
}

Stage.prototype = Object.create(PIXI.Container.prototype);
Stage.prototype.constructor = Stage;

Stage.prototype.initialize = function () {
  PIXI.Container.call(this);
};

/**
 * Destroys the stage.
 */
Stage.prototype.destroy = function () {
  const options = { children: true, texture: true };
  PIXI.Container.prototype.destroy.call(this, options);
};

//-----------------------------------------------------------------------------
/**
 * The audio object of Web Audio API.
 *
 * @class
 * @param {string} url - The url of the audio file.
 */
function WebAudio() {
  this.initialize(...arguments);
}

WebAudio.prototype.initialize = function (url) {
  this.clear();
  this._url = url;
  this._startLoading();
};

/**
 * Initializes the audio system.
 *
 * @returns {boolean} True if the audio system is available.
 */
WebAudio.initialize = function () {
  this._context = null;
  this._masterGainNode = null;
  this._masterVolume = 1;
  this._createContext();
  this._createMasterGainNode();
  this._setupEventHandlers();
  return !!this._context;
};

/**
 * Sets the master volume for all audio.
 *
 * @param {number} value - The master volume (0 to 1).
 */
WebAudio.setMasterVolume = function (value) {
  this._masterVolume = value;
  this._resetVolume();
};

WebAudio._createContext = function () {
  try {
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    this._context = new AudioContext();
  } catch (e) {
    this._context = null;
  }
};

WebAudio._currentTime = function () {
  return this._context ? this._context.currentTime : 0;
};

WebAudio._createMasterGainNode = function () {
  const context = this._context;
  if (context) {
    this._masterGainNode = context.createGain();
    this._resetVolume();
    this._masterGainNode.connect(context.destination);
  }
};

WebAudio._setupEventHandlers = function () {
  const onUserGesture = this._onUserGesture.bind(this);
  const onVisibilityChange = this._onVisibilityChange.bind(this);
  document.addEventListener("keydown", onUserGesture);
  document.addEventListener("mousedown", onUserGesture);
  document.addEventListener("touchend", onUserGesture);
  document.addEventListener("visibilitychange", onVisibilityChange);
};

WebAudio._onUserGesture = function () {
  const context = this._context;
  if (context && context.state === "suspended") {
    context.resume();
  }
};

WebAudio._onVisibilityChange = function () {
  if (document.visibilityState === "hidden") {
    this._onHide();
  } else {
    this._onShow();
  }
};

WebAudio._onHide = function () {
  if (this._shouldMuteOnHide()) {
    this._fadeOut(1);
  }
};

WebAudio._onShow = function () {
  if (this._shouldMuteOnHide()) {
    this._fadeIn(1);
  }
};

WebAudio._shouldMuteOnHide = function () {
  return Utils.isMobileDevice() && !window.navigator.standalone;
};

WebAudio._resetVolume = function () {
  if (this._masterGainNode) {
    const gain = this._masterGainNode.gain;
    const volume = this._masterVolume;
    const currentTime = this._currentTime();
    gain.setValueAtTime(volume, currentTime);
  }
};

WebAudio._fadeIn = function (duration) {
  if (this._masterGainNode) {
    const gain = this._masterGainNode.gain;
    const volume = this._masterVolume;
    const currentTime = this._currentTime();
    gain.setValueAtTime(0, currentTime);
    gain.linearRampToValueAtTime(volume, currentTime + duration);
  }
};

WebAudio._fadeOut = function (duration) {
  if (this._masterGainNode) {
    const gain = this._masterGainNode.gain;
    const volume = this._masterVolume;
    const currentTime = this._currentTime();
    gain.setValueAtTime(volume, currentTime);
    gain.linearRampToValueAtTime(0, currentTime + duration);
  }
};

/**
 * Clears the audio data.
 */
WebAudio.prototype.clear = function () {
  this.stop();
  this._data = null;
  this._fetchedSize = 0;
  this._fetchedData = [];
  this._buffers = [];
  this._sourceNodes = [];
  this._gainNode = null;
  this._pannerNode = null;
  this._totalTime = 0;
  this._sampleRate = 0;
  this._loop = 0;
  this._loopStart = 0;
  this._loopLength = 0;
  this._loopStartTime = 0;
  this._loopLengthTime = 0;
  this._startTime = 0;
  this._volume = 1;
  this._pitch = 1;
  this._pan = 0;
  this._endTimer = null;
  this._loadListeners = [];
  this._stopListeners = [];
  this._lastUpdateTime = 0;
  this._isLoaded = false;
  this._isError = false;
  this._isPlaying = false;
  this._decoder = null;
};

/**
 * The url of the audio file.
 *
 * @readonly
 * @type string
 * @name WebAudio#url
 */
Object.defineProperty(WebAudio.prototype, "url", {
  get: function () {
    return this._url;
  },
  configurable: true,
});

/**
 * The volume of the audio.
 *
 * @type number
 * @name WebAudio#volume
 */
Object.defineProperty(WebAudio.prototype, "volume", {
  get: function () {
    return this._volume;
  },
  set: function (value) {
    this._volume = value;
    if (this._gainNode) {
      this._gainNode.gain.setValueAtTime(this._volume, WebAudio._currentTime());
    }
  },
  configurable: true,
});

/**
 * The pitch of the audio.
 *
 * @type number
 * @name WebAudio#pitch
 */
Object.defineProperty(WebAudio.prototype, "pitch", {
  get: function () {
    return this._pitch;
  },
  set: function (value) {
    if (this._pitch !== value) {
      this._pitch = value;
      if (this.isPlaying()) {
        this.play(this._loop, 0);
      }
    }
  },
  configurable: true,
});

/**
 * The pan of the audio.
 *
 * @type number
 * @name WebAudio#pan
 */
Object.defineProperty(WebAudio.prototype, "pan", {
  get: function () {
    return this._pan;
  },
  set: function (value) {
    this._pan = value;
    this._updatePanner();
  },
  configurable: true,
});

/**
 * Checks whether the audio data is ready to play.
 *
 * @returns {boolean} True if the audio data is ready to play.
 */
WebAudio.prototype.isReady = function () {
  return this._buffers && this._buffers.length > 0;
};

/**
 * Checks whether a loading error has occurred.
 *
 * @returns {boolean} True if a loading error has occurred.
 */
WebAudio.prototype.isError = function () {
  return this._isError;
};

/**
 * Checks whether the audio is playing.
 *
 * @returns {boolean} True if the audio is playing.
 */
WebAudio.prototype.isPlaying = function () {
  return this._isPlaying;
};

/**
 * Plays the audio.
 *
 * @param {boolean} loop - Whether the audio data play in a loop.
 * @param {number} offset - The start position to play in seconds.
 */
WebAudio.prototype.play = function (loop, offset) {
  this._loop = loop;
  if (this.isReady()) {
    offset = offset || 0;
    this._startPlaying(offset);
  } else if (WebAudio._context) {
    this.addLoadListener(() => this.play(loop, offset));
  }
  this._isPlaying = true;
};

/**
 * Stops the audio.
 */
WebAudio.prototype.stop = function () {
  this._isPlaying = false;
  this._removeEndTimer();
  this._removeNodes();
  this._loadListeners = [];
  if (this._stopListeners) {
    while (this._stopListeners.length > 0) {
      const listner = this._stopListeners.shift();
      listner();
    }
  }
};

/**
 * Destroys the audio.
 */
WebAudio.prototype.destroy = function () {
  this._destroyDecoder();
  this.clear();
};

/**
 * Performs the audio fade-in.
 *
 * @param {number} duration - Fade-in time in seconds.
 */
WebAudio.prototype.fadeIn = function (duration) {
  if (this.isReady()) {
    if (this._gainNode) {
      const gain = this._gainNode.gain;
      const currentTime = WebAudio._currentTime();
      gain.setValueAtTime(0, currentTime);
      gain.linearRampToValueAtTime(this._volume, currentTime + duration);
    }
  } else {
    this.addLoadListener(() => this.fadeIn(duration));
  }
};

/**
 * Performs the audio fade-out.
 *
 * @param {number} duration - Fade-out time in seconds.
 */
WebAudio.prototype.fadeOut = function (duration) {
  if (this._gainNode) {
    const gain = this._gainNode.gain;
    const currentTime = WebAudio._currentTime();
    gain.setValueAtTime(this._volume, currentTime);
    gain.linearRampToValueAtTime(0, currentTime + duration);
  }
  this._isPlaying = false;
  this._loadListeners = [];
};

/**
 * Gets the seek position of the audio.
 */
WebAudio.prototype.seek = function () {
  if (WebAudio._context) {
    let pos = (WebAudio._currentTime() - this._startTime) * this._pitch;
    if (this._loopLengthTime > 0) {
      while (pos >= this._loopStartTime + this._loopLengthTime) {
        pos -= this._loopLengthTime;
      }
    }
    return pos;
  } else {
    return 0;
  }
};

/**
 * Adds a callback function that will be called when the audio data is loaded.
 *
 * @param {function} listner - The callback function.
 */
WebAudio.prototype.addLoadListener = function (listner) {
  this._loadListeners.push(listner);
};

/**
 * Adds a callback function that will be called when the playback is stopped.
 *
 * @param {function} listner - The callback function.
 */
WebAudio.prototype.addStopListener = function (listner) {
  this._stopListeners.push(listner);
};

/**
 * Tries to load the audio again.
 */
WebAudio.prototype.retry = function () {
  this._startLoading();
  if (this._isPlaying) {
    this.play(this._loop, 0);
  }
};

WebAudio.prototype._startLoading = function () {
  if (WebAudio._context) {
    const url = this._realUrl();
    if (Utils.isLocal()) {
      this._startXhrLoading(url);
    } else {
      this._startFetching(url);
    }
    const currentTime = WebAudio._currentTime();
    this._lastUpdateTime = currentTime - 0.5;
    this._isError = false;
    this._isLoaded = false;
    this._destroyDecoder();
    if (this._shouldUseDecoder()) {
      this._createDecoder();
    }
  }
};

WebAudio.prototype._shouldUseDecoder = function () {
  return !Utils.canPlayOgg() && typeof VorbisDecoder === "function";
};

WebAudio.prototype._createDecoder = function () {
  this._decoder = new VorbisDecoder(
    WebAudio._context,
    this._onDecode.bind(this),
    this._onError.bind(this)
  );
};

WebAudio.prototype._destroyDecoder = function () {
  if (this._decoder) {
    this._decoder.destroy();
    this._decoder = null;
  }
};

WebAudio.prototype._realUrl = function () {
  return this._url + (Utils.hasEncryptedAudio() ? "_" : "");
};

WebAudio.prototype._startXhrLoading = function (url) {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.responseType = "arraybuffer";
  xhr.onload = () => this._onXhrLoad(xhr);
  xhr.onerror = this._onError.bind(this);
  xhr.send();
};

WebAudio.prototype._startFetching = function (url) {
  const options = { credentials: "same-origin" };
  fetch(url, options)
    .then((response) => this._onFetch(response))
    .catch(() => this._onError());
};

WebAudio.prototype._onXhrLoad = function (xhr) {
  if (xhr.status < 400) {
    this._data = new Uint8Array(xhr.response);
    this._isLoaded = true;
    this._updateBuffer();
  } else {
    this._onError();
  }
};

WebAudio.prototype._onFetch = function (response) {
  if (response.ok) {
    const reader = response.body.getReader();
    const readChunk = ({ done, value }) => {
      if (done) {
        this._isLoaded = true;
        if (this._fetchedSize > 0) {
          this._concatenateFetchedData();
          this._updateBuffer();
          this._data = null;
        }
        return 0;
      } else {
        this._onFetchProcess(value);
        return reader.read().then(readChunk);
      }
    };
    reader
      .read()
      .then(readChunk)
      .catch(() => this._onError());
  } else {
    this._onError();
  }
};

WebAudio.prototype._onError = function () {
  if (this._sourceNodes.length > 0) {
    this._stopSourceNode();
  }
  this._data = null;
  this._isError = true;
};

WebAudio.prototype._onFetchProcess = function (value) {
  this._fetchedSize += value.length;
  this._fetchedData.push(value);
  this._updateBufferOnFetch();
};

WebAudio.prototype._updateBufferOnFetch = function () {
  const currentTime = WebAudio._currentTime();
  const deltaTime = currentTime - this._lastUpdateTime;
  const currentData = this._data;
  const currentSize = currentData ? currentData.length : 0;
  if (deltaTime >= 1 && currentSize + this._fetchedSize >= 200000) {
    this._concatenateFetchedData();
    this._updateBuffer();
    this._lastUpdateTime = currentTime;
  }
};

WebAudio.prototype._concatenateFetchedData = function () {
  const currentData = this._data;
  const currentSize = currentData ? currentData.length : 0;
  const newData = new Uint8Array(currentSize + this._fetchedSize);
  let pos = 0;
  if (currentData) {
    newData.set(currentData);
    pos += currentSize;
  }
  for (const value of this._fetchedData) {
    newData.set(value, pos);
    pos += value.length;
  }
  this._data = newData;
  this._fetchedData = [];
  this._fetchedSize = 0;
};

WebAudio.prototype._updateBuffer = function () {
  const arrayBuffer = this._readableBuffer();
  this._readLoopComments(arrayBuffer);
  this._decodeAudioData(arrayBuffer);
};

WebAudio.prototype._readableBuffer = function () {
  if (Utils.hasEncryptedAudio()) {
    return Utils.decryptArrayBuffer(this._data.buffer);
  } else {
    return this._data.buffer;
  }
};

WebAudio.prototype._decodeAudioData = function (arrayBuffer) {
  if (this._shouldUseDecoder()) {
    if (this._decoder) {
      this._decoder.send(arrayBuffer, this._isLoaded);
    }
  } else {
    // [Note] Make a temporary copy of arrayBuffer because
    //   decodeAudioData() detaches it.
    WebAudio._context
      .decodeAudioData(arrayBuffer.slice())
      .then((buffer) => this._onDecode(buffer))
      .catch(() => this._onError());
  }
};

WebAudio.prototype._onDecode = function (buffer) {
  if (!this._shouldUseDecoder()) {
    this._buffers = [];
    this._totalTime = 0;
  }
  this._buffers.push(buffer);
  this._totalTime += buffer.duration;
  if (this._loopLength > 0 && this._sampleRate > 0) {
    this._loopStartTime = this._loopStart / this._sampleRate;
    this._loopLengthTime = this._loopLength / this._sampleRate;
  } else {
    this._loopStartTime = 0;
    this._loopLengthTime = this._totalTime;
  }
  if (this._sourceNodes.length > 0) {
    this._refreshSourceNode();
  }
  this._onLoad();
};

WebAudio.prototype._refreshSourceNode = function () {
  if (this._shouldUseDecoder()) {
    const index = this._buffers.length - 1;
    this._createSourceNode(index);
    if (this._isPlaying) {
      this._startSourceNode(index);
    }
  } else {
    this._stopSourceNode();
    this._createAllSourceNodes();
    if (this._isPlaying) {
      this._startAllSourceNodes();
    }
  }
  if (this._isPlaying) {
    this._removeEndTimer();
    this._createEndTimer();
  }
};

WebAudio.prototype._startPlaying = function (offset) {
  if (this._loopLengthTime > 0) {
    while (offset >= this._loopStartTime + this._loopLengthTime) {
      offset -= this._loopLengthTime;
    }
  }
  this._startTime = WebAudio._currentTime() - offset / this._pitch;
  this._removeEndTimer();
  this._removeNodes();
  this._createPannerNode();
  this._createGainNode();
  this._createAllSourceNodes();
  this._startAllSourceNodes();
  this._createEndTimer();
};

WebAudio.prototype._startAllSourceNodes = function () {
  for (let i = 0; i < this._sourceNodes.length; i++) {
    this._startSourceNode(i);
  }
};

WebAudio.prototype._startSourceNode = function (index) {
  const sourceNode = this._sourceNodes[index];
  const seekPos = this.seek();
  const currentTime = WebAudio._currentTime();
  const loop = this._loop;
  const loopStart = this._loopStartTime;
  const loopLength = this._loopLengthTime;
  const loopEnd = loopStart + loopLength;
  const pitch = this._pitch;
  let chunkStart = 0;
  for (let i = 0; i < index; i++) {
    chunkStart += this._buffers[i].duration;
  }
  const chunkEnd = chunkStart + sourceNode.buffer.duration;
  let when = 0;
  let offset = 0;
  let duration = sourceNode.buffer.duration;
  if (seekPos >= chunkStart && seekPos < chunkEnd - 0.01) {
    when = currentTime;
    offset = seekPos - chunkStart;
  } else {
    when = currentTime + (chunkStart - seekPos) / pitch;
    offset = 0;
    if (loop) {
      if (when < currentTime - 0.01) {
        when += loopLength / pitch;
      }
      if (seekPos >= loopStart && chunkStart < loopStart) {
        when += (loopStart - chunkStart) / pitch;
        offset = loopStart - chunkStart;
      }
    }
  }
  if (loop && loopEnd < chunkEnd) {
    duration = loopEnd - chunkStart - offset;
  }
  if (this._shouldUseDecoder()) {
    if (when >= currentTime && offset < duration) {
      sourceNode.loop = false;
      sourceNode.start(when, offset, duration);
      if (loop && chunkEnd > loopStart) {
        sourceNode.onended = () => {
          this._createSourceNode(index);
          this._startSourceNode(index);
        };
      }
    }
  } else {
    if (when >= currentTime && offset < sourceNode.buffer.duration) {
      sourceNode.start(when, offset);
    }
  }
  chunkStart += sourceNode.buffer.duration;
};

WebAudio.prototype._stopSourceNode = function () {
  for (const sourceNode of this._sourceNodes) {
    try {
      sourceNode.onended = null;
      sourceNode.stop();
    } catch (e) {
      // Ignore InvalidStateError
    }
  }
};

WebAudio.prototype._createPannerNode = function () {
  this._pannerNode = WebAudio._context.createPanner();
  this._pannerNode.panningModel = "equalpower";
  this._pannerNode.connect(WebAudio._masterGainNode);
  this._updatePanner();
};

WebAudio.prototype._createGainNode = function () {
  const currentTime = WebAudio._currentTime();
  this._gainNode = WebAudio._context.createGain();
  this._gainNode.gain.setValueAtTime(this._volume, currentTime);
  this._gainNode.connect(this._pannerNode);
};

WebAudio.prototype._createAllSourceNodes = function () {
  for (let i = 0; i < this._buffers.length; i++) {
    this._createSourceNode(i);
  }
};

WebAudio.prototype._createSourceNode = function (index) {
  const sourceNode = WebAudio._context.createBufferSource();
  const currentTime = WebAudio._currentTime();
  sourceNode.buffer = this._buffers[index];
  sourceNode.loop = this._loop && this._isLoaded;
  sourceNode.loopStart = this._loopStartTime;
  sourceNode.loopEnd = this._loopStartTime + this._loopLengthTime;
  sourceNode.playbackRate.setValueAtTime(this._pitch, currentTime);
  sourceNode.connect(this._gainNode);
  this._sourceNodes[index] = sourceNode;
};

WebAudio.prototype._removeNodes = function () {
  if (this._sourceNodes && this._sourceNodes.length > 0) {
    this._stopSourceNode();
    this._sourceNodes = [];
    this._gainNode = null;
    this._pannerNode = null;
  }
};

WebAudio.prototype._createEndTimer = function () {
  if (this._sourceNodes.length > 0 && !this._loop) {
    const endTime = this._startTime + this._totalTime / this._pitch;
    const delay = endTime - WebAudio._currentTime();
    this._endTimer = setTimeout(this.stop.bind(this), delay * 1000);
  }
};

WebAudio.prototype._removeEndTimer = function () {
  if (this._endTimer) {
    clearTimeout(this._endTimer);
    this._endTimer = null;
  }
};

WebAudio.prototype._updatePanner = function () {
  if (this._pannerNode) {
    const x = this._pan;
    const z = 1 - Math.abs(x);
    this._pannerNode.setPosition(x, 0, z);
  }
};

WebAudio.prototype._onLoad = function () {
  while (this._loadListeners.length > 0) {
    const listner = this._loadListeners.shift();
    listner();
  }
};

WebAudio.prototype._readLoopComments = function (arrayBuffer) {
  const view = new DataView(arrayBuffer);
  let index = 0;
  while (index < view.byteLength - 30) {
    if (this._readFourCharacters(view, index) !== "OggS") {
      break;
    }
    index += 26;
    const numSegments = view.getUint8(index++);
    const segments = [];
    for (let i = 0; i < numSegments; i++) {
      segments.push(view.getUint8(index++));
    }
    const packets = [];
    while (segments.length > 0) {
      let packetSize = 0;
      while (segments[0] === 255) {
        packetSize += segments.shift();
      }
      if (segments.length > 0) {
        packetSize += segments.shift();
      }
      packets.push(packetSize);
    }
    let vorbisHeaderFound = false;
    for (const size of packets) {
      if (this._readFourCharacters(view, index + 1) === "vorb") {
        const headerType = view.getUint8(index);
        if (headerType === 1) {
          this._sampleRate = view.getUint32(index + 12, true);
        } else if (headerType === 3) {
          this._readMetaData(view, index, size);
        }
        vorbisHeaderFound = true;
      }
      index += size;
    }
    if (!vorbisHeaderFound) {
      break;
    }
  }
};

WebAudio.prototype._readMetaData = function (view, index, size) {
  for (let i = index; i < index + size - 10; i++) {
    if (this._readFourCharacters(view, i) === "LOOP") {
      let text = "";
      while (view.getUint8(i) > 0) {
        text += String.fromCharCode(view.getUint8(i++));
      }
      if (text.match(/LOOPSTART=([0-9]+)/)) {
        this._loopStart = parseInt(RegExp.$1);
      }
      if (text.match(/LOOPLENGTH=([0-9]+)/)) {
        this._loopLength = parseInt(RegExp.$1);
      }
      if (text === "LOOPSTART" || text === "LOOPLENGTH") {
        let text2 = "";
        i += 16;
        while (view.getUint8(i) > 0) {
          text2 += String.fromCharCode(view.getUint8(i++));
        }
        if (text === "LOOPSTART") {
          this._loopStart = parseInt(text2);
        } else {
          this._loopLength = parseInt(text2);
        }
      }
    }
  }
};

WebAudio.prototype._readFourCharacters = function (view, index) {
  let string = "";
  if (index <= view.byteLength - 4) {
    for (let i = 0; i < 4; i++) {
      string += String.fromCharCode(view.getUint8(index + i));
    }
  }
  return string;
};

//-----------------------------------------------------------------------------
/**
 * The static class that handles video playback.
 *
 * @namespace
 */
function Video() {
  throw new Error("This is a static class");
}

/**
 * Initializes the video system.
 *
 * @param {number} width - The width of the video.
 * @param {number} height - The height of the video.
 */
Video.initialize = function (width, height) {
  this._element = null;
  this._loading = false;
  this._volume = 1;
  this._createElement();
  this._setupEventHandlers();
  this.resize(width, height);
};

/**
 * Changes the display size of the video.
 *
 * @param {number} width - The width of the video.
 * @param {number} height - The height of the video.
 */
Video.resize = function (width, height) {
  if (this._element) {
    this._element.style.width = width + "px";
    this._element.style.height = height + "px";
  }
};

/**
 * Starts playback of a video.
 *
 * @param {string} src - The url of the video.
 */
Video.play = function (src) {
  this._element.src = src;
  this._element.onloadeddata = this._onLoad.bind(this);
  this._element.onerror = this._onError.bind(this);
  this._element.onended = this._onEnd.bind(this);
  this._element.load();
  this._loading = true;
};

/**
 * Checks whether the video is playing.
 *
 * @returns {boolean} True if the video is playing.
 */
Video.isPlaying = function () {
  return this._loading || this._isVisible();
};

/**
 * Sets the volume for videos.
 *
 * @param {number} volume - The volume for videos (0 to 1).
 */
Video.setVolume = function (volume) {
  this._volume = volume;
  if (this._element) {
    this._element.volume = this._volume;
  }
};

Video._createElement = function () {
  this._element = document.createElement("video");
  this._element.id = "gameVideo";
  this._element.style.position = "absolute";
  this._element.style.margin = "auto";
  this._element.style.top = 0;
  this._element.style.left = 0;
  this._element.style.right = 0;
  this._element.style.bottom = 0;
  this._element.style.opacity = 0;
  this._element.style.zIndex = 2;
  this._element.setAttribute("playsinline", "");
  this._element.oncontextmenu = () => false;
  document.body.appendChild(this._element);
};

Video._onLoad = function () {
  this._element.volume = this._volume;
  this._element.play();
  this._updateVisibility(true);
  this._loading = false;
};

Video._onError = function () {
  this._updateVisibility(false);
  const retry = () => {
    this._element.load();
  };
  throw ["LoadError", this._element.src, retry];
};

Video._onEnd = function () {
  this._updateVisibility(false);
};

Video._updateVisibility = function (videoVisible) {
  if (videoVisible) {
    Graphics.hideScreen();
  } else {
    Graphics.showScreen();
  }
  this._element.style.opacity = videoVisible ? 1 : 0;
};

Video._isVisible = function () {
  return this._element.style.opacity > 0;
};

Video._setupEventHandlers = function () {
  const onUserGesture = this._onUserGesture.bind(this);
  document.addEventListener("keydown", onUserGesture);
  document.addEventListener("mousedown", onUserGesture);
  document.addEventListener("touchend", onUserGesture);
};

Video._onUserGesture = function () {
  if (!this._element.src && this._element.paused) {
    this._element.play().catch(() => 0);
  }
};

//-----------------------------------------------------------------------------
/**
 * The static class that handles input data from the keyboard and gamepads.
 *
 * @namespace
 */
function Input() {
  throw new Error("This is a static class");
}

/**
 * Initializes the input system.
 */
Input.initialize = function () {
  this.clear();
  this._setupEventHandlers();
};

/**
 * The wait time of the key repeat in frames.
 *
 * @type number
 */
Input.keyRepeatWait = 24;

/**
 * The interval of the key repeat in frames.
 *
 * @type number
 */
Input.keyRepeatInterval = 6;

/**
 * A hash table to convert from a virtual key code to a mapped key name.
 *
 * @type Object
 */
Input.keyMapper = {
  9: "tab", // tab
  13: "ok", // enter
  16: "shift", // shift
  17: "control", // control
  18: "control", // alt
  27: "escape", // escape
  32: "ok", // space
  33: "pageup", // pageup
  34: "pagedown", // pagedown
  37: "left", // left arrow
  38: "up", // up arrow
  39: "right", // right arrow
  40: "down", // down arrow
  45: "escape", // insert
  81: "pageup", // Q
  87: "pagedown", // W
  88: "escape", // X
  90: "ok", // Z
  96: "escape", // numpad 0
  98: "down", // numpad 2
  100: "left", // numpad 4
  102: "right", // numpad 6
  104: "up", // numpad 8
  120: "debug", // F9
};

/**
 * A hash table to convert from a gamepad button to a mapped key name.
 *
 * @type Object
 */
Input.gamepadMapper = {
  0: "ok", // A
  1: "cancel", // B
  2: "shift", // X
  3: "menu", // Y
  4: "pageup", // LB
  5: "pagedown", // RB
  12: "up", // D-pad up
  13: "down", // D-pad down
  14: "left", // D-pad left
  15: "right", // D-pad right
};

/**
 * Clears all the input data.
 */
Input.clear = function () {
  this._currentState = {};
  this._previousState = {};
  this._gamepadStates = [];
  this._latestButton = null;
  this._pressedTime = 0;
  this._dir4 = 0;
  this._dir8 = 0;
  this._preferredAxis = "";
  this._date = 0;
  this._virtualButton = null;
};

/**
 * Updates the input data.
 */
Input.update = function () {
  this._pollGamepads();
  if (this._currentState[this._latestButton]) {
    this._pressedTime++;
  } else {
    this._latestButton = null;
  }
  for (const name in this._currentState) {
    if (this._currentState[name] && !this._previousState[name]) {
      this._latestButton = name;
      this._pressedTime = 0;
      this._date = Date.now();
    }
    this._previousState[name] = this._currentState[name];
  }
  if (this._virtualButton) {
    this._latestButton = this._virtualButton;
    this._pressedTime = 0;
    this._virtualButton = null;
  }
  this._updateDirection();
};

/**
 * Checks whether a key is currently pressed down.
 *
 * @param {string} keyName - The mapped name of the key.
 * @returns {boolean} True if the key is pressed.
 */
Input.isPressed = function (keyName) {
  if (this._isEscapeCompatible(keyName) && this.isPressed("escape")) {
    return true;
  } else {
    return !!this._currentState[keyName];
  }
};

/**
 * Checks whether a key is just pressed.
 *
 * @param {string} keyName - The mapped name of the key.
 * @returns {boolean} True if the key is triggered.
 */
Input.isTriggered = function (keyName) {
  if (this._isEscapeCompatible(keyName) && this.isTriggered("escape")) {
    return true;
  } else {
    return this._latestButton === keyName && this._pressedTime === 0;
  }
};

/**
 * Checks whether a key is just pressed or a key repeat occurred.
 *
 * @param {string} keyName - The mapped name of the key.
 * @returns {boolean} True if the key is repeated.
 */
Input.isRepeated = function (keyName) {
  if (this._isEscapeCompatible(keyName) && this.isRepeated("escape")) {
    return true;
  } else {
    return (
      this._latestButton === keyName &&
      (this._pressedTime === 0 ||
        (this._pressedTime >= this.keyRepeatWait &&
          this._pressedTime % this.keyRepeatInterval === 0))
    );
  }
};

/**
 * Checks whether a key is kept depressed.
 *
 * @param {string} keyName - The mapped name of the key.
 * @returns {boolean} True if the key is long-pressed.
 */
Input.isLongPressed = function (keyName) {
  if (this._isEscapeCompatible(keyName) && this.isLongPressed("escape")) {
    return true;
  } else {
    return (
      this._latestButton === keyName && this._pressedTime >= this.keyRepeatWait
    );
  }
};

/**
 * The four direction value as a number of the numpad, or 0 for neutral.
 *
 * @readonly
 * @type number
 * @name Input.dir4
 */
Object.defineProperty(Input, "dir4", {
  get: function () {
    return this._dir4;
  },
  configurable: true,
});

/**
 * The eight direction value as a number of the numpad, or 0 for neutral.
 *
 * @readonly
 * @type number
 * @name Input.dir8
 */
Object.defineProperty(Input, "dir8", {
  get: function () {
    return this._dir8;
  },
  configurable: true,
});

/**
 * The time of the last input in milliseconds.
 *
 * @readonly
 * @type number
 * @name Input.date
 */
Object.defineProperty(Input, "date", {
  get: function () {
    return this._date;
  },
  configurable: true,
});

Input.virtualClick = function (buttonName) {
  this._virtualButton = buttonName;
};

Input._setupEventHandlers = function () {
  document.addEventListener("keydown", this._onKeyDown.bind(this));
  document.addEventListener("keyup", this._onKeyUp.bind(this));
  window.addEventListener("blur", this._onLostFocus.bind(this));
};

Input._onKeyDown = function (event) {
  if (this._shouldPreventDefault(event.keyCode)) {
    event.preventDefault();
  }
  if (event.keyCode === 144) {
    // Numlock
    this.clear();
  }
  const buttonName = this.keyMapper[event.keyCode];
  if (buttonName) {
    this._currentState[buttonName] = true;
  }
};

Input._shouldPreventDefault = function (keyCode) {
  switch (keyCode) {
    case 8: // backspace
    case 9: // tab
    case 33: // pageup
    case 34: // pagedown
    case 37: // left arrow
    case 38: // up arrow
    case 39: // right arrow
    case 40: // down arrow
      return true;
  }
  return false;
};

Input._onKeyUp = function (event) {
  const buttonName = this.keyMapper[event.keyCode];
  if (buttonName) {
    this._currentState[buttonName] = false;
  }
};

Input._onLostFocus = function () {
  this.clear();
};

Input._pollGamepads = function () {
  if (navigator.getGamepads) {
    const gamepads = navigator.getGamepads();
    if (gamepads) {
      for (const gamepad of gamepads) {
        if (gamepad && gamepad.connected) {
          this._updateGamepadState(gamepad);
        }
      }
    }
  }
};

Input._updateGamepadState = function (gamepad) {
  const lastState = this._gamepadStates[gamepad.index] || [];
  const newState = [];
  const buttons = gamepad.buttons;
  const axes = gamepad.axes;
  const threshold = 0.5;
  newState[12] = false;
  newState[13] = false;
  newState[14] = false;
  newState[15] = false;
  for (let i = 0; i < buttons.length; i++) {
    newState[i] = buttons[i].pressed;
  }
  if (axes[1] < -threshold) {
    newState[12] = true; // up
  } else if (axes[1] > threshold) {
    newState[13] = true; // down
  }
  if (axes[0] < -threshold) {
    newState[14] = true; // left
  } else if (axes[0] > threshold) {
    newState[15] = true; // right
  }
  for (let j = 0; j < newState.length; j++) {
    if (newState[j] !== lastState[j]) {
      const buttonName = this.gamepadMapper[j];
      if (buttonName) {
        this._currentState[buttonName] = newState[j];
      }
    }
  }
  this._gamepadStates[gamepad.index] = newState;
};

Input._updateDirection = function () {
  let x = this._signX();
  let y = this._signY();
  this._dir8 = this._makeNumpadDirection(x, y);
  if (x !== 0 && y !== 0) {
    if (this._preferredAxis === "x") {
      y = 0;
    } else {
      x = 0;
    }
  } else if (x !== 0) {
    this._preferredAxis = "y";
  } else if (y !== 0) {
    this._preferredAxis = "x";
  }
  this._dir4 = this._makeNumpadDirection(x, y);
};

Input._signX = function () {
  const left = this.isPressed("left") ? 1 : 0;
  const right = this.isPressed("right") ? 1 : 0;
  return right - left;
};

Input._signY = function () {
  const up = this.isPressed("up") ? 1 : 0;
  const down = this.isPressed("down") ? 1 : 0;
  return down - up;
};

Input._makeNumpadDirection = function (x, y) {
  if (x === 0 && y === 0) {
    return 0;
  } else {
    return 5 - y * 3 + x;
  }
};

Input._isEscapeCompatible = function (keyName) {
  return keyName === "cancel" || keyName === "menu";
};

//-----------------------------------------------------------------------------
/**
 * The static class that handles input data from the mouse and touchscreen.
 *
 * @namespace
 */
function TouchInput() {
  throw new Error("This is a static class");
}

/**
 * Initializes the touch system.
 */
TouchInput.initialize = function () {
  this.clear();
  this._setupEventHandlers();
};

/**
 * The wait time of the pseudo key repeat in frames.
 *
 * @type number
 */
TouchInput.keyRepeatWait = 24;

/**
 * The interval of the pseudo key repeat in frames.
 *
 * @type number
 */
TouchInput.keyRepeatInterval = 6;

/**
 * The threshold number of pixels to treat as moved.
 *
 * @type number
 */
TouchInput.moveThreshold = 10;

/**
 * Clears all the touch data.
 */
TouchInput.clear = function () {
  this._mousePressed = false;
  this._screenPressed = false;
  this._pressedTime = 0;
  this._clicked = false;
  this._newState = this._createNewState();
  this._currentState = this._createNewState();
  this._x = 0;
  this._y = 0;
  this._triggerX = 0;
  this._triggerY = 0;
  this._moved = false;
  this._date = 0;
};

/**
 * Updates the touch data.
 */
TouchInput.update = function () {
  this._currentState = this._newState;
  this._newState = this._createNewState();
  this._clicked = this._currentState.released && !this._moved;
  if (this.isPressed()) {
    this._pressedTime++;
  }
};

/**
 * Checks whether the mouse button or touchscreen has been pressed and
 * released at the same position.
 *
 * @returns {boolean} True if the mouse button or touchscreen is clicked.
 */
TouchInput.isClicked = function () {
  return this._clicked;
};

/**
 * Checks whether the mouse button or touchscreen is currently pressed down.
 *
 * @returns {boolean} True if the mouse button or touchscreen is pressed.
 */
TouchInput.isPressed = function () {
  return this._mousePressed || this._screenPressed;
};

/**
 * Checks whether the left mouse button or touchscreen is just pressed.
 *
 * @returns {boolean} True if the mouse button or touchscreen is triggered.
 */
TouchInput.isTriggered = function () {
  return this._currentState.triggered;
};

/**
 * Checks whether the left mouse button or touchscreen is just pressed
 * or a pseudo key repeat occurred.
 *
 * @returns {boolean} True if the mouse button or touchscreen is repeated.
 */
TouchInput.isRepeated = function () {
  return (
    this.isPressed() &&
    (this._currentState.triggered ||
      (this._pressedTime >= this.keyRepeatWait &&
        this._pressedTime % this.keyRepeatInterval === 0))
  );
};

/**
 * Checks whether the left mouse button or touchscreen is kept depressed.
 *
 * @returns {boolean} True if the left mouse button or touchscreen is long-pressed.
 */
TouchInput.isLongPressed = function () {
  return this.isPressed() && this._pressedTime >= this.keyRepeatWait;
};

/**
 * Checks whether the right mouse button is just pressed.
 *
 * @returns {boolean} True if the right mouse button is just pressed.
 */
TouchInput.isCancelled = function () {
  return this._currentState.cancelled;
};

/**
 * Checks whether the mouse or a finger on the touchscreen is moved.
 *
 * @returns {boolean} True if the mouse or a finger on the touchscreen is moved.
 */
TouchInput.isMoved = function () {
  return this._currentState.moved;
};

/**
 * Checks whether the mouse is moved without pressing a button.
 *
 * @returns {boolean} True if the mouse is hovered.
 */
TouchInput.isHovered = function () {
  return this._currentState.hovered;
};

/**
 * Checks whether the left mouse button or touchscreen is released.
 *
 * @returns {boolean} True if the mouse button or touchscreen is released.
 */
TouchInput.isReleased = function () {
  return this._currentState.released;
};

/**
 * The horizontal scroll amount.
 *
 * @readonly
 * @type number
 * @name TouchInput.wheelX
 */
Object.defineProperty(TouchInput, "wheelX", {
  get: function () {
    return this._currentState.wheelX;
  },
  configurable: true,
});

/**
 * The vertical scroll amount.
 *
 * @readonly
 * @type number
 * @name TouchInput.wheelY
 */
Object.defineProperty(TouchInput, "wheelY", {
  get: function () {
    return this._currentState.wheelY;
  },
  configurable: true,
});

/**
 * The x coordinate on the canvas area of the latest touch event.
 *
 * @readonly
 * @type number
 * @name TouchInput.x
 */
Object.defineProperty(TouchInput, "x", {
  get: function () {
    return this._x;
  },
  configurable: true,
});

/**
 * The y coordinate on the canvas area of the latest touch event.
 *
 * @readonly
 * @type number
 * @name TouchInput.y
 */
Object.defineProperty(TouchInput, "y", {
  get: function () {
    return this._y;
  },
  configurable: true,
});

/**
 * The time of the last input in milliseconds.
 *
 * @readonly
 * @type number
 * @name TouchInput.date
 */
Object.defineProperty(TouchInput, "date", {
  get: function () {
    return this._date;
  },
  configurable: true,
});

TouchInput._createNewState = function () {
  return {
    triggered: false,
    cancelled: false,
    moved: false,
    hovered: false,
    released: false,
    wheelX: 0,
    wheelY: 0,
  };
};

TouchInput._setupEventHandlers = function () {
  const pf = { passive: false };
  document.addEventListener("mousedown", this._onMouseDown.bind(this));
  document.addEventListener("mousemove", this._onMouseMove.bind(this));
  document.addEventListener("mouseup", this._onMouseUp.bind(this));
  document.addEventListener("wheel", this._onWheel.bind(this), pf);
  document.addEventListener("touchstart", this._onTouchStart.bind(this), pf);
  document.addEventListener("touchmove", this._onTouchMove.bind(this), pf);
  document.addEventListener("touchend", this._onTouchEnd.bind(this));
  document.addEventListener("touchcancel", this._onTouchCancel.bind(this));
  window.addEventListener("blur", this._onLostFocus.bind(this));
};

TouchInput._onMouseDown = function (event) {
  if (event.button === 0) {
    this._onLeftButtonDown(event);
  } else if (event.button === 1) {
    this._onMiddleButtonDown(event);
  } else if (event.button === 2) {
    this._onRightButtonDown(event);
  }
};

TouchInput._onLeftButtonDown = function (event) {
  const x = Graphics.pageToCanvasX(event.pageX);
  const y = Graphics.pageToCanvasY(event.pageY);
  if (Graphics.isInsideCanvas(x, y)) {
    this._mousePressed = true;
    this._pressedTime = 0;
    this._onTrigger(x, y);
  }
};

TouchInput._onMiddleButtonDown = function (/*event*/) {
  //
};

TouchInput._onRightButtonDown = function (event) {
  const x = Graphics.pageToCanvasX(event.pageX);
  const y = Graphics.pageToCanvasY(event.pageY);
  if (Graphics.isInsideCanvas(x, y)) {
    this._onCancel(x, y);
  }
};

TouchInput._onMouseMove = function (event) {
  const x = Graphics.pageToCanvasX(event.pageX);
  const y = Graphics.pageToCanvasY(event.pageY);
  if (this._mousePressed) {
    this._onMove(x, y);
  } else if (Graphics.isInsideCanvas(x, y)) {
    this._onHover(x, y);
  }
};

TouchInput._onMouseUp = function (event) {
  if (event.button === 0) {
    const x = Graphics.pageToCanvasX(event.pageX);
    const y = Graphics.pageToCanvasY(event.pageY);
    this._mousePressed = false;
    this._onRelease(x, y);
  }
};

TouchInput._onWheel = function (event) {
  this._newState.wheelX += event.deltaX;
  this._newState.wheelY += event.deltaY;
  event.preventDefault();
};

TouchInput._onTouchStart = function (event) {
  for (const touch of event.changedTouches) {
    const x = Graphics.pageToCanvasX(touch.pageX);
    const y = Graphics.pageToCanvasY(touch.pageY);
    if (Graphics.isInsideCanvas(x, y)) {
      this._screenPressed = true;
      this._pressedTime = 0;
      if (event.touches.length >= 2) {
        this._onCancel(x, y);
      } else {
        this._onTrigger(x, y);
      }
      event.preventDefault();
    }
  }
  if (window.cordova || window.navigator.standalone) {
    event.preventDefault();
  }
};

TouchInput._onTouchMove = function (event) {
  for (const touch of event.changedTouches) {
    const x = Graphics.pageToCanvasX(touch.pageX);
    const y = Graphics.pageToCanvasY(touch.pageY);
    this._onMove(x, y);
  }
};

TouchInput._onTouchEnd = function (event) {
  for (const touch of event.changedTouches) {
    const x = Graphics.pageToCanvasX(touch.pageX);
    const y = Graphics.pageToCanvasY(touch.pageY);
    this._screenPressed = false;
    this._onRelease(x, y);
  }
};

TouchInput._onTouchCancel = function (/*event*/) {
  this._screenPressed = false;
};

TouchInput._onLostFocus = function () {
  this.clear();
};

TouchInput._onTrigger = function (x, y) {
  this._newState.triggered = true;
  this._x = x;
  this._y = y;
  this._triggerX = x;
  this._triggerY = y;
  this._moved = false;
  this._date = Date.now();
};

TouchInput._onCancel = function (x, y) {
  this._newState.cancelled = true;
  this._x = x;
  this._y = y;
};

TouchInput._onMove = function (x, y) {
  const dx = Math.abs(x - this._triggerX);
  const dy = Math.abs(y - this._triggerY);
  if (dx > this.moveThreshold || dy > this.moveThreshold) {
    this._moved = true;
  }
  if (this._moved) {
    this._newState.moved = true;
    this._x = x;
    this._y = y;
  }
};

TouchInput._onHover = function (x, y) {
  this._newState.hovered = true;
  this._x = x;
  this._y = y;
};

TouchInput._onRelease = function (x, y) {
  this._newState.released = true;
  this._x = x;
  this._y = y;
};

//-----------------------------------------------------------------------------
/**
 * The static class that handles JSON with object information.
 *
 * @namespace
 */
function JsonEx() {
  throw new Error("This is a static class");
}

/**
 * The maximum depth of objects.
 *
 * @type number
 * @default 100
 */
JsonEx.maxDepth = 100;

/**
 * Converts an object to a JSON string with object information.
 *
 * @param {object} object - The object to be converted.
 * @returns {string} The JSON string.
 */
JsonEx.stringify = function (object) {
  return JSON.stringify(this._encode(object, 0));
};

/**
 * Parses a JSON string and reconstructs the corresponding object.
 *
 * @param {string} json - The JSON string.
 * @returns {object} The reconstructed object.
 */
JsonEx.parse = function (json) {
  return this._decode(JSON.parse(json));
};

/**
 * Makes a deep copy of the specified object.
 *
 * @param {object} object - The object to be copied.
 * @returns {object} The copied object.
 */
JsonEx.makeDeepCopy = function (object) {
  return this.parse(this.stringify(object));
};

JsonEx._encode = function (value, depth) {
  // [Note] The handling code for circular references in certain versions of
  //   MV has been removed because it was too complicated and expensive.
  if (depth >= this.maxDepth) {
    throw new Error("Object too deep");
  }
  const type = Object.prototype.toString.call(value);
  if (type === "[object Object]" || type === "[object Array]") {
    const constructorName = value.constructor.name;
    if (constructorName !== "Object" && constructorName !== "Array") {
      value["@"] = constructorName;
    }
    for (const key of Object.keys(value)) {
      value[key] = this._encode(value[key], depth + 1);
    }
  }
  return value;
};

JsonEx._decode = function (value) {
  const type = Object.prototype.toString.call(value);
  if (type === "[object Object]" || type === "[object Array]") {
    if (value["@"]) {
      const constructor = window[value["@"]];
      if (constructor) {
        Object.setPrototypeOf(value, constructor.prototype);
      }
    }
    for (const key of Object.keys(value)) {
      value[key] = this._decode(value[key]);
    }
  }
  return value;
};

//-----------------------------------------------------------------------------