/*:
 * @target MZ
 * @author F_
 * @base Fs
 * @base ApngCore
 * @orderAfter Fs
 *
 * @plugindesc
 *
 * 独自のメニュー画面を実装します。
 *
 * @help
 *
 * 独自のメニュー画面を実装します。
 *
 *
 * ■ アクター画像の設定
 *
 * 各アクターに対応する画像は img/pictures/ 以下に配置する必要があります。
 *
 * 配置した画像は「アクター画像設定」により各アクターと対応付けます。
 *
 *
 *
 * @param ActorImages
 * @type struct<ActorImage>[]
 * @default []
 * @text アクター画像設定
 * @desc アクターに対応する画像の設定。
 *
 * @param HelpWindowConfig
 * @type struct<HelpWindowConfig>
 * @text ヘルプウィンドウ設定
 * @desc ヘルプウィンドウに関する設定。
 *
 * @param CommandWindowConfig
 * @type struct<CommandWindowConfig>
 * @text コマンドウィンドウ設定
 * @desc コマンドウィンドウに関する設定。
 *
 * @param GoldWindowConfig
 * @type struct<GoldWindowConfig>
 * @text 所持金ウィンドウ設定
 * @desc 所持金ウィンドウに関する設定。
 *
 * @param StatusWindowConfig
 * @type struct<StatusWindowConfig>
 * @text ステータスウィンドウ設定
 * @desc ステータスウィンドウに関する設定。
 */

/*~struct~ActorImage:
 * @param actorId
 * @type actor
 * @default 0
 * @text 対象アクター
 * @desc 画像を設定する対象のアクター。
 *
 * @param path
 * @type file
 * @dir img/pictures/
 * @text 画像
 * @desc アクターに対応する画像。
 */

/*~struct~HelpWindowConfig:
 * @param lines
 * @type number
 * @min 1
 * @max 9
 * @default 1
 * @text 行数
 * @desc 説明文の表示行数。
 *
 * @param descriptions
 * @type struct<CommandDescs>
 * @text コマンド説明文
 * @desc 各コマンドに対応する説明文。
 */

/*~struct~CommandDescs:
 * @param item
 * @type multiline_string
 * @text アイテム
 * @desc 「アイテム」コマンドの説明文。
 *
 * @param skill
 * @type multiline_string
 * @text スキル
 * @desc 「スキル」コマンドの説明文。
 *
 * @param equip
 * @type multiline_string
 * @text 装備
 * @desc 「装備」コマンドの説明文。
 *
 * @param status
 * @type multiline_string
 * @text ステータス
 * @desc 「ステータス」コマンドの説明文。
 *
 * @param formation
 * @type multiline_string
 * @text 並び替え
 * @desc 「並び替え」コマンドの説明文。
 *
 * @param options
 * @type multiline_string
 * @text オプション
 * @desc 「オプション」コマンドの説明文。
 *
 * @param save
 * @type multiline_string
 * @text セーブ
 * @desc 「セーブ」コマンドの説明文。
 *
 * @param load
 * @type multiline_string
 * @text ロード
 * @desc 「ロード」コマンドの説明文。
 *
 * @param gameEnd
 * @type multiline_string
 * @text ゲーム終了
 * @desc 「ゲーム終了」コマンドの説明文。
 */

/*~struct~CommandWindowConfig:
 * @param useLoadCommand
 * @type boolean
 * @default true
 * @text ロードコマンドの追加
 * @desc 「ロード」コマンドを追加するかどうか。
 *
 * @param loadCommandName
 * @parent useLoadCommand
 * @type string
 * @default ロード
 * @text ロードコマンド名
 * @desc 「ロード」コマンドの表示名。
 */

/*~struct~GoldWindowConfig:
 * @param goldLabel
 * @type string
 * @default 所持金
 * @text 所持金項目名
 * @desc 所持金の左側に表示する項目名。
 *
 * @param playtimeLabel
 * @type string
 * @default プレイ時間
 * @text プレイ時間項目名
 * @desc プレイ時間の左側に表示する項目名。
 *
 * @param lineHeight
 * @type number
 * @default 36
 * @text 行の高さ
 * @desc 行の高さ(px)。
 *
 * @param fontSize
 * @type number
 * @default 26
 * @text フォントサイズ
 * @desc フォントサイズ(px)。
 */

/*~struct~StatusWindowConfig:
 * @param columns
 * @type number
 * @min 1
 * @max 8
 * @default 4
 * @text 列数
 * @desc 同時に表示する列数。
 *
 * @param baseX
 * @type number
 * @default 0
 * @text 基本水平位置
 * @desc 基本情報のX方向の位置(px)。
 *
 * @param baseY
 * @type number
 * @default 0
 * @text 基本垂直位置
 * @desc 基本情報のY方向の位置(px)。
 *
 * @param imageY
 * @type number
 * @default 0
 * @text アクター画像位置
 * @desc アクター画像下端のY方向の位置(px)。
 *
 * @param gaugeY
 * @type number
 * @default 0
 * @text ゲージ位置
 * @desc ゲージ群上端のY方向の位置(px)。
 *
 * @param gaugeWidth
 * @type number
 * @default 128
 * @text ゲージ長
 * @desc ゲージのラベルを含めたX方向の長さ(px)。
 */

"use strict";

{
  const PLUGIN_NAME = "CustomMadeMenuScene";

  const DEFAULT_COMMAND_DESCS = {
    item: "",
    skill: "",
    equip: "",
    status: "",
    formation: "",
    options: "",
    save: "",
    load: "",
    gameEnd: "",
  };

  const DEFAULT_HELP_WINDOW_CONFIG = {
    lines: 1,
    descriptions: DEFAULT_COMMAND_DESCS,
  };

  const DEFAULT_COMMAND_WINDOW_CONFIG = {
    useLoadCommand: true,
    loadCommandName: "Load",
  };

  const DEFAULT_GOLD_WINDOW_CONFIG = {
    goldLabel: "Gold",
    playtimeLabel: "Play Time",
    lineHeight: 36,
    fontSize: 26,
  };

  const DEFAULT_STATUS_WINDOW_CONFIG = {
    columns: 4,
    baseX: 0,
    baseY: 0,
    imageY: 0,
    gaugeY: 0,
    gaugeWidth: 128,
  };

  const { P, Z } = Fs.v0_4;

  const parseParam = P.begin(PluginManager.parameters(PLUGIN_NAME));

  const ACTOR_IMAGE_MAP = parseParam(
    "ActorImages",
    P.validate(
      P.like([
        {
          actorId: P.natural,
          path: P.string,
        },
      ]),
      (value, ok, err) => {
        const map = new Map();
        for (const { actorId, path } of value) {
          if (!map.has(actorId)) {
            map.set(actorId, path);
          } else {
            return err(`duplicate actor ID: ${actorId}`);
          }
        }
        return ok(map);
      }
    )
  );

  const HELP_WINDOW_CONFIG = parseParam(
    "HelpWindowConfig",
    P.optional(
      P.like({
        lines: P.natural,
        descriptions: P.optional(
          P.like({
            item: P.string,
            skill: P.string,
            equip: P.string,
            status: P.string,
            formation: P.string,
            options: P.string,
            save: P.string,
            load: P.string,
            gameEnd: P.string,
          }),
          DEFAULT_COMMAND_DESCS
        ),
      }),
      DEFAULT_HELP_WINDOW_CONFIG
    )
  );

  const COMMAND_WINDOW_CONFIG = parseParam(
    "CommandWindowConfig",
    P.optional(
      P.like({
        useLoadCommand: P.boolean,
        loadCommandName: P.string,
      }),
      DEFAULT_COMMAND_WINDOW_CONFIG
    )
  );

  const GOLD_WINDOW_CONFIG = parseParam(
    "GoldWindowConfig",
    P.optional(
      P.like({
        goldLabel: P.string,
        playtimeLabel: P.string,
        lineHeight: P.natural,
        fontSize: P.natural,
      }),
      DEFAULT_GOLD_WINDOW_CONFIG
    )
  );

  const STATUS_WINDOW_CONFIG = parseParam(
    "StatusWindowConfig",
    P.optional(
      P.like({
        columns: P.natural,
        baseX: P.natural,
        baseY: P.natural,
        imageY: P.natural,
        gaugeY: P.natural,
        gaugeWidth: P.natural,
      }),
      DEFAULT_STATUS_WINDOW_CONFIG
    )
  );

  const LOAD_COMMAND_SYMBOL = "load";

  class ApngSprite extends PIXI.Sprite {
    #apng;
    #time;
    #prevTime;
    #canvas;
    #prevCanvas;
    #texture;

    constructor() {
      super(null);

      this.#apng = undefined;
      this.#time = 0;
      this.#prevTime = -1;
      this.#canvas = this.#initCanvas();
      this.#prevCanvas = this.#initCanvas();
      this.#texture = this.#initTexture(this.#canvas);
    }

    get time() {
      return this.#time;
    }
    set time(value) {
      this.#time = value;
    }

    setImage(apng) {
      if (this.#apng !== apng) {
        this.#apng = apng;
        this.#prevTime = -1;
        if (apng !== undefined) {
          this.#canvas.width = apng.width;
          this.#canvas.height = apng.height;
          this.#prevCanvas.width = apng.width;
          this.#prevCanvas.height = apng.height;
          this.texture = this.#texture;
        } else {
          this.texture = null;
        }
      }
    }

    update() {
      this.#updateCanvas();
    }

    destroy(...args) {
      const texture = this.#texture;
      if (texture !== null) {
        this.texture = this.#texture = null;
        texture.destroy(true);
      }

      super.destroy(...args);
    }

    #initCanvas() {
      return Object.assign(document.createElement("canvas"), {
        width: 1,
        height: 1,
      });
    }

    #initTexture(canvas) {
      return new PIXI.Texture(
        new PIXI.BaseTexture(canvas, {
          mipmap: PIXI.MIPMAP_MODES.OFF,
          scaleMode: PIXI.SCALE_MODES.LINEAR,
        })
      );
    }

    #updateCanvas() {
      const apng = this.#apng;
      const time = this.#time;
      const prevTime = this.#prevTime;
      const canvas = this.#canvas;
      const prevCanvas = this.#prevCanvas;

      if (apng !== undefined) {
        const data = this.#makeRenderData(apng, prevTime, time);
        if (data !== undefined) {
          this.#renderFrames(canvas, prevCanvas, data);
          this.#texture.update();
        }
        this.#prevTime = time;
      }
    }

    #makeRenderData(apng, prevTime, time) {
      if (prevTime !== time) {
        const prevOrder = this.#frameOrder(apng, prevTime);
        const order = this.#frameOrder(apng, time);
        if (prevTime < 0 || prevOrder !== order) {
          return order !== -1
            ? this.#makeRangeRenderData(apng, prevOrder, order)
            : this.#makeClearRenderData();
        }
      }
      return undefined;
    }

    #frameOrder(apng, time) {
      const { numPlays, duration, frames } = apng;
      if (time >= 0) {
        const maxLoops = numPlays !== 0 ? numPlays : Number.POSITIVE_INFINITY;
        const loops =
          duration !== 0 ? Math.min(Math.floor(time / duration), maxLoops) : 0;
        const frame = this.#frameIndex(frames, time - loops * duration);
        const order = frame !== -1 ? frame + loops * frames.length : -1;
        return order;
      } else {
        return -1;
      }
    }

    #frameIndex(frames, position) {
      let rest = position;
      for (const [index, frame] of frames.entries()) {
        if (rest <= frame.delay) {
          return index;
        }
        rest -= frame.delay;
      }
      return frames.length - 1;
    }

    #makeRangeRenderData(apng, prevOrder, order) {
      const { frames } = apng;
      const { start, end } = this.#deltaFrameRange(frames, prevOrder, order);
      const last = start - 1;
      return {
        lastFrame: last !== -1 ? frames[last] : undefined,
        frames: this.#deltaFrames(frames, start, end),
      };
    }

    #deltaFrameRange(frames, prevOrder, order) {
      const prevIndex = prevOrder % frames.length;
      const index = order % frames.length;
      const start =
        prevOrder !== -1 && prevOrder < order && prevIndex < index
          ? prevIndex + 1
          : 0;
      const end = index + 1;
      return { start, end };
    }

    #deltaFrames(frames, start, end) {
      const result = [];
      const filledRects = [];
      for (const [n, frame] of frames.slice(start, end).reverse().entries()) {
        const { offsetX, offsetY, width, height, disposeOp } = frame;
        if (n === 0 || disposeOp !== APNG.DISPOSE_OP_PREVIOUS) {
          const minX = offsetX;
          const maxX = offsetX + width;
          const minY = offsetY;
          const maxY = offsetY + height;
          if (
            filledRects.every(
              (r) =>
                minX < r.minX || maxX > r.maxX || minY < r.minY || maxY > r.maxY
            )
          ) {
            result.push(frame);
            if (disposeOp === APNG.DISPOSE_OP_BACKGROUND) {
              filledRects.push({ minX, maxX, minY, maxY });
            }
          }
        }
      }
      return result.reverse();
    }

    #makeClearRenderData() {
      return { lastFrame: undefined, frames: [] };
    }

    #renderFrames(canvas, prevCanvas, data) {
      const { lastFrame, frames } = data;
      const context = canvas.getContext("2d");
      const prevContext = prevCanvas.getContext("2d");
      if (frames.length !== 0) {
        let prevFrame = lastFrame;
        for (const frame of frames) {
          this.#renderSingleFrame(context, prevContext, frame, prevFrame);
          prevFrame = frame;
        }
      } else {
        this.#disposeOfPrevFrame(context, prevContext, lastFrame);
      }
    }

    #renderSingleFrame(context, prevContext, frame, prevFrame) {
      this.#disposeOfPrevFrame(context, prevContext, prevFrame);
      this.#savePrevFrameIfRequired(context, prevContext, frame);
      this.#drawFrameImage(context, frame);
    }

    #disposeOfPrevFrame(context, prevContext, prevFrame) {
      if (prevFrame !== undefined) {
        this.#applyDisposeOp(context, prevContext, prevFrame);
      } else {
        const { canvas } = context;
        context.clearRect(0, 0, canvas.width, canvas.height);
      }
    }

    #applyDisposeOp(context, prevContext, frame) {
      const { disposeOp } = frame;
      switch (disposeOp) {
        case APNG.DISPOSE_OP_NONE:
          return this.#applyDisposeOpNone();
        case APNG.DISPOSE_OP_BACKGROUND:
          return this.#applyDisposeOpBackground(context, frame);
        case APNG.DISPOSE_OP_PREVIOUS:
          return this.#applyDisposeOpPrevious(context, prevContext, frame);
        default:
          throw new Error(`invalid APNG disposeOp: ${disposeOp}`);
      }
    }

    #applyDisposeOpNone() {}

    #applyDisposeOpBackground(context, frame) {
      const { offsetX, offsetY, width, height } = frame;
      context.clearRect(offsetX, offsetY, width, height);
    }

    #applyDisposeOpPrevious(context, prevContext, frame) {
      const { offsetX, offsetY, width, height } = frame;
      context.clearRect(offsetX, offsetY, width, height);
      context.drawImage(
        prevContext.canvas,
        offsetX,
        offsetY,
        width,
        height,
        offsetX,
        offsetY,
        width,
        height
      );
    }

    #savePrevFrameIfRequired(context, prevContext, frame) {
      const { offsetX, offsetY, width, height, disposeOp } = frame;
      if (disposeOp === APNG.DISPOSE_OP_PREVIOUS) {
        prevContext.clearRect(offsetX, offsetY, width, height);
        prevContext.drawImage(
          context.canvas,
          offsetX,
          offsetY,
          width,
          height,
          offsetX,
          offsetY,
          width,
          height
        );
      }
    }

    #drawFrameImage(context, frame) {
      const { offsetX, offsetY, width, height, image } = frame;
      if (this.#shouldClearRect(frame)) {
        context.clearRect(offsetX, offsetY, width, height);
      }
      context.drawImage(image, offsetX, offsetY, width, height);
    }

    #shouldClearRect(frame) {
      const { blendOp } = frame;
      switch (blendOp) {
        case APNG.BLEND_OP_SOURCE:
          return true;
        case APNG.BLEND_OP_OVER:
          return false;
        default:
          throw new Error(`invalid APNG blendOp: ${blendOp}`);
      }
    }
  }

  class MenuActorImageSprite extends Sprite {
    initialize() {
      super.initialize();

      this._actor = null;
      this._path = undefined;
      this._time = 0;
      this._innerSprite = this.initApngSprite();
    }

    initApngSprite() {
      const sprite = new ApngSprite();
      sprite.anchor.set(0.5, 1);
      return this.addChild(sprite);
    }

    setup(actor) {
      this._actor = actor;
      this.updateBitmap();
    }

    update() {
      super.update();

      this.updateTime();
      this.updateBitmap();
    }

    updateTime() {
      this._time++;
    }

    updateBitmap() {
      const ASSUMED_FPS = 60;
      const actor = this._actor;
      const time = this._time;
      const sprite = this._innerSprite;

      const path =
        actor !== null ? ACTOR_IMAGE_MAP.get(actor.actorId()) : undefined;
      if (this._path !== path) {
        this._path = path;
        ImageManager.loadApngPicture(path, (apng) => sprite.setImage(apng));
      }

      sprite.time = (time * 1000) / ASSUMED_FPS;
    }
  }

  class MenuGaugeSprite extends Sprite_Gauge {
    bitmapWidth() {
      return STATUS_WINDOW_CONFIG.gaugeWidth;
    }
  }

  class MenuHelpWindow extends Window_Help {
    refresh() {
      const text = this._text;
      const lines = text.split("\n");
      const rect = this.baseTextRect();
      const lineHeight = this.lineHeight();

      this.contents.clear();
      for (const [i, line] of lines.entries()) {
        this.drawText(
          line,
          rect.x,
          rect.y + lineHeight * i,
          rect.width,
          "left"
        );
      }
    }
  }

  class MenuGoldWindow extends Window_Gold {
    initialize(...args) {
      super.initialize(...args);

      this._playtime = 0;
    }

    lineHeight() {
      return GOLD_WINDOW_CONFIG.lineHeight;
    }

    update() {
      super.update();

      this.updatePlaytime();
    }

    updatePlaytime() {
      const playtime = $gameSystem.playtime();
      if (this._playtime !== playtime) {
        this._playtime = playtime;
        this.refreshPlaytimeValue();
      }
    }

    resetFontSettings() {
      super.resetFontSettings();

      this.contents.fontSize = GOLD_WINDOW_CONFIG.fontSize;
    }

    refresh() {
      this.contents.clear();
      this.drawGoldLabel();
      this.drawGoldValue();
      this.drawPlaytimeLabel();
      this.drawPlaytimeValue();
    }

    refreshPlaytimeValue() {
      this.clearPlaytimeValue();
      this.drawPlaytimeValue();
    }

    drawGoldLabel() {
      const { x, y, width } = this.itemLineRect(0);
      this.resetTextColor();
      this.drawText(GOLD_WINDOW_CONFIG.goldLabel, x, y, width, "left");
    }

    drawGoldValue() {
      const { x, y, width } = this.itemLineRect(1);
      this.drawCurrencyValue(this.value(), this.currencyUnit(), x, y, width);
    }

    drawPlaytimeLabel() {
      const { x, y, width } = this.itemLineRect(2);
      this.resetTextColor();
      this.drawText(GOLD_WINDOW_CONFIG.playtimeLabel, x, y, width, "left");
    }

    drawPlaytimeValue() {
      const { x, y, width } = this.itemLineRect(3);
      this.resetTextColor();
      this.drawText($gameSystem.playtimeText(), x, y, width, "right");
    }

    clearPlaytimeValue() {
      const { x, y, width, height } = this.itemLineRect(3);
      this.contents.clearRect(x, y, width, height);
    }
  }

  const swapSpriteGauge = Z.swapper("Sprite_Gauge");
  const swapWindowSelectable = Z.swapper("Window_Selectable");
  const swapWindowHelp = Z.swapper("Window_Help");
  const swapWindowGold = Z.swapper("Window_Gold");

  Z.redef(ImageManager, ($) => {
    const PICTURE_DIR = "img/pictures/";
    const APNG_EXT = ".png";
    const ENCRYPTED_FILE_SUFFIX = "_";

    $._apngEntries = new Map();

    $.modifyApngEntry = function (url, modifyFn) {
      const entries = this._apngEntries;
      const oldEntry = entries.get(url);
      const newEntry = modifyFn(oldEntry);
      const nextEntry =
        newEntry.status === "loaded"
          ? this.onApngEntryLoaded(newEntry)
          : newEntry;
      entries.set(url, nextEntry);
    };

    $.onApngEntryLoaded = function (entry) {
      const { callbacks, value } = entry;
      for (const callback of callbacks) {
        callback(value);
      }
      return { ...entry, callbacks: [] };
    };

    $.loadApngPicture = function (filename, callback) {
      this.loadApng(PICTURE_DIR, filename, callback);
    };

    $.loadApng = function (folder, filename, callback) {
      if (filename !== "") {
        const url = folder + Utils.encodeURI(filename) + APNG_EXT;
        this.loadApngFromUrl(url, callback);
      } else {
        callback(APNG.EMPTY);
      }
    };

    $.loadApngFromUrl = function (url, callback) {
      this.modifyApngEntry(url, (entry) => {
        if (entry !== undefined) {
          return {
            ...entry,
            callbacks: [...entry.callbacks, callback],
          };
        } else {
          return this.startLoadApngEntry(
            {
              status: "none",
              url,
              callbacks: [callback],
            },
            (result) => this.applyApngLoadResult(url, result)
          );
        }
      });
    };

    $.retryLoadApngFromUrl = function (url) {
      this.modifyApngEntry(url, (entry) => {
        if (entry.status === "error") {
          return this.startLoadApngEntry(entry, (result) =>
            this.applyApngLoadResult(url, result)
          );
        } else {
          return entry;
        }
      });
    };

    $.applyApngLoadResult = function (url, result) {
      this.modifyApngEntry(url, (entry) => {
        const { url, callbacks } = entry;
        if (result.ok) {
          return {
            status: "loaded",
            url,
            callbacks,
            value: result.value,
          };
        } else {
          return {
            status: "error",
            url,
            callbacks,
            error: result.error,
          };
        }
      });
    };

    $.startLoadApngEntry = function (entry, callback) {
      const { url, callbacks } = entry;
      const controller = new AbortController();
      const { signal } = controller;

      const invokeCallback = (result) => {
        if (!signal.aborted) {
          callback(result);
        }
      };

      this.loadApngFromUrlAsync(url, { signal }).then(
        (value) => invokeCallback({ ok: true, value }),
        (error) => invokeCallback({ ok: false, error })
      );

      return {
        status: "loading",
        url,
        callbacks,
        controller,
      };
    };

    $.loadApngFromUrlAsync = async function (url, options) {
      const encrypted = Utils.hasEncryptedImages();
      const actualUrl = encrypted ? url + ENCRYPTED_FILE_SUFFIX : url;
      const response = await fetch(actualUrl, options);
      const buffer = await response.arrayBuffer();
      const data = encrypted ? Utils.decryptArrayBuffer(buffer) : buffer;
      const apng = await APNG.parse(data);
      return apng;
    };

    const base$clear = $.clear;
    $.clear = function () {
      base$clear.apply(this, arguments);

      this.clearApngImages();
    };

    $.clearApngImages = function () {
      const entries = this._apngEntries;
      for (const entry of entries.values()) {
        if (entry.status === "loading") {
          entry.controller.abort();
        }
      }
      entries.clear();
    };

    const base$isReady = $.isReady;
    $.isReady = function () {
      const normalReady = base$isReady.apply(this, arguments);
      const apngReady = this.checkApngLoaded();
      return normalReady && apngReady;
    };

    $.checkApngLoaded = function () {
      const entries = Array.from(this._apngEntries.values());
      const error = entries.find((entry) => entry.status === "error");
      if (error !== undefined) {
        const { url } = error;
        const retry = this.retryLoadApngFromUrl.bind(this, url);
        throw ["LoadError", url, retry];
      } else {
        return entries.every((entry) => entry.status === "loaded");
      }
    };
  });

  Z.redef(Window_MenuCommand.prototype, ($) => {
    $.updateHelp = function () {
      const { descriptions } = HELP_WINDOW_CONFIG;
      const helpWindow = this._helpWindow;
      if (helpWindow !== null) {
        const symbol = this.currentSymbol();
        const text =
          symbol !== null && descriptions.hasOwnProperty(symbol)
            ? descriptions[symbol]
            : "";
        helpWindow.setText(text);
      }
    };

    const base$addSaveCommand = $.addSaveCommand;
    $.addSaveCommand = function () {
      base$addSaveCommand.apply(this, arguments);

      this.addLoadCommand();
    };

    $.addLoadCommand = function () {
      const { useLoadCommand, loadCommandName } = COMMAND_WINDOW_CONFIG;
      if (useLoadCommand) {
        const enabled = this.isLoadEnabled();
        this.addCommand(loadCommandName, LOAD_COMMAND_SYMBOL, enabled);
      }
    };

    $.isLoadEnabled = function () {
      return !DataManager.isEventTest() && DataManager.isAnySavefileExists();
    };
  });

  Z.redef(Window_MenuStatus.prototype, ($) => {
    $.maxCols = function () {
      return STATUS_WINDOW_CONFIG.columns;
    };

    $.itemHeight = function () {
      return this.innerHeight;
    };

    $.drawItemImage = function (_index) {};

    $.drawItemStatus = function (index) {
      const {
        baseX: baseOffsetX,
        baseY: baseOffsetY,
        imageY: imageOffsetY,
        gaugeY: gaugeOffsetY,
        gaugeWidth,
      } = STATUS_WINDOW_CONFIG;

      const actor = this.actor(index);
      const { x, y, width } = this.itemRect(index);
      const lineHeight = this.lineHeight();

      const baseX = x + baseOffsetX;
      const baseY = y + baseOffsetY;
      const imageX = x + width / 2;
      const imageY = y + imageOffsetY;
      const gaugeX = x + Math.floor((width - gaugeWidth) / 2);
      const gaugeY = y + gaugeOffsetY;

      this.drawActorName(actor, baseX, baseY + lineHeight * 0);
      this.drawActorLevel(actor, baseX, baseY + lineHeight * 1);
      this.placeActorImage(actor, imageX, imageY);
      swapSpriteGauge(globalThis, MenuGaugeSprite, () => {
        this.placeBasicGauges(actor, gaugeX, gaugeY);
      });
    };

    $.placeActorImage = function (actor, x, y) {
      const key = "actor%1-image".format(actor.actorId());
      const sprite = this.createInnerSprite(key, MenuActorImageSprite);
      sprite.setup(actor);
      sprite.move(x, y);
      sprite.show();
    };
  });

  Z.redef(Scene_Menu.prototype, ($) => {
    $.isBottomHelpMode = function () {
      return false;
    };

    $.helpAreaHeight = function () {
      return this.calcWindowHeight(HELP_WINDOW_CONFIG.lines, false);
    };

    const base$create = $.create;
    $.create = function () {
      base$create.apply(this, arguments);

      this.createHelpWindow();
      this._commandWindow.setHelpWindow(this._helpWindow);
    };

    const base$createHelpWindow = $.createHelpWindow;
    $.createHelpWindow = function () {
      swapWindowHelp(globalThis, MenuHelpWindow, () =>
        base$createHelpWindow.apply(this, arguments)
      );
    };

    const base$createCommandWindow = $.createCommandWindow;
    $.createCommandWindow = function () {
      base$createCommandWindow.apply(this, arguments);

      this._commandWindow.setHandler(
        LOAD_COMMAND_SYMBOL,
        this.commandLoad.bind(this)
      );
    };

    const base$createGoldWindow = $.createGoldWindow;
    $.createGoldWindow = function () {
      swapWindowGold(globalThis, MenuGoldWindow, () =>
        base$createGoldWindow.apply(this, arguments)
      );
    };

    $.goldWindowRect = function () {
      return swapWindowSelectable(globalThis, MenuGoldWindow, () => {
        const ww = this.mainCommandWidth();
        const wh = this.calcWindowHeight(4, true);
        const wx = this.isRightInputMode() ? Graphics.boxWidth - ww : 0;
        const wy = this.mainAreaBottom() - wh;
        return new Rectangle(wx, wy, ww, wh);
      });
    };

    $.commandLoad = function () {
      SceneManager.push(Scene_Load);
    };
  });
}
