//============================================================================
// SU_SyncSpineAnimation
//============================================================================

var Imported = Imported || {};
Imported.SU_SyncSpine = true;
var SU = SU || {};
SU.SyncSpine = SU.SyncSpine || {};

/*:
* @plugindesc マップに Spine モデルを表示してプレイヤーの歩行と同期してアニメーションさせるためのプラグイン
* @author Su
* @target MV
*
* @help
* ============================================================================
* Introduction
* ============================================================================
* マップ上に Spine モデルを表示させ、プレイヤーの移動に応じてアニメーションを再生するプラグインです。
* 
* ============================================================================
* Usage : 使い方
* ============================================================================
* マップ上にSpineモデルを表示します。　
* 
* マップ上での移動に合わせてアニメーションを再生します。
*
* SU_FadeSpine.js を入れていると、表示・非表示の際にフェード効果をかけることができます。
* 
* 
* スクリプトで立ち絵のSpineデータを取得したい場合は、
*
*   $gameSystem._mapStand.spine
*
* に入っているので、例えばスキンを変更したい場合は
*
*   $gameSystem._mapStand.spine.setSkin('SkinName');
*
* などとしてください。
* 
* ============================================================================
* Plugin Command : プラグインコマンド
* ============================================================================
* SyncSpine_Enable
* - モデルを表示します。
*
* SyncSpine_Disable
* - モデルを非表示にします。
*
* ============================================================================
* Changelog : 更新履歴
* ============================================================================
* Version 1.2 2023/03/28:
* - 一定時間無操作状態が続くと放置アニメーションを再生する設定を追加しました
*
* Version 1.1 2023/03/09:
* - 初期スキンを指定できるようにしました。
* - セーブ処理を変更（この変更により、以前のセーブデータをロードした際に立ち絵が表示されていない場合があります。再び表示のコマンドを実行することで表示はされるはずです）
*
* Version 1.00:
* - Plugin Released.
*
* ============================================================================
* @param Appearance settings
    * @desc この項目自体は設定用ではありません
    * @text Spine 表示設定
*
* @param ModelName
* @type string
* @desc 使用するSpineファイルを指定します
* @text スケルトン名
* @parent Appearance settings
* 
* @param Position
* @type struct<Coordinate>
* @desc 表示するSpineモデル（画像）のルート座標を指定します
* @text モデル（画像）の表示位置
* @parent Appearance settings
*
* @param Scale
* @type number
* @decimals 2
* @default 1.0
* @desc Spineモデル（画像）の拡大率を指定します
* 1.0 で等倍表示になります
* @text モデル（画像）の拡大率
* @parent Appearance settings
*
* @param DefaultSkin
* @type string
* @desc 初期状態のスキンを指定します　
* @text 初期スキン
* @parent Appearance settings
*
* @param Animation settings
* @desc この項目自体は設定用ではありません
* @text アニメーション設定
*
* @param Waiting Animation
* @desc この項目自体は設定用ではありません
* @text === 待機中 ===
* @parent Animation settings
*
* @param Waiting
* @type struct<Animation>
* @desc 待機のアニメーションを指定します
* @text 待機アニメーション
* @parent Waiting Animation
* 
* @param WaitingRandom
* @type struct<RandomAnimation>
    * @desc 放置状態（一定時間プレイヤーの入力が無い）のときに、一定間隔で再生するアニメーションを指定します
    * @text 放置中アニメーション
    * @parent Waiting
*
* @param Moving Animation
* @desc この項目自体は設定用ではありません
* @text === 移動 ===
* @parent Animation settings
*
* @param StartWalking
* @type struct<Animation>
* @desc 歩き出しのアニメーションを指定します
* @text 歩き出し
* @parent Moving Animation
* 
* @param Walking
* @type struct<Animation>
* @desc 歩行中のアニメーションを指定します
* @text 歩行中
* @parent Moving Animation
* 
* @param EndWalking
* @type struct<Animation>
* @desc 立ち止まる際のアニメーションを指定します
* @text 立ち止まる
* @parent Moving Animation
* 
* @param InterruptWalk
* @type boolean
* @default false
    * @desc 立ち止まる時に、歩きのアニメーションを中断するか、終了まで待つか選択します
    * @text 中断して立ち止まる
* @parent EndWalking
*
* @param StopDelay
* @type number
* @default 6
* @desc 移動キーを話してから立ち止まるまでのフレーム数
* @text 停止遅延フレーム数
* @parent EndWalking
*
    * @param Others
    * @desc この項目自体は設定用ではありません
    * @text === トラック設定 ===
* @parent Animation settings
    * 
* @param OptionalAnimations
* @type struct<Optional>[]
* @desc トラック毎にアニメーションを追加で設定できます
* @text トラック指定アニメーション
* @parent Others
*
* @param ResetAnimation
* @type string
* @default 000
* @desc トラックリセット用のアニメーション名
* @text リセットアニメーション
* @parent Others
*
* @param Mix Settings
* @desc この項目自体は設定用ではありません
* @text ミックス値の設定
* 
* @param WholeMix
* @type number
* @decimals 2
* @default 0.0
* @desc 全てのアニメーション間に同じミックス値を設定します
* @text 全体のミックス値
* @parent Mix Settings
*
* @param IndividualMix
* @type struct<Individuals>[]
* @default []
* @desc 指定のアニメーション間で個別にミックス値を設定します
* @text 個別のミックス値
* @parent Mix Settings
*
* @param FadeCount
* @type number
* @default 6
* @desc SpineFade をインポートしている場合、モデルの表示・非表示の際にフェード効果を掛けることができ、そのフレーム数を指定します
* @text 変化フレーム数（要 FadeSpine.js）
*/

/*~struct~Coordinate:
* @param X
* @type number
* @default 0
* @max 9007199254740991
* @min -9008199254740991
* @desc x座標
*
* @param Y
* @type number
* @default 0
* @max 9007199254740991
* @min -9008199254740991
* @desc y座標
*/

/*~struct~Animation:
* @param Animation
* @type string
* @desc アニメーション名を指定します
* @text アニメーション名
*
* @param TimeScale
* @type number
* @decimals 2
* @default 1.0
* @desc 再生速度
* @text 再生速度
*/

/*~struct~RandomAnimation:
    * @param Animation
    * @type struct<Animation>[]
    * @desc アニメーションを指定します
    * 複数指定している場合はランダムに再生されます
    * @text 放置中アニメーション
    *
    * @param WaitFrames
    * @type number
    * @default 240
    * @desc プレイヤーの入力が無くなってから、放置状態に入る（最初にアニメーションが再生される）までのフレーム数を指定します
    * @text 放置状態化フレーム数
    *
    * @param AnimationSpan
    * @type number
    * @default 180
    * @desc 放置状態中にアニメーションが再生される間隔を指定します
    * @text アニメーション間隔
*/

/*~struct~Optional:
* @param TrackId
* @type number
* @min 1
* @desc トラック番号
* ０はメインのアニメーションで使用しているので最低値は１
* @text トラック番号
*
* @param Animation
* @type string
* @desc アニメーション名
* @text アニメーション名
*
* @param Enabled
* @type boolean
* @default true
* @desc アニメーションを再生した状態で表示するかどうか指定します
* @text デフォルトで有効
*
* @param IsRandomPlay
* @type boolean
* @default false
* @desc 設定値の間のランダムなフレーム数でアニメーションを再生します
* false の場合は常に繰り返し再生されます
* @text ランダム再生の有効化(今は意味無いです)
*
* @param MinSpan
* @type number
* @min 1
* @desc アニメーションを実行するフレーム間隔の最小値
* @text 最小フレーム数
* @parent IsRandomPlay
*
* @param MaxSpan
* @type number
* @min 1
* @desc アニメーションを実行するフレーム間隔の最大値
* @text 最大フレーム数
* @parent IsRandomPlay
*
*/

/*~struct~Individuals:
* @param Src
* @type string
* @desc 切り替わる前のアニメーション
* @text 切り替わる前のアニメーション
*
* @param Dst
* @type string
* @desc 切り替えた後のアニメーション
* @text 切り替えた後のアニメーション
*
* @param Value
* @type number
* @decimals 2
* @default 0.0
* @desc ミックス値
* @text ミックス値
*/


(() => {
    'use strict';

    //============================================================================
    // パラメータ取得
    //============================================================================
    const pluginName = document.currentScript.src.split('/').pop().replace(/\.js$/, '');
    const pluginParams = PluginManager.parameters(pluginName);
    const parsedParams = JSON.parse(JSON.stringify(
        pluginParams,
        (key, value) => {
            try { return JSON.parse(value); } catch (e) { }
            return value
        }
    ));

    const modelName = parsedParams['ModelName'];
    const position = parsedParams['Position'];
    const scale = parsedParams['Scale'];
    const defaultSkin = parsedParams['DefaultSkin'];
    const startWalking = parsedParams['StartWalking'];
    const walking = parsedParams['Walking'];
    const endWalking = parsedParams['EndWalking'];
    const waiting = parsedParams['Waiting'];
    const waitingRandom = parsedParams['WaitingRandom'];
    const noControlDuration = waitingRandom['WaitFrames'];
    const animationSpan = waitingRandom['AnimationSpan'];
    const interruptWalk = parsedParams['InterruptWalk'];
    const stopDelay = parsedParams['StopDelay'];
    const reset = parsedParams['ResetAnimation'];

    const wholeMix = parsedParams['WholeMix'];
    const individualMixes = parsedParams['IndividualMix'];
    const fadeFrameCount = parsedParams['FadeCount'];

    let waitFrames = 0;
    let walkStopFrames = 0;

    class Spine_Map {
        constructor() {
            this.initialize();
        }

        get spine() { return this._spine };
        get enabled() { return this._enabled };
        // sprite の中身が入るとセーブできなくなる
        // get sprite() { return this._spriteSpine };

        initialize() {
            this._spine = new Game_Spine();
            this._enabled = false;
            this._spriteSpine = null;

            this.optionalAnimations = parsedParams['OptionalAnimations'].map(e => {
                e.FrameCount = 0;
                e.NextPlayFrame = e['MinSpan'];
                return e;
            });
        }

        create() {
            const sprite = new Sprite_Spine(this.spine);
            this._enabled = true;

            return sprite;
        }

        isImportedFadeSpine() { return !!SU.FadeSpine };

        setModel() {
            const spine = this._spine;
            spine.setSkeleton(modelName)
                .setOffset(position['X'], position['Y'])
                .setScale(scale, scale)
                .setSkin(defaultSkin)
                .setVisible(false)
                .setAnimation(0, [waiting['Animation']], 'sequential', 'continue', true);
            if (SU.FadeSpine) {
                spine.setColor(1, 1, 1, 0);
            }

            // トラックのアニメーション指定
            for (const animation of this.optionalAnimations) {
                if (!animation['Enabled']) continue;

                spine.setAnimation(animation['TrackId'], animation['Animation'], 'sequential', 'continue', false);
            }

            // アニメーションのミックス値を設定
            // 全体
            spine.setMix(wholeMix);
            // 個別
            for (const setting of individualMixes) {
                const src = setting['Src'];
                const dst = setting['Dst'];
                const value = setting['Value'];
                if (src && dst)
                    spine.setMix(src, dst, value);
            }

            return spine;

        }

        //============================================================================
        // モデルの表示・非表示
        //============================================================================
        appear() {
            const spine = this._spine;
            if (!spine.skeleton) {
                this.setModel();
            }
            if (SU.FadeSpine) {
                spine.setVisible(true);
                SU.FadeSpine.executions[spine.skeleton] = SU.FadeSpine.setAlpha(spine, 1, fadeFrameCount,);
            }
            else
                spine.setVisible(true);
            //TODO: メニューから戻った時に待機状態でパッと表示されるので、spine データを保存するなどして対応できるかも
            //※上記は Sprite の生成時にしかできない
        }
        disappear() {
            const spine = this._spine;
            if (SU.FadeSpine)
                SU.FadeSpine.executions[spine.skeleton] = SU.FadeSpine.setAlpha(spine, 0, fadeFrameCount, () => spine.setVisible(false));
            else
                spine.setVisible(false);
        }

        enable() {
            this.appear();

            this._enabled = true;
        };
        disable() {
            this.disappear();

            this._enabled = false;
        };

        //TODO: トラック毎のアニメーション設定を見直す
        enableTrack(trackId) {
            this._spine.setAnimation()
        }

        disableTrack(trackId) {
            this._spine.setAnimation(trackId, reset);
        }

        update() {
            this.updateOptionals();
        }

        updateOptionals() {
            const spine = this._spine;
            for (const animation of this.optionalAnimations) {
                // ランダム再生が有効なものだけ実行
                if (!animation['IsRandomPlay']) continue;

                let frameCount = animation['FrameCount'];
                frameCount++

                if (frameCount >= animation['NextPlayFrame']) {
                    if (this._spine) {
                        spine.setAnimation(animation['TrackId'], [animation['Animation'], reset], 'sequential', 'none', false);
                    }
                    frameCount = 0;
                    const minSpan = Number(animation['MinSpan']);
                    const maxSpan = Number(animation['MaxSpan']);
                    if (minSpan > maxSpan) {
                        console.error(`"maxSpan" must be bigger than "minSpan" on Track ID animation['TrackId']`);
                        return;
                    }
                    animation['NextPlayFrame'] = minSpan + Math.random() * (maxSpan - minSpan);
                }
                animation['FrameCount'] = frameCount;
            }
        }
    }
    window.Spine_Map = Spine_Map;




    //============================================================================
    // モデルをマップに表示
    //============================================================================

    (_createUpperLayer_ => {
        Spriteset_Map.prototype.createUpperLayer = function () {
            _createUpperLayer_.call(this);

            let spine = null;
            if ($gameSystem._mapStand) {
                spine = $gameSystem._mapStand
            }
            else {
                spine = new Spine_Map();
                $gameSystem._mapStand = spine;
            }
            this.addChild(spine.create());
        }
    })(Spriteset_Map.prototype.createUpperLayer);

    (_update_ => {
        Spriteset_Map.prototype.update = function () {
            _update_.call(this);

            $gameSystem._mapStand.update();
        }
    })(Spriteset_Map.prototype.update);

    // // イベント中は非表示
    // (_setup_ => {
    //     Game_Interpreter.prototype.setup = function (list, eventId) {
    //         _setup_.apply(this, arguments);

    //         if (!spineMap || !spineMap.enabled) return;
    //         spineMap.disappear();
    //     }
    // })(Game_Interpreter.prototype.setup);

    // (_terminate_ => {
    //     Game_Interpreter.prototype.terminate = function () {
    //         _terminate_.call(this);

    //         if (!spineMap || !spineMap.enabled) return;
    //         spineMap.appear();
    //     }
    // })(Game_Interpreter.prototype.terminate);

    //============================================================================
    // 歩行アニメーション
    //============================================================================
    let isMoveStarted = false;
    let isWalking = false;

    (_increaseSteps_ => {
        Game_Player.prototype.increaseSteps = function () {
            _increaseSteps_.call(this);

            if (isMoveStarted) {
                // setAnimation の重ね掛けを避ける
                if (isWalking) return;

                const walkAnim = $gameSystem._mapStand.spine.track;
                if (walkAnim['0'].list[0].name === walking['Animation']) {
                    // 歩き出しアニメーションと歩き中アニメーションが同じとき
                    $gameSystem._mapStand.spine.setTimeScale(walking['TimeScale']);
                } else {
                    // 別々のアニメーションが設定されているとき
                    const stepAnimation = `${walking['Animation']}/timesCale=${walking['TimeScale']}`;
                    $gameSystem._mapStand.spine.setAnimation(0,
                        [stepAnimation],
                        'sequential',
                        'continue',
                        interruptWalk);
                }
                isWalking = true;
            } else {
                // 歩き出すとき
                const stepAnimation = `${startWalking['Animation'] || walking['Animation']}/timesCale=${startWalking['TimeScale']}`;
                const animation = [stepAnimation];

                $gameSystem._mapStand.spine.setAnimation(0,
                    animation,
                    'sequential',
                    'continue',
                    true);
                isMoveStarted = true;
            }
        }
    })(Game_Player.prototype.increaseSteps);

    //============================================================================
    // 待機中のランダムアニメーション
    //============================================================================
    // 放置アニメーションを再生している最中であるか
    let isPlayingWait = false;

    // 移動の入力があれば放置フレーム数をリセット
    (_executeMove_ => {
        Game_Player.prototype.executeMove = function (direction) {
            _executeMove_.apply(this, arguments);

            this.resetWaitFrame();
            isPlayingWait = false;
        }
    })(Game_Player.prototype.executeMove);

    Game_Player.prototype.resetWaitFrame = function () { waitFrames = 0; };

    // 停止
    (_updateStop_ => {
        Game_Player.prototype.updateStop = function () {
            _updateStop_.call(this);

            if (walkStopFrames >= stopDelay) {
                if (isMoveStarted) {
                    const waitAnim = `${waiting['Animation']}/timeScale=${waiting['TimeScale']}`;

                    $gameSystem._mapStand.spine.setAnimation(0,
                        endWalking ? [`${endWalking['Animation']}/timeScale=${endWalking['TimeScale']}`, waitAnim] : [waitAnim],
                        'sequential', 'continue', isWalking ? true : false)
                        .setTimeScale(1);
                    isWalking = false;
                    isMoveStarted = false;

                    walkStopFrames = 0;
                }
            }

            if (isMoveStarted)
                walkStopFrames++;

            if (!waitingRandom) return;

            if (this.checkStop(noControlDuration)) {
                const randomAnimation = waitingRandom['Animation'];

                // 指定されたアニメーションが複数あれば乱数で決定する
                const animation = randomAnimation.length >= 2
                    ? randomAnimation[Math.floor(Math.random() * (randomAnimation.length))]['Animation']
                    : randomAnimation[0]['Animation'];

                if (waitFrames >= animationSpan) {
                    $gameSystem._mapStand.spine.setAnimation(0,
                        [animation],
                        'sequential', 'continue', true);
                    isPlayingWait = true;
                }
            }

            if (isPlayingWait) {
                this.resetWaitFrame();
            }
            else {
                waitFrames++;
            }
        }
    })(Game_Player.prototype.updateStop);

    (_updatePlayData_ => {
        Game_Spine.prototype.updatePlayData = function (entry, reason) {
            let playData = this._playData[entry.trackIndex];
            if (!playData) return;

            // メインのアニメーションが終了した時
            if (reason === 'complete') {
                if (Number(entry.trackIndex) !== 0) return;

                // 待機中ランダムアニメーションを終了させる
                if (isPlayingWait) {
                    isPlayingWait = false;
                    const waitAnim = `${waiting['Animation']}/timeScale=${waiting['TimeScale']}`;
                    this.setAnimation(0,
                        [waitAnim],
                        'sequential', 'continue', true)
                        .setTimeScale(1);
                }
            }

            _updatePlayData_.apply(this, arguments);
        }
    })(Game_Spine.prototype.updatePlayData);

    //============================================================================
    // プラグインコマンド
    //============================================================================

    (_pluginCommand_ => {
        Game_Interpreter.prototype.pluginCommand = function (command, args) {
            _pluginCommand_.apply(this, arguments);

            const commandParts = command.toUpperCase().split('_');
            const prefix = commandParts[0].replace().toUpperCase();
            const commandName = commandParts[1];
            if (prefix !== 'SYNCSPINE') return;

            if (commandName === 'ENABLE') {
                $gameSystem._mapStand.enable();
            }
            else if (commandName === 'DISABLE') {
                $gameSystem._mapStand.disable();
            }
        }
    })(Game_Interpreter.prototype.pluginCommand);
})();