//=============================================================================
// AutomaticState.js
// ----------------------------------------------------------------------------
// (C) 2016 Triacontane
// This software is released under the MIT License.
// http://opensource.org/licenses/mit-license.php
// ----------------------------------------------------------------------------
// Version
// 1.4.1 2019/01/27 AS武器装備およびAS防具装備のタグが敵にも適用される問題を修正
// 1.4.0 2017/09/13 自動ステート対象をアクター、敵キャラIDで限定するときに複数指定できる機能を追加
// 1.3.1 2017/08/06 メモリ上にロードされていないアクターの自動ステートチェックが実行されるとエラーになる現象を修正
//                  処理の軽量化
// 1.3.0 2017/07/21 パーティの並び順で自動ステートを付与する機能を追加
// 1.2.5 2017/05/27 競合の可能性のある記述（Objectクラスへのプロパティ追加）をリファクタリング
// 1.2.4 2017/05/27 全回復の直後に自動ステートが解除されてしまう問題を修正
// 1.2.3 2017/02/16 1.2.0以降、下限MPの設定が無効になっていた問題を修正
// 1.2.2 2017/02/07 端末依存の記述を削除
// 1.2.1 2017/01/17 1.2.0でスイッチの項目が正しく機能しなかった問題を修正
// 1.2.0 2017/01/14 ステート自動付与の条件に計算式を適用できる機能を追加
//                  処理の軽量化
// 1.1.2 2016/06/03 スペルミス修正
// 1.1.1 2016/05/23 条件式で割合を算出する際に端数を切り捨てるよう修正
// 1.1.0 2016/05/22 複数の条件を適用できるよう修正
// 1.0.2 2016/04/30 アクターと敵キャラ限定のオートステートが一部正しく機能していなかった問題を修正
// 1.0.1 2016/02/14 オートステートの追加時にメッセージを表示する仕様を追加
// 1.0.0 2016/02/08 初版
// ----------------------------------------------------------------------------
// [Blog]   : https://triacontane.blogspot.jp/
// [Twitter]: https://twitter.com/triacontane/
// [GitHub] : https://github.com/triacontane/
//=============================================================================

/*:
 * @plugindesc ステート自動付与プラグイン
 * @author トリアコンタン
 *
 * @help AutomaticState.js
 *
 * 条件を満たしている間、指定したステートを付与します。
 * ステートのメモ欄に以下の書式で条件を入力してください。
 *
 * メモ欄書式（ステートIDには制御文字を利用できます）
 *
 * 1. 自動付与ステートの条件
 * <AS上限HP:（HPの割合[百分率]）>
 *     指定したHP(割合)を上回っている（>=）間、対象ステートを付与する。
 * <AS下限HP:（HPの割合[百分率]）>
 *     指定したHP(割合)を下回っている（<=）間、対象ステートを付与する。
 * <AS上限MP:（MPの割合[百分率]）>
 *     指定したMP(割合)を上回っている（>=）間、対象ステートを付与する。
 * <AS下限MP:（MPの割合[百分率]）>
 *     指定したMP(割合)を下回っている（<=）間、対象ステートを付与する。
 * <AS上限TP:（TPの割合[百分率]）>
 *     指定したMP(割合)を上回っている（>=）間、対象ステートを付与する。
 * <AS下限TP:（TPの割合[百分率]）>
 *     指定したMP(割合)を下回っている（<=）間、対象ステートを付与する。
 * <AS武器装備:（武器ID）>
 *     指定した武器を装備している間、対象ステートを付与する。
 * <AS防具装備:（防具ID）>
 *     指定した武器を装備している間、対象ステートを付与する。
 * <ASスイッチ:（スイッチID）>
 *     指定したスイッチがONになっている間、対象ステートを付与する。
 * <AS計算式:（JS計算式）>
 *     指定したJavaScript計算式がtrueの間、対象ステートを付与する。
 * <AS並び順:（インデックス）>
 *     並び順(1～)が指定した値と一致する間、対象ステートを付与する。
 *
 * 計算式中で不等号を使いたい場合、以下のように記述してください。
 * < → &lt;
 * > → &gt;
 * 例：<AS計算式:\v[2] &gt; 1> // 変数[2]が1より大きい場合
 *
 * 2. 自動付与ステートの対象
 * <ASアクター:（アクターID）>
 *     ステート自動付与の対象を指定したアクターのみに設定する。
 *     IDの指定がない場合、全てのアクターに有効になる。
 *     複数のアクターを指定したい場合、数値をカンマで区切る。
 * 例：<ASアクター:1,2,3>
 *
 * <AS敵キャラ:（敵キャラID）>
 *     ステート自動付与の対象を指定した敵キャラのみに設定する。
 *     IDの指定がない場合、全ての敵キャラに有効になる。
 *     複数の敵キャラを指定したい場合、数値をカンマで区切る。
 * 例：<AS敵キャラ:1,2,3>
 *
 * 複数の条件が指定された場合は、全ての条件を満たした場合のみ
 * ステートが付与されます。
 *
 * スクリプト
 * 自動付与ステートが有効になったときに所定のメッセージを表示する場合
 * $gameSystem.automaticStateAddMessage = true;
 *
 * 自動付与ステートが有効になったときに所定のメッセージを表示しない場合
 * $gameSystem.automaticStateAddMessage = false;
 *
 * 利用規約：
 *  作者に無断で改変、再配布が可能で、利用形態（商用、18禁利用等）
 *  についても制限はありません。
 *  このプラグインはもうあなたのものです。
 */

(function () {
    'use strict';
    var metaTagPrefix = 'AS';

    var getArgNumber = function (arg, min, max) {
        if (arguments.length < 2) min = -Infinity;
        if (arguments.length < 3) max = Infinity;
        return (parseInt(convertEscapeCharacters(arg)) || 0).clamp(min, max);
    };

    var convertEscapeCharacters = function(text) {
        if (text == null || text === true) text = '';
        text = text.replace(/&gt;?/gi, '>');
        text = text.replace(/&lt;?/gi, '<');
        text = text.replace(/\\/g, '\x1b');
        text = text.replace(/\x1b\x1b/g, '\\');
        text = text.replace(/\x1bV\[(\d+)\]/gi, function() {
            return $gameVariables.value(parseInt(arguments[1]));
        }.bind(this));
        text = text.replace(/\x1bV\[(\d+)\]/gi, function() {
            return $gameVariables.value(parseInt(arguments[1]));
        }.bind(this));
        text = text.replace(/\x1bN\[(\d+)\]/gi, function() {
            var actor = parseInt(arguments[1]) >= 1 ? $gameActors.actor(parseInt(arguments[1])) : null;
            return actor ? actor.name() : '';
        }.bind(this));
        text = text.replace(/\x1bP\[(\d+)\]/gi, function() {
            var actor = parseInt(arguments[1]) >= 1 ? $gameParty.members()[parseInt(arguments[1]) - 1] : null;
            return actor ? actor.name() : '';
        }.bind(this));
        text = text.replace(/\x1bG/gi, TextManager.currencyUnit);
        return text;
    };

    var isEmpty = function(that) {
        return Object.keys(that).length <= 0;
    };

    var iterate = function(that, handler) {
        Object.keys(that).forEach(function(key, index) {
            handler.call(that, key, that[key], index);
        });
    };

    //=============================================================================
    // Game_System
    //  自動付与ステートのメッセージ表示フラグを定義します。
    //=============================================================================
    var _Game_System_initialize = Game_System.prototype.initialize;
    Game_System.prototype.initialize = function() {
        _Game_System_initialize.apply(this, arguments);
        this.automaticStateAddMessage = null;
    };

    //=============================================================================
    // Game_BattlerBase
    //  自動付与ステートの更新処理を定義します。
    //=============================================================================
    Game_BattlerBase.prototype.updateAutomaticState = function() {
        DataManager.iterateAutomaticState(function(state) {
            if (!state || isEmpty(state.meta)) return;
            var stateId = state.id;
            var result = this.isAutomaticValid(state, false);
            if (result) {
                if (!this.isStateAffected(stateId) && this.isStateAddable(stateId)) {
                    this.addState(stateId);
                    if ($gameSystem.automaticStateAddMessage) this.showAddedStates();
                    this._result.deleteAddedStates(stateId);
                }
            } else if (result === false && this.isStateAffected(stateId)) {
                this.removeState(stateId);
                this._result.deleteRemovedStates(stateId);
            }
        }.bind(this));
    };

    Game_BattlerBase.prototype.showAddedStates = function() {
    };

    Game_BattlerBase.prototype.isAutomaticValid = function(state, result) {
        this._automaticTargetState = state;
        if (!this.isAutoStateBattler()) {
            return null;
        }
        var switchId = this.getStateMetaNumber(0, 1);
        if (switchId !== null) {
            if ($gameSwitches.value(switchId)) {
                result = true;
            } else {
                return false;
            }
        }
        var formula = this.getStateMetaString(1);
        if (formula !== null) {
            if (eval(formula)) {
                result = true;
            } else {
                return false;
            }
        }
        var upperLimitHp = this.getStateMetaNumber(2, 0, 100);
        if (upperLimitHp !== null) {
            if (Math.floor(this.hpRate() * 100) >= upperLimitHp) {
                result = true;
            } else {
                return false;
            }
        }
        var lowerLimitHp = this.getStateMetaNumber(3, 0, 100);
        if (lowerLimitHp !== null) {
            if (Math.floor(this.hpRate() * 100) <= lowerLimitHp) {
                result = true;
            } else {
                return false;
            }
        }
        var upperLimitMp = this.getStateMetaNumber(4, 0, 100);
        if (upperLimitMp !== null) {
            if (Math.floor(this.mpRate() * 100) >= upperLimitMp) {
                result = true;
            } else {
                return false;
            }
        }
        var lowerLimitMp = this.getStateMetaNumber(5, 0, 100);
        if (lowerLimitMp !== null) {
            if (Math.floor(this.mpRate() * 100) <= lowerLimitMp) {
                result = true;
            } else {
                return false;
            }
        }
        var upperLimitTp = this.getStateMetaNumber(6, 0, 100);
        if (upperLimitTp !== null) {
            if (Math.floor(this.tpRate() * 100) >= upperLimitTp) {
                result = true;
            } else {
                return false;
            }
        }
        var lowerLimitTp = this.getStateMetaNumber(7, 0, 100);
        if (lowerLimitTp !== null) {
            if (Math.floor(this.tpRate() * 100) <= lowerLimitTp) {
                result = true;
            } else {
                return false;
            }
        }
        var unitIndex = this.getStateMetaNumber(12, 0);
        if (unitIndex !== null) {
            if (this.index() === unitIndex - 1) {
                result = true;
            } else {
                return false;
            }
        }
        return result;
    };

    Game_BattlerBase.prototype.index = function() {
        return 0;
    };

    Game_BattlerBase.prototype.getStateMetaNumber = function(tagIndex, min, max) {
        var value = this._automaticTargetState.meta[DataManager.getAutomaticTagName(tagIndex)];
        if (arguments.length < 2) min = -Infinity;
        if (arguments.length < 3) max = Infinity;
        return value !== undefined ? getArgNumber(value, min, max) : null;
    };

    Game_BattlerBase.prototype.getStateMetaString = function(tagIndex) {
        var value = this._automaticTargetState.meta[DataManager.getAutomaticTagName(tagIndex)];
        return value !== undefined ? convertEscapeCharacters(value) : null;
    };

    Game_BattlerBase.prototype.isAutoStateBattler = function() {
        if (this.isStateMetaInfo(this.getOppositeBattlerCheckTagIndex())) {
            return false;
        }
        var battlerIdValue = this.getStateMetaString(this.getBattlerCheckTagIndex());
        if (!battlerIdValue) {
            return true;
        }
        var batterId = this.getBattlerId();
        return battlerIdValue.split(',').some(function(stateTargetIdText) {
            return batterId === parseInt(stateTargetIdText);
        });
    };

    Game_BattlerBase.prototype.isStateMetaInfo = function(tagIndex) {
        return this._automaticTargetState.meta.hasOwnProperty(DataManager.getAutomaticTagName(tagIndex));
    };

    var _Game_BattlerBase_setHp = Game_BattlerBase.prototype.setHp;
    Game_BattlerBase.prototype.setHp = function(hp) {
        _Game_BattlerBase_setHp.apply(this, arguments);
        this.updateAutomaticState();
    };

    var _Game_BattlerBase_setMp = Game_BattlerBase.prototype.setMp;
    Game_BattlerBase.prototype.setMp = function(mp) {
        _Game_BattlerBase_setMp.apply(this, arguments);
        this.updateAutomaticState();
    };

    var _Game_BattlerBase_setTp = Game_BattlerBase.prototype.setTp;
    Game_BattlerBase.prototype.setTp = function(tp) {
        _Game_BattlerBase_setTp.apply(this, arguments);
        this.updateAutomaticState();
    };

    var _Game_BattlerBase_recoverAll = Game_BattlerBase.prototype.recoverAll;
    Game_BattlerBase.prototype.recoverAll = function() {
        _Game_BattlerBase_recoverAll.apply(this, arguments);
        this.updateAutomaticState();
    };

    Game_BattlerBase.prototype.getBattlerId = function() {
        return 0;
    };

    Game_BattlerBase.prototype.getBattlerCheckTagIndex = function() {
        return 0;
    };

    Game_BattlerBase.prototype.getOppositeBattlerCheckTagIndex = function() {
        return 0;
    };

    //=============================================================================
    // Game_Actors
    //  データが存在するかどうかを返します。
    //=============================================================================
    Game_Actors.prototype.isExistData = function(actorId) {
        return !!this._data[actorId];
    };

    //=============================================================================
    // Game_Actor
    //  自動付与ステートの更新処理を定義します。
    //=============================================================================
    Game_Actor.prototype.updateAutomaticState = function() {
        if (!$gameActors.isExistData(this._actorId)) return;
        Game_BattlerBase.prototype.updateAutomaticState.call(this);
    };

    Game_Actor.prototype.isAutomaticValid = function(state) {
        this._automaticTargetState = state;
        var result = null;
        var weaponId = this.getStateMetaString(10, 1);
        if (weaponId !== null) {
            if (this.hasWeapon($dataWeapons[weaponId])) {
                result = true;
            } else {
                return false;
            }
        }
        var armorId = this.getStateMetaNumber(11, 1);
        if (armorId !== null) {
            if (this.hasArmor($dataArmors[armorId])) {
                result = true;
            } else {
                return false;
            }
        }
        return Game_BattlerBase.prototype.isAutomaticValid.call(this, state, result);
    };

    var _Game_Actor_changeEquip = Game_Actor.prototype.changeEquip;
    Game_Actor.prototype.changeEquip = function(slotId, item) {
        _Game_Actor_changeEquip.apply(this, arguments);
        this.updateAutomaticState();
    };

    Game_Actor.prototype.getBattlerId = function() {
        return this._actorId;
    };

    Game_Actor.prototype.getBattlerCheckTagIndex = function() {
        return 8;
    };

    Game_Actor.prototype.getOppositeBattlerCheckTagIndex = function() {
        return Game_Enemy.prototype.getBattlerCheckTagIndex.call(this);
    };

    //=============================================================================
    // Game_Enemy
    //  自動付与ステートの更新処理を定義します。
    //=============================================================================
    Game_Enemy.prototype.getBattlerId = function() {
        return this._enemyId;
    };

    Game_Enemy.prototype.getBattlerCheckTagIndex = function() {
        return 9;
    };

    Game_Enemy.prototype.getOppositeBattlerCheckTagIndex = function() {
        return Game_Actor.prototype.getBattlerCheckTagIndex.call(this);
    };

    //=============================================================================
    // Game_Party
    //  全てのアクターの自動付与ステートを更新します。
    //=============================================================================
    Game_Party.prototype.requestUpdateAutomaticState = function() {
        this._requestUpdateAutomaticState = true;
    };

    Game_Party.prototype.updateAutomaticState = function() {
        this.members().forEach(function(actor) {
            actor.updateAutomaticState();
        });
        this._lastUpdateFrameAutomaticState = Graphics.frameCount;
    };

    Game_Party.prototype.updateAutomaticStateIfRequested = function() {
        var coolTime = (this._lastUpdateFrameAutomaticState + 10 > Graphics.frameCount);
        if (this._requestUpdateAutomaticState && !coolTime) {
            this.updateAutomaticState();
            this._requestUpdateAutomaticState = false;
        }
    };

    var _Game_Party_swapOrder = Game_Party.prototype.swapOrder;
    Game_Party.prototype.swapOrder = function(index1, index2) {
        _Game_Party_swapOrder.apply(this, arguments);
        this.updateAutomaticState();
    };

    //=============================================================================
    // Game_Map
    //  場所移動時に自動付与ステートを更新します。
    //=============================================================================
    var _Game_Map_refreshIfNeeded = Game_Map.prototype.refreshIfNeeded;
    Game_Map.prototype.refreshIfNeeded = function() {
        _Game_Map_refreshIfNeeded.apply(this, arguments);
        $gameParty.updateAutomaticStateIfRequested();
    };

    var _Game_Map_setup = Game_Map.prototype.setup;
    Game_Map.prototype.setup = function(mapId) {
        _Game_Map_setup.apply(this, arguments);
        $gameParty.updateAutomaticState();
    };

    var _Game_Map_refresh = Game_Map.prototype.refresh;
    Game_Map.prototype.refresh = function() {
        _Game_Map_refresh.apply(this, arguments);
        $gameParty.requestUpdateAutomaticState();
    };

    //=============================================================================
    // Game_ActionResult
    //  ステート付与、解除時のメッセージを抑制します。
    //=============================================================================
    Game_ActionResult.prototype.deleteRemovedStates = function(stateId) {
        iterate(this.removedStates, function(key, value, index) {
            if (value === stateId) this.removedStates.splice(index, 1);
        }.bind(this));
    };

    Game_ActionResult.prototype.deleteAddedStates = function(stateId) {
        iterate(this.addedStates, function(key, value, index) {
            if (value === stateId) this.addedStates.splice(index, 1);
        }.bind(this));
    };

    //=============================================================================
    // DataManager
    //  データベースのロード時に自動付与ステートのみを抽出します。
    //=============================================================================
    DataManager._automaticTagNames = [
        'スイッチ',
        '計算式',
        '上限HP',
        '下限HP',
        '上限MP',
        '下限MP',
        '上限TP',
        '下限TP',
        'アクター',
        '敵キャラ',
        '武器装備',
        '防具装備',
        '並び順'
    ];

    var _DataManager_onLoad = DataManager.onLoad;
    DataManager.onLoad = function(object) {
        _DataManager_onLoad.apply(this, arguments);
        if (object === $dataStates) {
            this.setupAutomaticState();
        }
    };

    DataManager.setupAutomaticState = function() {
        this._automaticStates = $dataStates.filter(function(state) {
            return this._automaticTagNames.some(function(tagName) {
                return state && !!state.meta[metaTagPrefix + tagName];
            });
        }.bind(this));
    };

    DataManager.iterateAutomaticState = function(callBackFunc) {
        this._automaticStates.forEach(callBackFunc);
    };

    DataManager.getAutomaticTagName = function(index) {
        return metaTagPrefix + this._automaticTagNames[index];
    };
})();