// DarkPlasma_TinyMedal 3.1.0
// Copyright (c) 2020 DarkPlasma
// This software is released under the MIT license.
// http://opensource.org/licenses/mit-license.php

/**
 * 2023/05/15 3.1.0 typescript移行
 *                  報酬メッセージに交換に必要な枚数を含める機能追加
 * 2022/05/14 3.0.0 報酬獲得済みフラグのキーを変更（2.x以前とセーブデータ互換性がありません）
 * 2021/07/05 2.2.2 MZ 1.3.2に対応
 * 2021/06/22 2.2.1 サブフォルダからの読み込みに対応
 * 2021/02/13 2.2.0 報酬メッセージ複数行対応
 * 2020/10/10 2.1.2 リファクタ
 * 2020/09/29 2.1.1 プラグインコマンドに説明を追加
 * 2020/09/18 2.1.0 入手順を必要メダルの少ない順に変更
 * 2020/09/10 2.0.0 パラメータ名を変更
 *                  ウェイトなし歩行が遅れる不具合を修正
 * 2020/08/25 1.0.0 MZ版公開
 */

/*:
 * @plugindesc ちいさなメダルシステム
 * @author DarkPlasma
 * @license MIT
 *
 * @target MZ
 * @url https://github.com/elleonard/DarkPlasma-MZ-Plugins/tree/release
 *
 * @param medalItem
 * @desc メダルアイテム
 * @text メダルアイテム
 * @type item
 * @default 1
 *
 * @param medalCountVariable
 * @desc メダルの預かり数を記録する変数
 * @text メダル預かり数変数
 * @type variable
 * @default 1
 * @min 1
 *
 * @param medalUnit
 * @desc メダルカウントの単位
 * @text メダルの単位
 * @type string
 * @default 枚
 *
 * @param rewardItems
 * @desc 報酬アイテムの一覧
 * @text 報酬アイテム
 * @type struct<RewardItems>[]
 * @default []
 *
 * @param rewardWeapons
 * @desc 報酬武器の一覧
 * @text 報酬武器
 * @type struct<RewardWeapons>[]
 * @default []
 *
 * @param rewardArmors
 * @desc 報酬防具の一覧
 * @text 報酬防具
 * @type struct<RewardArmors>[]
 * @default []
 *
 * @param rewardMessages
 * @desc 報酬メッセージリスト
 * @text 報酬メッセージ
 * @type struct<RewardMessage>[]
 * @default ["{\"message\":\"メダルを${medalCount}個集めた。\n「${itemName}」を手に入れた！\n\",\"faceFile\":\"\",\"faceIndex\":\"0\"}"]
 *
 * @param migrateV2ToV3
 * @desc 2.xからバージョンアップする際のセーブデータ変換をするかどうか。詳細はヘルプ
 * @text 2.xからのセーブデータ変換
 * @type boolean
 * @default true
 *
 * @command gotoSceneMedal
 * @text ちいさなメダルシーンを開く
 * @desc 小さなメダルメニューが開き、報酬アイテム一覧が確認できます。
 *
 * @command processTinyMedal
 * @text ちいさなメダルを渡す
 * @desc ちいさなメダルシーンに移行せずにメダルを渡す処理だけします。
 *
 * @help
 * version: 3.1.0
 * DQシリーズのちいさなメダルシステム（累計式）を実現します。
 *
 * 同じメダル数で同じアイテム・武器・防具の報酬を
 * 設定した場合の動作は保証しません。
 * デフォルトにおける獲得済みフラグのキーは
 * メダル数とアイテム・武器・防具の種別とIDです。
 * 例: アイテムID:1のアイテムをメダル10枚で獲得した場合のキー
 * 10_1_1
 *
 * これをカスタマイズしたい場合、
 * 以下を上書きするプラグインを追加してください。
 * Data_TinyMedal_RewardItem.prototype.rewardKey: () => string
 *
 * ゲームのリリース後にキーとなる値を変更してしまうと、
 * セーブデータ互換性を破壊することに注意してください。
 *
 * 報酬メッセージの特殊な書式
 * ${itemName}: 報酬アイテム名
 * ${medalCount}: その報酬獲得に必要なメダル数
 *
 * 報酬メッセージの例:
 * よし！ それでは メダルを
 * ${medalCount}枚 集めた ほうびとして
 * ${itemName} を さずけようぞ！
 *
 * 2.xからのセーブデータ変換について
 * この機能をONにした場合、2.x以前を使用していたときのセーブデータを
 * ロードした際に3.x用に変換します。
 * ただし、報酬設定の順序や内容を変更すると正しくセーブデータが変換されず、
 * 意図しない挙動となることに注意してください。
 *
 * 2.xから3.x以降へバージョンアップする場合、
 * セーブデータ変換をOFFにすると、
 * 2.x以前を使用していたときのセーブデータにおいて、
 * 報酬獲得状況がリセットされます。
 */
/*~struct~RewardItems:
 * @param medalCount
 * @desc アイテムをもらうために必要なメダルの数
 * @text 必要メダル数
 * @type number
 * @default 1
 *
 * @param id
 * @desc もらえるアイテム
 * @text 報酬アイテム
 * @type item
 * @default 1
 */
/*~struct~RewardWeapons:
 * @param medalCount
 * @desc 武器をもらうために必要なメダルの数
 * @text 必要メダル数
 * @type number
 * @default 1
 *
 * @param id
 * @desc もらえる武器
 * @text 報酬武器
 * @type weapon
 * @default 1
 */
/*~struct~RewardArmors:
 * @param medalCount
 * @desc 防具をもらうために必要なメダルの数
 * @text 必要メダル数
 * @type number
 * @default 1
 *
 * @param id
 * @desc もらえる防具
 * @text 報酬防具
 * @type armor
 * @default 1
 */
/*~struct~RewardMessage:
 * @param message
 * @desc 報酬をもらった際のメッセージ。特殊な書式はヘルプ参照
 * @text 報酬メッセージ
 * @type multiline_string
 * @default メダルを${medalCount}個集めた。\n「${itemName}」を手に入れた！\n
 *
 * @param faceFile
 * @desc 報酬メッセージの顔グラファイル
 * @text 顔グラファイル
 * @type file
 * @dir img/faces
 *
 * @param faceIndex
 * @desc 報酬メッセージの顔グラ番号
 * @text 顔グラ番号
 * @type number
 * @default 0
 * @max 7
 */
(() => {
  'use strict';

  const pluginName = document.currentScript.src.replace(/^.*\/(.*).js$/, function () {
    return arguments[1];
  });

  const command_gotoSceneMedal = 'gotoSceneMedal';

  const command_processTinyMedal = 'processTinyMedal';

  const pluginParametersOf = (pluginName) => PluginManager.parameters(pluginName);

  const pluginParameters = pluginParametersOf(pluginName);

  const settings = {
    medalItem: Number(pluginParameters.medalItem || 1),
    medalCountVariable: Number(pluginParameters.medalCountVariable || 1),
    medalUnit: String(pluginParameters.medalUnit || `枚`),
    rewardItems: JSON.parse(pluginParameters.rewardItems || '[]').map((e) => {
      return ((parameter) => {
        const parsed = JSON.parse(parameter);
        return {
          medalCount: Number(parsed.medalCount || 1),
          id: Number(parsed.id || 1),
        };
      })(e || '{}');
    }),
    rewardWeapons: JSON.parse(pluginParameters.rewardWeapons || '[]').map((e) => {
      return ((parameter) => {
        const parsed = JSON.parse(parameter);
        return {
          medalCount: Number(parsed.medalCount || 1),
          id: Number(parsed.id || 1),
        };
      })(e || '{}');
    }),
    rewardArmors: JSON.parse(pluginParameters.rewardArmors || '[]').map((e) => {
      return ((parameter) => {
        const parsed = JSON.parse(parameter);
        return {
          medalCount: Number(parsed.medalCount || 1),
          id: Number(parsed.id || 1),
        };
      })(e || '{}');
    }),
    rewardMessages: JSON.parse(
      pluginParameters.rewardMessages ||
        '[{"message":"メダルを${medalCount}個集めた。\n「${itemName}」を手に入れた！\n","faceFile":"","faceIndex":"0"}]'
    ).map((e) => {
      return ((parameter) => {
        const parsed = JSON.parse(parameter);
        return {
          message: String(parsed.message || `メダルを${medalCount}個集めた。\n「${itemName}」を手に入れた！\n`),
          faceFile: String(parsed.faceFile || ``),
          faceIndex: Number(parsed.faceIndex || 0),
        };
      })(e || '{}');
    }),
    migrateV2ToV3: String(pluginParameters.migrateV2ToV3 || true) === 'true',
  };

  const ITEM_KIND = {
    ITEM: 1,
    WEAPON: 2,
    ARMOR: 3,
  };
  /**
   * @type {RewardMessage[]}
   */
  let reservedRewardMessages = [];
  class RewardItem {
    /**
     * @param {number} id ID
     * @param {number} kind アイテム種別
     * @param {number} medalCount 必要メダル数
     */
    constructor(id, kind, medalCount) {
      this._id = id;
      this._kind = kind;
      this._medalCount = medalCount;
    }
    /**
     * @param {object} object パース済みオブジェクト
     * @param {number} kind アイテム種別
     * @return {RewardItem}
     */
    static fromObject(object, kind) {
      return new RewardItem(object.id, kind, object.medalCount);
    }
    get medalCount() {
      return this._medalCount;
    }
    get itemData() {
      switch (this._kind) {
        case ITEM_KIND.ITEM:
          return $dataItems[this._id];
        case ITEM_KIND.WEAPON:
          return $dataWeapons[this._id];
        case ITEM_KIND.ARMOR:
          return $dataArmors[this._id];
        default:
          return null;
      }
    }
    /**
     * 報酬獲得フラグ用のキー
     * @return {string}
     */
    rewardKey() {
      return `${this._medalCount}_${this._kind}_${this._id}`;
    }
    /**
     * 報酬アイテムを入手する
     */
    complete() {
      const item = this.itemData;
      if (!item) {
        return;
      }
      $gameParty.gainItem(item, 1);
      $gameSystem.completeMedalReward(this.rewardKey());
      rewardMessages
        .map((message) => {
          return new RewardMessage(item.name, this._medalCount, message);
        })
        .forEach((message) => message.reserve());
    }
    /**
     * @return {boolean} 入手済みかどうか
     */
    completed() {
      return $gameSystem.isMedalRewardCompleted(this.rewardKey());
    }
  }
  class RewardMessage {
    constructor(itemName, requiredMedalCount, staticMessage) {
      this._itemName = itemName;
      this._requiredMedalCount = requiredMedalCount;
      this._staticMessage = staticMessage;
    }
    reserve() {
      reservedRewardMessages.push(this);
    }
    /**
     * アイテムを入手した際の報酬メッセージを表示する
     */
    show() {
      this._staticMessage.show(this._itemName, this._requiredMedalCount);
    }
  }
  /**
   * 報酬メッセージの固定部分
   */
  class RewardMessageStatic {
    constructor(message, faceFile, faceIndex) {
      this._message = message;
      this._faceFile = faceFile;
      this._faceIndex = faceIndex;
    }
    static fromObject(object) {
      return new RewardMessageStatic(object.message, object.faceFile, object.faceIndex);
    }
    /**
     * アイテムを入手した際の報酬メッセージを表示する
     */
    show(itemName, requiredMedalCount) {
      this.showFace();
      const message = this._message
        .replace(/\$\{itemName\}/gi, itemName)
        .replace(/\$\{medalCount\}/gi, `${requiredMedalCount}`);
      $gameMessage.add(message);
    }
    showFace() {
      if (this._faceFile) {
        $gameMessage.setFaceImage(this._faceFile, this._faceIndex);
      } else {
        $gameMessage.setFaceImage('', 0);
      }
    }
  }
  /**
   * @type {RewardItem[]}
   */
  const rewardItems = settings.rewardItems
    .map((rewardItem) => RewardItem.fromObject(rewardItem, ITEM_KIND.ITEM))
    .concat(settings.rewardWeapons.map((rewardWeapon) => RewardItem.fromObject(rewardWeapon, ITEM_KIND.WEAPON)))
    .concat(settings.rewardArmors.map((rewardArmor) => RewardItem.fromObject(rewardArmor, ITEM_KIND.ARMOR)));
  /**
   * @type {RewardMessageStatic[]}
   */
  const rewardMessages = settings.rewardMessages.map((rewardMessage) => RewardMessageStatic.fromObject(rewardMessage));
  PluginManager.registerCommand(pluginName, command_gotoSceneMedal, function () {
    SceneManager.push(Scene_TinyMedal);
  });
  PluginManager.registerCommand(pluginName, command_processTinyMedal, function () {
    $gameSystem.processTinyMedal();
    if (!$gameMessage.isBusy() && reservedRewardMessages.length > 0) {
      const reservedMessage = reservedRewardMessages.shift();
      reservedMessage?.show();
    }
  });
  /**
   * 2.xのデータから3.xのデータに変換する
   * @param {boolean[]} v2Array
   * @return {{[key: string]: boolean}}
   */
  function convertV2DataToV3(v2Array) {
    const result = {};
    if (v2Array.length === 0) {
      return result;
    }
    /**
     * 元のセーブデータは先頭がダミーデータのため捨てる
     */
    v2Array.slice(1).forEach((isCompleted, oldRewardId) => {
      const reward = rewardItems[oldRewardId];
      if (reward) {
        result[reward.rewardKey()] = isCompleted;
      }
    });
    return result;
  }
  /**
   * @param {Game_System.prototype} gameSystem
   */
  function Game_System_TinyMedalMixIn(gameSystem) {
    const _initialize = gameSystem.initialize;
    gameSystem.initialize = function () {
      _initialize.call(this);
      this.initializeMedalRewardsCompletion();
    };
    /**
     * メダル報酬獲得フラグの初期化
     */
    gameSystem.initializeMedalRewardsCompletion = function () {
      this._medalRewardsCompletion = {};
    };
    /**
     * メダル報酬獲得フラグの更新
     * @param {string} rewardKey 報酬ID
     */
    gameSystem.completeMedalReward = function (rewardKey) {
      this._medalRewardsCompletion[rewardKey] = true;
    };
    gameSystem.isMedalRewardCompleted = function (rewardKey) {
      return this._medalRewardsCompletion[rewardKey] || false;
    };
    const _onAfterLoad = gameSystem.onAfterLoad;
    gameSystem.onAfterLoad = function () {
      _onAfterLoad.call(this);
      if (!this._medalRewardsCompletion) {
        this.initializeMedalRewardsCompletion();
      }
      /**
       * 2.xからのバージョンアップ用
       */
      if (settings.migrateV2ToV3 && Array.isArray(this._medalRewardsCompletion)) {
        this._medalRewardsCompletion = convertV2DataToV3(this._medalRewardsCompletion);
      }
    };
    gameSystem.processTinyMedal = function () {
      const beforeCount = $gameVariables.value(settings.medalCountVariable);
      $gameVariables.setValue(settings.medalCountVariable, beforeCount + $gameParty.numMedalItems());
      $gameParty.loseAllMedalItem();
      const afterCount = $gameVariables.value(settings.medalCountVariable);
      // 報酬アイテム入手
      const gainRewards = rewardItems
        .sort((a, b) => a.medalCount - b.medalCount)
        .filter((rewardItem) => !rewardItem.completed() && afterCount >= rewardItem.medalCount);
      gainRewards.forEach((rewardItem) => rewardItem.complete());
    };
  }
  Game_System_TinyMedalMixIn(Game_System.prototype);
  /**
   * @param {Game_Party.prototype} gameParty
   */
  function Game_Party_TinyMedalMixIn(gameParty) {
    /**
     * @return {number} メダルアイテムの数
     */
    gameParty.numMedalItems = function () {
      return this.numItems($dataItems[settings.medalItem]);
    };
    /**
     * @return {boolean} メダルアイテムを持っているかどうか
     */
    gameParty.hasMedalItem = function () {
      return this.hasItem($dataItems[settings.medalItem]);
    };
    /**
     * 所持しているメダルアイテムをすべて失う
     */
    gameParty.loseAllMedalItem = function () {
      this.loseItem($dataItems[settings.medalItem], this.numMedalItems(), false);
    };
  }
  Game_Party_TinyMedalMixIn(Game_Party.prototype);
  /**
   * @param {Game_Interpreter.prototype} gameInterpreter
   */
  function Game_Interpreter_TinyMedalMixIn(gameInterpreter) {
    const _executeCommand = gameInterpreter.executeCommand;
    gameInterpreter.executeCommand = function () {
      /**
       * 報酬メッセージがあればそれを先に処理する
       */
      if (reservedRewardMessages.length > 0) {
        this.processReservedRewardMessages();
        return true;
      }
      return _executeCommand.call(this);
    };
    /**
     * 報酬メッセージを表示する
     */
    gameInterpreter.processReservedRewardMessages = function () {
      if (!$gameMessage.isBusy() && reservedRewardMessages.length > 0) {
        const reservedMessage = reservedRewardMessages.shift();
        reservedMessage?.show();
      }
    };
  }
  Game_Interpreter_TinyMedalMixIn(Game_Interpreter.prototype);
  class Scene_TinyMedal extends Scene_Base {
    initialize() {
      Scene_Base.prototype.initialize.call(this);
      this._rewardsAutoGained = false;
    }
    create() {
      Scene_Base.prototype.create.call(this);
      this.createBackground();
      if (!this._rewardsAutoGained) {
        this.createWindowLayer();
        this.createMedalWindow();
      }
    }
    createBackground() {
      this._backgroundSprite = new Sprite();
      this._backgroundSprite.bitmap = SceneManager.backgroundBitmap();
      this.addChild(this._backgroundSprite);
    }
    createMedalWindow() {
      this._helpWindow = new Window_Help(this.helpWindowRect());
      this._menuWindow = new Window_MedalMenu(this.medalMenuWindowRect());
      this._menuWindow.setHandler('pushMedal', this.commandPushMedal.bind(this));
      this._menuWindow.setHandler('showRewards', this.commandShowRewards.bind(this));
      this._menuWindow.setHandler('cancel', this.popScene.bind(this));
      this._rewardsWindow = new Window_MedalRewardList(this.medalRewardListWindowRect());
      this._rewardsWindow.setHandler('cancel', this.onRewardsListCancel.bind(this));
      this._rewardsWindow.setHelpWindow(this._helpWindow);
      this._countWindow = new Window_MedalCount(this.medalCountWindowRect());
      this.addChild(this._helpWindow);
      this.addChild(this._menuWindow);
      this.addChild(this._rewardsWindow);
      this.addChild(this._countWindow);
    }
    helpWindowRect() {
      return new Rectangle(0, 0, Graphics.boxWidth, this.calcWindowHeight(2, false));
    }
    medalMenuWindowRect() {
      return new Rectangle(0, this._helpWindow.height, 240, this.calcWindowHeight(3, true));
    }
    medalRewardListWindowRect() {
      const x = this._menuWindow.width;
      const y = this._helpWindow.height;
      return new Rectangle(x, y, Graphics.boxWidth - x, Graphics.boxHeight - y);
    }
    medalCountWindowRect() {
      const height = this.calcWindowHeight(1, true);
      return new Rectangle(0, Graphics.boxHeight - height, 240, height);
    }
    /**
     * メダルを預かってもらう
     */
    processMedal() {
      $gameSystem.processTinyMedal();
    }
    /**
     * 所持しているメダルを預かってもらう
     */
    commandPushMedal() {
      this.processMedal();
      this.popScene();
    }
    commandShowRewards() {
      this._menuWindow.deselect();
      this._rewardsWindow.activate();
    }
    onRewardsListCancel() {
      this._rewardsWindow.deselect();
      this._menuWindow.activate();
      this._menuWindow.selectSymbol('showRewards');
    }
  }
  class Window_MedalMenu extends Window_Command {
    makeCommandList() {
      this.addCommand('メダルを預ける', 'pushMedal', $gameParty.hasMedalItem());
      this.addCommand('報酬を確認する', 'showRewards');
      this.addCommand('閉じる', 'cancel');
    }
  }
  function Window_ItemListLikeMixIn(windowClass) {
    return class extends windowClass {
      maxCols() {
        return 2;
      }
      colSpacing() {
        return 16;
      }
      maxItems() {
        return this._data ? this._data.length : 1;
      }
      item() {
        return this.itemAt(this.index());
      }
      itemAt(index) {
        return this._data && index >= 0 ? this._data[index] : null;
      }
      isCurrentItemEnabled() {
        return this.isEnabled(this.item());
      }
      isEnabled(item) {
        return false;
      }
      includes(item) {
        return false;
      }
      makeItemList() {}
      drawItem(index) {
        const item = this.itemAt(index);
        if (item) {
          const numberWidth = this.numberWidth();
          const rect = this.itemLineRect(index);
          this.changePaintOpacity(this.isEnabled(item));
          this.drawItemName(this.drawableItem(item), rect.x, rect.y, rect.width - numberWidth);
          this.drawItemNumber(item, rect.x, rect.y, rect.width);
          this.changePaintOpacity(true);
        }
      }
      drawableItem(item) {
        return null;
      }
      numberWidth() {
        return this.textWidth('000');
      }
      needsNumber() {
        return false;
      }
      drawItemNumber(item, x, y, width) {
        if (this.needsNumber()) {
          this.drawText(':', x, y, width - this.textWidth('00'), 'right');
          this.drawText(`${this.number(item)}`, x, y, width, 'right');
        }
      }
      number(item) {
        return 0;
      }
      refresh() {
        this.makeItemList();
        super.refresh();
      }
    };
  }
  class Window_MedalRewardList extends Window_ItemListLikeMixIn(Window_Selectable) {
    constructor(rect) {
      super(rect);
      this.initialize(rect);
      this.refresh();
    }
    maxCols() {
      return 1;
    }
    isEnabled(item) {
      return !item.completed();
    }
    makeItemList() {
      this._data = rewardItems;
    }
    includes(item) {
      return true;
    }
    drawableItem(item) {
      return item.itemData;
    }
    needsNumber() {
      return true;
    }
    number(item) {
      return item.medalCount;
    }
    updateHelp() {
      this.setHelpWindowItem(this.item()?.itemData || null);
    }
  }
  class Window_MedalCount extends Window_Gold {
    value() {
      return $gameParty.numMedalItems();
    }
    currencyUnit() {
      return settings.medalUnit;
    }
  }
  globalThis.Data_TinyMedal_RewardItem = RewardItem;
})();
