//=============================================================================
// RPG Maker MZ - EventHook
//
// Copyright 2024 Panzer-IV. All rights reserved.
// This source code or any portion thereof must not be
// reproduced or used without licensed in any manner whatsoever.
//=============================================================================

/*:
 * @target MZ
 * @plugindesc イベントフック
 * @author 四号戦車
 * @base PluginCommonBase
 * @orderAfter PluginCommonBase
 * @orderAfter DebugSwitch
 *
 * @param hooks
 * @text イベントフック
 * @desc 監視する各イベントと実行するコモンイベントの設定です。
 * @type struct<WatchHook>[]
 * @default []
 *
 * @param stateHooks
 * @text ステートフック
 * @desc 監視する各ステートと実行するコモンイベントの設定です。
 * @type struct<WatchStates>[]
 * @default []
 *
 * @param battleHooks
 * @text 戦闘フック
 * @desc 監視する各戦闘イベントと実行するコモンイベントの設定です。
 * @type struct<WatchBattleHook>[]
 * @default []
 *
 * @help イベントフックプラグイン
 *
 * EventHook
 * Version: 0.0.3
 *
 * ゲーム中の各状態を監視して、事前に登録したコモンイベントを実行するプラグインです。
 * 監視できるイベントは以下の通りです。
 *
 * ・新規ゲーム開始
 * ・セーブデータのロード直後
 * ・変数が変更された
 * ・変数が指定の値になった
 * ・変数が指定の値ではなくなった
 * ・マップに移動した
 * ・指定のステートが追加された
 * ・指定のステートが削除された
 * ・ステートが全て削除された
 * ・指定キャラのHPが一定以上/以下になった
 * ・指定キャラに任意のダメージが入った
 *
 * 使い方：
 * 　プラグイン管理から[実行するタイミング種別]を選択して、[コモンイベントID]を指定してください。
 * 　そのタイミングでコモンイベントが１度だけ実行されます。
 * 　また、選択したタイミング種別によって追加の設定を行ってください。
 *
 * 　新規ゲーム開始：
 * 　　その他の設定は不要です。イベントが実行されます。
 *     新規ゲーム開始直後、最初のマップの呼び出し後に実行される事に注意して下さい。
 *     新規ゲーム開始時、別のシーンを実行するプラグイン等を使用した場合はプレイヤーがマップに配置されるまで実行されません。
 *
 * 　セーブデータのロード直後：
 * 　　その他の設定は不要です。イベントが実行されます。
 *
 * 　変数が変更された：
 * 　　[監視対象の変数]に監視対象の変数のIDを指定してください。
 * 　　指定されたIDの変数が変更された場合に呼び出されます。
 * 　　代入されても値が変わらない(変数に代入された値が代入前と同じ)場合は無視します。
 *
 * 　変数が指定の値になった
 * 　変数が指定の値ではなくなった：
 * 　　[監視対象の変数]に監視対象の変数のIDを指定してください。
 * 　　また、[指定の値]にタイミングとして判定する値を入れるか、[指定の値の変数]に別の変数のIDを設定しておいてください。
 * 　　対象の変数が指定の値になるか、なくなった時に設定されたイベントを実行します。
 * 　　これは変更のタイミングで何度でも呼び出されます。
 *
 * 　マップに移動した：
 * 　　[指定の値]に指定するマップのIDもしくは名前を入れるか、
 * 　　[指定の値の変数]にマップのIDもしくは名前を格納した変数のIDを設定しておいてください。
 *     数字が格納されていた場合マップIDだと判定します。
 * 　　マップに突入した際に何度でも呼び出されます。
 *
 *     [指定の値]もしくは[指定の値の変数]にマップ名を指定した場合、同じマップ名のマップが複数ある場合
 * 　　その全てのマップで実行されてしまうことに注意してください。
 *
 *   指定のステートが追加された
 *   指定のステートが削除された
 *   ステートが全て削除された：
 *     [対象アクター]もしくは[対象エネミー]と[監視対象ステート]を指定して下さい。
 *     指定しない場合全てのアクターもしくはエネミーが対象になります。
 *     また、戦闘中以外でのステート付与では発動しないようにする場合[戦闘中のみ]をONに設定して下さい。
 *
 */

/*~struct~WatchHook:
 *
 * @param watchType
 * @text 実行するタイミング種別
 * @desc 実行するタイミングの種別を指定します。
 * @type select
 * @default new_game
 * @option 新規ゲーム開始
 * @value new_game
 * @option セーブデータのロード直後
 * @value data_loaded
 * @option 変数の変更全て
 * @value variable_change_any
 * @option 変数が指定の値になったとき
 * @value variable_change_match
 * @option 変数が指定の値ではなくなったとき
 * @value variable_change_unmatch
 * @option マップに移動した
 * @value map_transfer
 *
 * @param target_variable
 * @text 監視対象の変数
 * @desc 監視する対象の変数を指定します。
 * @type variable
 * @default 0
 *
 * @param value
 * @text 指定の値
 * @desc タイミング種別で値を指定している場合に指定してください。
 *       「変数が指定の値になったとき」「変数が指定の値ではなくなったとき」「マップに移動した」以外の時は無視されます。
 * @type string
 *
 * @param value_variable
 * @text 指定の値の変数
 * @desc タイミング種別で変数を指定している場合に指定してください。
 *       「変数が指定の値になったとき」「変数が指定の値ではなくなったとき」「マップに移動した」以外の時は無視されます。
 * @type variable
 * @default 0
 *
 * @param commonEventId
 * @text コモンイベントID
 * @desc 指定の状態になった時に呼び出すコモンイベントのIDです。
 * @type common_event
 * @default 0
 *
 * @param comment
 * @text 注釈
 * @desc プラグイン側では使用しません。
 *       メモ欄としてお使いください。
 * @type multiline_string
 * @default
 *
 */

/*~struct~WatchStates:
 *
 * @param watchType
 * @text 実行するタイミング種別
 * @desc 実行するタイミングの種別を指定します。
 * @type select
 * @default add_state
 * @option 指定ステートが追加された
 * @value add_state
 * @option 指定ステートが除去された
 * @value remove_state
 * @option 全てのステートが除去された
 * @value clear_state
 *
 * @param actors
 * @text 対象アクター
 * @desc ステートの監視対象にするアクターです。
 *       複数指定可能です。
 * @type actor[]
 * @default []
 *
 * @param enemies
 * @text 対象エネミー
 * @desc ステートの監視対象にするエネミーです。
 *       複数指定可能です。
 * @type enemy[]
 * @default []
 *
 * @param state
 * @text 対象のステート
 * @desc 監視対象のステートです
 * @type state
 * @default 0
 *
 * @param onlyBattle
 * @text 戦闘中のみ
 * @desc 戦闘中のステート変更のみトリガにします。
 * @type boolean
 * @default false
 *
 * @param commonEventId
 * @text コモンイベントID
 * @desc 指定の状態になった時に呼び出すコモンイベントのIDです。
 * @type common_event
 * @default 0
 *
 * @param comment
 * @text 注釈
 * @desc プラグイン側では使用しません。
 *       メモ欄としてお使いください。
 * @type multiline_string
 * @default
 *
 */
/*~struct~WatchBattleHook:
 *
 * @param watchEvent
 * @text 実行するタイミング種別
 * @desc 実行するタイミングの種別を指定します。
 * @type select
 * @default parameter
 * @option 指定キャラの指定パラメータが指定の状態になった
 * @value parameter
 * @option 指定キャラに指定のダメージが入った
 * @value damage
 *
 * @param actors
 * @text 対象アクター
 * @desc ステートの監視対象にするアクターです。
 *       複数指定可能です。
 * @type actor[]
 * @default []
 *
 * @param enemies
 * @text 対象エネミー
 * @desc ステートの監視対象にするエネミーです。
 *       複数指定可能です。
 * @type enemy[]
 * @default []
 *
 * @param target_value
 * @text 監視対象のパラメータ
 * @desc 監視する対象のパラメータを指定します。
 * @type select
 * @default hp
 * @option HP
 * @value hp
 * @option MP
 * @value mp
 * @option TP
 * @value tp
 *
 * @param parcent
 * @text 指定のパーセント
 * @desc 値に割合を指定している場合に指定してください。
 * @type string
 *
 * @param value
 * @text 指定の固定値
 * @desc 値に固定値を指定している場合に指定してください。
 * @type string
 *
 * @param value_variable
 * @text 指定の変数
 * @desc 値に変数を指定している場合に指定してください。
 * @type variable
 * @default 0
 *
 * @param condition
 * @text 指定値の状態
 * @desc 以上・以下・一致などどの状態になるのか指定して下さい。
 * @type select
 * @default equal
 * @option 一致した
 * @value equal
 * @option 未満になった
 * @value lt
 * @option 超えた
 * @value gt
 * @option 以下になった
 * @value ol
 * @option 以上になった
 * @value om
 *
 * @param onlyBattle
 * @text 戦闘中のみ
 * @desc 戦闘中のステート変更のみトリガにします。
 * @type boolean
 * @default false
 *
 * @param commonEventId
 * @text コモンイベントID
 * @desc 指定の状態になった時に呼び出すコモンイベントのIDです。
 * @type common_event
 * @default 0
 *
 * @param comment
 * @text 注釈
 * @desc プラグイン側では使用しません。
 *       メモ欄としてお使いください。
 * @type multiline_string
 * @default
 *
 */

(() => {
	"use strict";

    const pluginName = "EventHook";
    const script = document.currentScript;
    const param  = PluginManagerEx.createParameter(script);

    //========================================================================
    // 共通基盤
    //========================================================================
    const inBattle = () => {
        return SceneManager._scene instanceof Scene_Battle;
    };

    const fireCommonEvent = (eventId) => {
        if (inBattle()) {
            $gameTemp.reserveCommonEvent(eventId);
        } else {
            $gameMap.setupDynamicCommon(eventId);
        }
    };

    const getBattlerId = (battler) => {
        if (battler instanceof Game_Actor) {
            return battler.actorId();
        } else if (battler instanceof Game_Enemy) {
            return battler.enemyId();
        } else {
            // unknown battler
            return 0;
        }
    };

    const filterTarget = (hooks, battler) => {
        const id = getBattlerId(battler);
        return hooks.filter(hook => {
            if (hook.actors.length === 0 && hook.enemies.length === 0) {
                return true;
            } else if (battler instanceof Game_Actor) {
                return hook.actors.includes(id);
            } else if (battler instanceof Game_Enemy) {
                return hook.enemies.includes(id);
            } else {
                return false;
            }
        });
    };

    //========================================================================
    // 新規ゲーム開始直後
    //========================================================================
    Game_Temp.prototype.setNewGameFlag = function(value) {
        this._isNewGame = value;
    };

    Game_Temp.prototype.isNewGame = function() {
        return this._isNewGame;
    };

    Game_Temp.prototype.setDataLoaded = function(value) {
        this._isDataLoadSuccess = value;
    };

    Game_Temp.prototype.isDataLoaded = function() {
        return this._isDataLoadSuccess;
    };

    const _DataManager_setupNewGame = DataManager.setupNewGame;
    DataManager.setupNewGame = function() {
        _DataManager_setupNewGame.apply(this, arguments);

        // ここではまだローディングが完了していないため、フラグを設定するだけとする
        // 実際の判定と実行はマップロード後にマップ監視と一緒に行う
        $gameTemp.setNewGameFlag(true);
    };

    //========================================================================
    // ゲームデータロード直後監視
    //========================================================================
    const _Game_System_onAfterLoad = Game_System.prototype.onAfterLoad;
    Game_System.prototype.onAfterLoad = function() {
        _Game_System_onAfterLoad.apply(this, arguments);

        // ここではまだローディングが完了していないため、フラグを設定するだけとする
        // 実際の判定と実行はマップロード後にマップ監視と一緒に行う
        $gameTemp.setDataLoaded(true);

        const hookList = findGameLoadRegistered();
        hookList.forEach(hook => {
            // 条件を満たしたのでイベントを実行
            fireCommonEvent(hook.commonEventId);
        });
    };

    //========================================================================
    // 変数監視系
    //========================================================================
    const findVariableRegistered = (variableId) => {
        return param.hooks.filter(p => p._parameter.watchType.startsWith('variable_change')).filter(p => {
            if (p._parameter.target_variable !== variableId) {
                return false; // 監視対象の変数じゃない
            }

            if (p._parameter.commonEventId <= 0) {
                console.log('[findVariableRegistered] invalid common event ID: ' + p._parameter.commonEventId);
                return false; // コモンイベントの指定が不正
            }
            return true;
        }).map(p => p._parameter);
    }

    const _Game_Variables_setValue = Game_Variables.prototype.setValue;
    Game_Variables.prototype.setValue = function(variableId, value) {
        const hookList = findVariableRegistered(variableId);
        if (hookList.length <= 0) {
            // 監視対象ではないので通常のsetValueを実行して終了
            _Game_Variables_setValue.apply(this, arguments);
        } else {
            // とりあえず現在値を退避
            const oldValue = this.value(variableId);

            // 変数を変更
            _Game_Variables_setValue.apply(this, arguments);

            // 変更後の値を取得
            const value = this.value(variableId);

            // 値に変更があるかどうかを確認(同じ値は何もしない)
            if (oldValue !== value) {
                hookList.forEach(hook => {
                    const isFireNeeded = () => {
                        switch (hook.watchType) {
                            case 'variable_change_any':
                                // 値に変更があった
                                return true;
                            case 'variable_change_match':
                            case 'variable_change_unmatch':
                                const staticValue = (!hook.value || hook.value === '') ? 0 : hook.value;
                                const target = (hook.value_variable > 0) ? this.value(hook.value_variable) : staticValue;
                                if (hook.watchType === 'variable_change_match') {
                                    // 変数が指定の値になった
                                    return (value === target);
                                } else if (hook.watchType === 'variable_change_unmatch') {
                                    // 変数が指定の値ではなくなった
                                    return (oldValue === target);
                                }
                                break;
                        } // switch
                        return false;
                    };

                    if (isFireNeeded()) {
                        // 条件を満たしたのでイベントを実行
                        fireCommonEvent(hook.commonEventId);
                    }
                });
            }
        }
    }

    //========================================================================
    // マップ移動監視
    //========================================================================
    const findGameLoadRegistered = () => {
        return param.hooks.filter(p => p._parameter.watchType === 'data_loaded').filter(p => {
            if (p._parameter.commonEventId <= 0) {
                console.log('[findGameLoadRegistered] invalid common event ID: ' + p._parameter.commonEventId);
                return false; // コモンイベントの指定が不正
            }
            return true;
        }).map(p => p._parameter);
    };

    const findMapRegistered = (mapId, mapName) => {
        return param.hooks.filter(p => p._parameter.watchType === 'map_transfer').map(p => p._parameter).filter(hook => {
            if (hook.commonEventId <= 0) {
                console.log('[findMapRegistered] invalid common event ID: ' + hook.commonEventId);
                return false; // コモンイベントの指定が不正
            }

            const staticValue = (!hook.value || hook.value === '') ? 0 : hook.value;
            const target = (hook.value_variable > 0) ? this.value(hook.value_variable) : staticValue;

            if (isNaN(target)) {
                // 対象の値はマップ名
                return (mapName === target); // 監視対象ならtrue
            } else {
                // 対象の値はマップID
                return (mapId === target); // 監視対象ならtrue
            }
        });
    };

    const _Game_Player_performTransfer = Game_Player.prototype.performTransfer;
    Game_Player.prototype.performTransfer = function() {
        // 遷移先マップIDを取得しておく（マップ移動処理時にクリアされるので）
        const mapId = this._newMapId;
        const mapName = !!$dataMapInfos[mapId] ? $dataMapInfos[mapId].name : undefined;

        // ここで新規ゲーム開始フラグとデータロードフラグを倒しておく
        // （呼び出したコモンイベントでさらにマップの移動が行われた際に不具合が起きるのを回避）
        const isNewGame = $gameTemp.isNewGame();
        const isDataLoaded = $gameTemp.isDataLoaded();

        // とりあえず通常のマップ移動処理を実行
        _Game_Player_performTransfer.apply(this, arguments);

        if (isNewGame) {
            // 新規ゲーム開始時
            $gameTemp.setNewGameFlag(false);

            param.hooks.filter(p => {
                if (p._parameter.watchType !== 'new_game') {
                    return false;
                }

                if (p._parameter.commonEventId <= 0) {
                    console.log('[new game] invalid common event ID: ' + p._parameter.commonEventId);
                    return false; // コモンイベントの指定が不正なので何もしない
                }
                return true;
            }).map(p => p._parameter).forEach(hook => {
                fireCommonEvent(hook.commonEventId);
            });
        } else if (isDataLoaded) {
            // データロード直後
            $gameTemp.setDataLoaded(false);
            const hookList = findGameLoadRegistered();
            hookList.forEach(hook => {
                fireCommonEvent(hook.commonEventId);
            });
        } else {
            // 通常のマップ移動
            const hookList = findMapRegistered(mapId, mapName);
            hookList.forEach(hook => {
                fireCommonEvent(hook.commonEventId);
            });
        }
    };

    //========================================================================
    // ステート監視
    //========================================================================
    const StateEvents = {
        ADD: "add_state",
        REMOVE: "remove_state",
        CLEAR: "clear_state",
    }

    const findTargetTrigger = (battler, stateId, event) => {
        const hooks = filterTarget(param.stateHooks.map(p => p._parameter), battler);
        return hooks.filter(hook => {
            // filter by only battle setting
            return hook.onlyBattle === false || inBattle();
        }).filter(hook => {
            // filter by state
            if (hook.watchType === StateEvents.CLEAR) {
                // ステートクリアは対象ステート無視
                return true;
            }
            return hook.state === stateId;
        }).filter(hook => {
            // filter by event
            return hook.watchType === event;
        });
    };

    const triggerState = (battler, stateId, event) => {
        findTargetTrigger(battler, stateId, event).forEach(hook => {
            fireCommonEvent(hook.commonEventId);
        });
    };

    const _Game_BattlerBase_addNewState = Game_BattlerBase.prototype.addNewState;
    Game_BattlerBase.prototype.addNewState = function(stateId) {
        _Game_BattlerBase_addNewState.apply(this, arguments);
        triggerState(this, stateId, StateEvents.ADD);
    };

    const _Game_BattlerBase_eraseState = Game_BattlerBase.prototype.eraseState;
    Game_BattlerBase.prototype.eraseState = function(stateId) {
        _Game_BattlerBase_eraseState.apply(this, arguments);
        triggerState(this, stateId, StateEvents.REMOVE);
    };

    const _Game_BattlerBase_clearStates = Game_BattlerBase.prototype.clearStates;
    Game_BattlerBase.prototype.clearStates = function() {
        _Game_BattlerBase_clearStates.apply(this, arguments);
        triggerState(this, 0, StateEvents.CLEAR);
    };

    //========================================================================
    // 戦闘パラメータ/ダメージ監視
    //========================================================================
    const decideTargetValue = (hook, max) => {
        if (!!hook.parcent && hook.parcent > 0) {
            return PluginUtils.percent(max, hook.parcent);
        } else if (!!hook.value_variable && hook.value_variable > 0) {
            return PluginUtils.getVariables(hook.value_variable);
        } else {
            return hook.value;
        }
    };

    const triggerBattleEvent = (battler, parameterType, currentValue, value, max) => {
        const hooks = filterTarget(param.battleHooks.map(p => p._parameter), battler);
        return hooks.filter(hook => {
            // filter by only battle setting
            return hook.onlyBattle === false || inBattle();
        }).filter(hook => {
            if (hook.watchEvent !== 'damage') {
                return hook.target_value === parameterType;
            } else {
                return parameterType === 'damage';
            }
        }).filter(hook => {
            const targetValue = decideTargetValue(hook, max);
            switch (hook.condition) {
                case 'equal':
                    return targetValue === value && value !== currentValue;
                case 'lt':
                    return targetValue > value && value !== currentValue;
                case 'gt':
                    return targetValue < value && value !== currentValue;
                case 'ol':
                    return targetValue >= value && value !== currentValue;
                case 'om':
                    return targetValue <= value && value !== currentValue;
                default:
                    // nothing to do
            };
            return false;
        }).forEach(hook => {
            fireCommonEvent(hook.commonEventId);
        });
    };

    //========================================================================
    // 戦闘予約イベント問題対策
    //========================================================================
    // 戦闘系のイベントは予約していても戦闘シーンを抜けた際に全て破棄されてしまうのでイベントが残っている場合待つ
    const _BattleManager_checkBattleEnd = BattleManager.checkBattleEnd;
    BattleManager.checkBattleEnd = function() {
        if (!$gameTemp.isCommonEventReserved() && !$gameTroop.isEventRunning()) {
            return _BattleManager_checkBattleEnd.apply(this, arguments);
        }
        return false;
    };

    const _BattleManager_checkAbort = BattleManager.checkAbort;
    BattleManager.checkAbort = function() {
        if (!$gameTemp.isCommonEventReserved() && !$gameTroop.isEventRunning()) {
            return _BattleManager_checkAbort.apply(this, arguments);
        }
        return this.updateEventMain();  // イベント進行中なのでイベントを回す
    };

    //========================================================================
    // 戦闘監視
    //========================================================================
    const _Game_BattlerBase_prototype_setHp = Game_BattlerBase.prototype.setHp;
    Game_BattlerBase.prototype.setHp = function(hp) {
        const current = this.hp;
        _Game_BattlerBase_prototype_setHp.apply(this, arguments);
        triggerBattleEvent(this, 'hp', current, this.hp, this.mhp);
    };

    const _Game_BattlerBase_prototype_setMp = Game_BattlerBase.prototype.setMp;
    Game_BattlerBase.prototype.setMp = function(mp) {
        const current = this.mp;
        _Game_BattlerBase_prototype_setMp.apply(this, arguments);
        triggerBattleEvent(this, 'mp', current, this.mp, this.mmp);
    };

    const _Game_BattlerBase_prototype_setTp = Game_BattlerBase.prototype.setTp;
    Game_BattlerBase.prototype.setTp = function(tp) {
        const current = this.tp;
        _Game_BattlerBase_prototype_setTp.apply(this, arguments);
        triggerBattleEvent(this, 'tp', current, this.tp, 100);
    };

    const _Game_Battler_prototype_onDamage = Game_Battler.prototype.onDamage;
    Game_Battler.prototype.onDamage = function(value) {
        _Game_Battler_prototype_onDamage.apply(this, arguments);
        triggerBattleEvent(this, 'damage', 0, value, this.mhp);
    };
})();
