//=============================================================================
// Keke_LoopBalloon - ループ・フキダシアイコン
// バージョン: 1.1.0
//=============================================================================
// Copyright (c) 2022 ケケー
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
//=============================================================================

/*:
 * @target MZ
 * @plugindesc 改造_フキダシアイコンをループ表示させる
 * @author ケケー
 * @url https://kekeelabo.com
 *
 * @help
 * 【ver.1.1.0】
 * ◎フキダシアイコンをループ表示できる
 * ◎プラグインコマンドや注釈でフキダシアイコンを制御できる
 *
 *
 * 【機能1】プラグインコマンドでフキダシアイコンを表示
 * => プラグインコマンド → フキダシアイコンの表示
 * 　対象キャラ・フキダシアイコンIDを入力
 *
 *
 * 【機能2】プラグインコマンドでフキダシアイコンを消去
 * => プラグインコマンド → フキダシアイコンの消去
 * 　対象キャラを入力
 *
 *
 * 【機能3】注釈でフキダシアイコンを表示
 * => イベントコマンドの『注釈』を使う
 * ★注釈の置き場所は「現在のページの一番上」か「1ページ目の一番上」
 * 　まず現在のページのがあればそれを使い、なければ1ページ目のものを使う
 * 　注釈は複数つなげることも可能
 * 置いた注釈の中に
 * <balloon: フキダシアイコンID, (ループウェイト), (ループ回数)>
 * 　※ループウェイトとループ回数は省略可能
 * 例)
 * <balloon: 1>
 * 　びっくりのフキダシアイコンを表示
 * <balloon: 0>
 * 　フキダシアイコンを表示しない
 * <balloon: 2, 30, 5>
 * 　はてなのフキダシアイコンを表示
 * 　ループ間に 30フレーム のウェイトを挟み、
 * 　5回 表示したら終了
 *
 *
 * 【付属】標準のアイコンID
 * 1:  びっくり
 * 2:  はてな
 * 3:  音符
 * 4:  ハート
 * 5:  怒り
 * 6:  汗
 * 7:  くしゃくしゃ
 * 8:  沈黙
 * 9:  電球
 * 10: Zzz
 * 11: ユーザー定義1
 * 12: ユーザー定義2
 * 13: ユーザー定義3
 * 14: ユーザー定義4
 * 15: ユーザー定義5
 *
 *
 * ● 利用規約 ●
 * MITライセンスのもと、自由に使ってくれて大丈夫です
 *
 *
 *
 * @param 位置の自動調整
 * @desc positionAdjust フキダシの位置をキャラの拡大率に応じて自動調整する
 * @type boolean
 * @default true
 *
 * @param サイズ自動調整
 * @desc sizeAdjust フキダシの大きさをキャラの拡大率に応じて自動調整する
 * @type boolean
 * @default true
 *
 *
 * @command フキダシアイコンの表示
 * @desc showBalloon フキダシアイコンをループ表示する
 *
 * @arg 対象キャラ-名前
 * @desc taget-name ***:イベント検索、セルフ:イベント自身、プレイヤー:同、全:全キャラ、\v[***]:変数。, で複数指定
 *
 * @arg 対象キャラ-ID
 * @desc taget-chara 1〜:イベントID。0:イベント自身、-1:プレイヤー、-2:全キャラ、\v[***]:変数, で複数、~ でまとめて指定
 *
 * @arg フキダシアイコンID
 * @desc balloonId ループ表示するフキダシアイコンのID
 * @type number
 *
 * @arg ループウェイト
 * @desc roopWait ループの間に挟むウェイト。5 なら 5フレーム。0~5 なら 0 から 5 の間でランダム
 *
 * @arg ループ回数
 * @desc roopNum ループ表示する回数。5 なら 5回 表示。0~5 なら 0 から 5 の間でランダム。空欄なら無制限
 *
 *
 *
 * @command フキダシアイコンの消去
 * @desc deleteBalloon 表示中のフキダシアイコンを消去する
 *
 * @arg 対象キャラ-名前
 * @desc target-name ***:イベント検索、セルフ:イベント自身、プレイヤー:同、全:全キャラ、\v[***]:変数。, で複数指定
 *
 * @arg 対象キャラ-ID
 * @desc target-id 1〜:イベントID。0:イベント自身、-1:プレイヤー、-2:全キャラ、\v[***]:変数, で複数、~ でまとめて指定
 */

(() => {
	//- プラグイン名
	const pluginName = document.currentScript.src.match(/^.*\/(.*).js$/)[1];

	//==================================================
	//--  パラメータ受け取り
	//==================================================

	//- 真偽化
	function toBoolean(str) {
		if (!str) {
			return false;
		}
		const str2 = str.toString().toLowerCase();
		if (str2 == "true" || str2 == "on") {
			return true;
		}
		if (str2 == "false" || str2 == "off") {
			return false;
		}
		return Number(str);
	}

	let parameters = PluginManager.parameters(pluginName);

	const keke_positionAdjust = toBoolean(parameters["位置の自動調整"]);
	const keke_sizeAdjust = toBoolean(parameters["サイズ自動調整"]);

	parameters = null;

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

	//- フキダシアイコンの表示
	PluginManager.registerCommand(pluginName, "フキダシアイコンの表示", (args) => {
		// フキダシIDを取得
		const balloonId = Number(convertVariable(args["フキダシアイコンID"]));
		if (!balloonId) {
			return;
		}
		// 対象キャラを取得
		const names = convertVariable(args["対象キャラ-名前"]);
		const ids = convertVariable(args["対象キャラ-ID"]);
		const charas = [...getCharasByName(names), ...getCharasById(ids)];
		if (!charas || !charas.length) {
			return;
		}
		// 各パラメータを取得
		const loopWait = convertVariable(args["ループウェイト"]);
		const loopNum = randomise(convertVariable(args["ループ回数"]));
		// ループフキダシをセット
		charas.forEach((chara) => {
			// フキダシスプライトの消去
			deleteBalloonSprite(chara);
			chara._loopBalloonKe = { id: balloonId, wait: loopWait, num: loopNum };
		});
	});

	//- フキダシアイコンの消去
	PluginManager.registerCommand(pluginName, "フキダシアイコンの消去", (args) => {
		// 対象キャラを取得
		const names = convertVariable(args["対象キャラ-名前"]);
		const ids = convertVariable(args["対象キャラ-ID"]);
		const charas = [...getCharasByName(names), ...getCharasById(ids)];
		if (!charas || !charas.length) {
			return;
		}
		// ループフキダシを消去
		charas.forEach((chara) => {
			// フキダシスプライトの消去
			deleteBalloonSprite(chara);
			chara._loopBalloonKe = null;
			chara._loopBalloonCommentKe = null;
			// イベントのみフキダシ消去済みフラグをオン
			if (chara._eventId) {
				chara._loopBalloonDeletedKe = true;
			}
		});
	});

	//==================================================
	//--  ループフキダシ
	//==================================================

	//- ゲーム・キャラクターベース/更新(処理追加)
	const _Game_CharacterBase_update = Game_CharacterBase.prototype.update;
	Game_CharacterBase.prototype.update = function () {
		_Game_CharacterBase_update.apply(this);
		// ループフキダシの更新
		updateLoopBalloon(this);
	};

	//- ループフキダシの更新
	function updateLoopBalloon(chara) {
		// 注釈からのフキダシ取得
		getBalloonByComment(chara);
		const balloon = chara._loopBalloonKe || chara._loopBalloonCommentKe;
		// フキダシデータがなければリターン
		if (!balloon) {
			return;
		}
		// フキダシ表示中はリターン
		if (chara._balloonPlaying) {
			return;
		}
		// ウェイトを適用
		if (balloon.waitDuration) {
			balloon.waitDuration--;
			return;
		}
		// フキダシを表示
		$gameTemp.requestBalloon(chara, balloon.id);
		// ウェイトをセット
		balloon.waitDuration = randomise(balloon.wait);
	}

	//- 注釈からのフキダシ取得
	function getBalloonByComment(event) {
		// イベント以外はリターン
		if (!event._eventId) {
			return;
		}
		// 消去済みならリターン
		if (event._loopBalloonDeletedKe) {
			return;
		}
		// 前回のフキダシを保存
		const preBalloon = event._loopBalloonCommentKe;
		// ページ切り替わったら初期化
		initAtPageChange(event);
		// すでに取得済みならリターン
		if (event._loopBalloonCommentKe) {
			return;
		}
		// フキダシパラメータの取得
		const p = getBalloonParameter(event);
		if (p) {
			event._loopBalloonCommentKe = p;
		}
		// フキダシが変更されたら
		if (preBalloon && (!p || preBalloon.id != p.id)) {
			// フキダシスプライトの消去
			deleteBalloonSprite(event);
		}
	}

	//- ページが切り替わったら初期化
	function initAtPageChange(event) {
		// ページが切り替わったら
		if (event._currentPageKeLpbl != event._pageIndex) {
			// 注釈パラメータを初期化
			event._loopBalloonCommentKe = null;
		}
		// 現在のページを保存
		event._currentPageKeLpbl = event._pageIndex;
	}

	//- フキダシパラメータの取得
	function getBalloonParameter(event) {
		// 最初の注釈の取得
		const comment = getFirstComment(event);
		if (!comment) {
			return null;
		}
		var fukidasi_list = [
			"びっくり",
			"はてな",
			"びっくりはてな",
			"げんき",
			"おんぷ",
			"ほし",
			"ながれぼし",
			"きらきら",
			"はーと",
			"しつれん",
			"くちびる",
			"せきめん",
			"よだれ",
			"むっつり",
			"むかむか",
			"かじる",
			"ばくだん",
			"あせり",
			"がす",
			"ほらー",
			"ちんもく",
			"くしゃくしゃ",
			"ぐるぐる",
			"ねむい",
			"やるき",
			"ばくはつ",
			"ぴこーん",
			"いわ",
			"ざんぞう",
			"われる",
			"けつえき",
			"ばんそうこう",
			"どくろ",
			"うんこ",
			"こおり",
			"きえる",
			"かぜ",
			"あせ",
			"あたふた",
			"ひらめき",
			"きらり",
			"きらりきらり",
			"わるい",
			"ほのお",
			"おはな",
			"はなまる",
			"ぱっかーん",
			"まと",
			"あっぷ",
			"だうん",
			"enter",
			"shop",
			"upleft",
			"down",
			"check",
			"armour",
			"upright",
			"left",
			"hint",
			"weapon",
			"downright",
			"right",
			"event",
			"inn",
			"downleft",
			"up",
			"quest",
			"item",
			"gDown",
			"yajiDown",
			"mission",
			"start",
			"gLeft",
			"yajiLeft",
			"boss",
			"goal",
			"gRight",
			"yajiRight",
			"save",
			"next",
			"gUp",
			"yajiUp"
		];
		// 全ての注釈を処理
		const regExp = /<fukidasi_auto\s+@name\s*=\s*([^>]+)>/i;
		let p = null;
		// パラメータを検索
		while (true) {
			// マッチング
			const match = comment.match(regExp);
			if (!match || !match[1]) {
				break;
			}
			// パラメータを取得
			const descs = match[1].replace(/\s/g, "").split(",");
			p = {};
			p.id = Number(fukidasi_list.indexOf(descs[0]));
			p.wait = descs[1];
			p.num = randomise(descs[2]);
			break;
		}
		return p;
	}

	//- ゲーム・キャラクターベース/フキダシの終了(処理追加)
	const _Game_CharacterBase_endBalloon = Game_CharacterBase.prototype.endBalloon;
	Game_CharacterBase.prototype.endBalloon = function () {
		_Game_CharacterBase_endBalloon.apply(this);
		// フキダシの回数カウント
		countBalloonNum(this);
	};

	//- フキダシの回数カウント
	function countBalloonNum(chara) {
		const balloon = chara._loopBalloonKe;
		if (!balloon || !balloon.num) {
			return;
		}
		balloon.num--;
		// 終了
		if (balloon.num < 0) {
			chara._loopBalloonKe = null;
		}
	}

	Sprite_Balloon.prototype.update = function () {
		Sprite.prototype.update.call(this);
		if (this._duration > 0) {
			this._duration--;
			if (this._duration <= 0 && this._target._character && this._target._character._loopBalloonCommentKe) {
				this._duration = 8 * this.speed() + this.waitTime();
			}
			if (this._duration > 0) {
				this.updatePosition();
				this.updateFrame();
			}
		}
	};

	//- フキダシスプライトの消去
	function deleteBalloonSprite(chara) {
		const spriteset = SceneManager._scene._spriteset;
		const balloons = spriteset._balloonSprites;
		if (!balloons || !balloons.length) {
			return;
		}
		balloons.forEach((balloon) => {
			if (balloon.targetObject == chara) {
				spriteset.removeBalloon(balloon);
			}
		});
	}

	//- スブライトフキダシ/破棄(処理追加)
	const _Sprite_Balloon_destroy = Sprite_Balloon.prototype.destroy;
	Sprite_Balloon.prototype.destroy = function (options) {
		// フキダシスプライト破棄時のエラー防止
		if (!this._texture) {
			return;
		}
		_Sprite_Balloon_destroy.apply(this, arguments);
	};

	//- ゲームインタープリター/クリア(処理追加)
	const _Game_Interpreter_clear = Game_Interpreter.prototype.clear;
	Game_Interpreter.prototype.clear = function () {
		_Game_Interpreter_clear.apply(this);
		// イベントのフキダシ消去済みフラグをオフ
		if ($gameMap) {
			$gameMap.events().forEach((event) => (event ? (event._loopBalloonDeletedKe = null) : null));
		}
	};

	//==================================================
	//--  フキダシ位置調整
	//==================================================

	//- スブライトフキダシ/位置の更新(処理追加)
	const _Sprite_Balloon_updatePosition = Sprite_Balloon.prototype.updatePosition;
	Sprite_Balloon.prototype.updatePosition = function () {
		_Sprite_Balloon_updatePosition.apply(this);
		// キャラスプライトを取得(顔グラメイクプラグインに対応)
		const target = this._target._faceBaseSpriteKeFcmk ? this._target._faceBaseSpriteKeFcmk._faceSprite : this._target;
		// 位置の自動調整
		if (keke_positionAdjust) {
			this.y = this._target.y - target.height * target.scale.y + 8;
		}
		// サイズ自動調整
		if (keke_sizeAdjust) {
			const scale = target.scale.y > 1 ? 1 + (target.scale.y - 1) / 2 : 1;
			this.scale.x = scale;
			this.scale.y = scale;
		}
	};

	//==================================================
	//--  文字列基本 /ベーシック
	//==================================================

	// 文字列の数字リスト化
	function strToNumList(str) {
		const list = [];
		str = str.replace(/\[/g, "");
		str = str.replace(/\]/g, "");
		const strs = str.split(",");
		let s2 = null;
		for (let s of strs) {
			if (s.includes("~")) {
				s2 = s.split("~");
				s2 = s2.map((s) => Number(s));
				if (s2[1] >= s2[0]) {
					for (let i = s2[0]; i <= s2[1]; i++) {
						list.push(i);
					}
				} else {
					for (let i = s2[1]; i <= s2[0]; i++) {
						list.push(i);
					}
				}
			} else {
				list.push(Number(s));
			}
		}
		return list;
	}

	//- ランダマイズ
	function randomise(val) {
		if (typeof val != "string" || !val.includes("~")) {
			return val;
		}
		const vals = val.replace(/\s/g, "").split("~");
		const calcs = [Number(vals[0]), Number(vals[1])];
		calcs.sort((a, b) => a - b);
		return Math.round(calcs[0] + Math.random() * (calcs[1] - calcs[0]));
	}

	//- 変数の置換
	function convertVariable(str) {
		if (!str) {
			return str;
		}
		const regExp = /[\x1b\\]v\[(\d+)\]/gi;
		while (true) {
			const match = regExp.exec(str);
			if (!match) {
				break;
			}
			const val = $gameVariables.value(Number(match[1]));
			str = str.replace(match[0], val);
		}
		return str;
	}

	//==================================================
	//--  イベント基本 /ベーシック
	//==================================================

	//- 最初の注釈の取得
	function getFirstComment(event) {
		const comments = [];
		// 現在のページの最初の注釈を取得
		const currentPage = event.page();
		comments.push(getFirstComment_page(currentPage));

		//現在のページだけでいいので、他のページは不要
		// // 1ページの最初の注釈を取得
		// if (event._pageIndex != 0) {
		// 	const onePage = event.event().pages[0];
		// 	comments.push(getFirstComment_page(onePage));
		// }

		// 注釈を全て統合
		let comment = "";
		comments.forEach((cmt) => {
			if (!cmt) {
				return;
			}
			comment += cmt + "\n";
		});
		return comment;
	}

	//- 最初の注釈の取得-ページ
	function getFirstComment_page(page) {
		if (!page) {
			return null;
		}
		let list = page.list;
		if (!list[0] || list[0].code != 108) {
			return null;
		}
		// 最初の注釈を全て読み込み
		let comment = "";
		let i = 0;
		while (true) {
			const command = list[i];
			if (!command || !(command.code == 108 || command.code == 408)) {
				break;
			}
			comment += command.parameters[0] + "\n";
			i++;
		}
		return comment;
	}

	//==================================================
	//--  プラグインコマンド基本 /ベーシック
	//==================================================

	let PlcmPreter = null;

	//- プラグインコマンド呼び出しプリターを保存(処理追加)
	const _PluginManager_callCommand = PluginManager.callCommand;
	PluginManager.callCommand = function (self, pluginName, commandName, args) {
		PlcmPreter = self;
		_PluginManager_callCommand.apply(this, arguments);
		PlcmPreter = null;
	};

	//- プラグインコマンド呼び出しイベントを取得
	function getPlcmEvent() {
		return getPreterEvent(PlcmPreter);
	}

	//- インタープリターのイベント取得
	function getPreterEvent(preter) {
		if (!preter) {
			return null;
		}
		return preter.character(preter.eventId());
	}

	//- 名前でのキャラリスト取得
	function getCharasByName(names, self) {
		if (!names) {
			return [];
		}
		const nameList = names.replace(/\s/g, "").split(",");
		let charas = [];
		let match = null;
		for (const name of nameList) {
			// イベントを取得
			$gameMap.events().forEach((event) => {
				const note = event.event().name + " " + event.event().note;
				if (note.includes(name)) {
					charas.push(event);
				}
			});
			// セルフを取得
			if (name.match(/^(セルフ|自分|自身|self)$/)) {
				self = self || getPlcmEvent() || (charaSpritePlcm ? charaSpritePlcm._character : null) || $gamePlayer;
				if (self) {
					charas.push(self);
				}
			}
			// プレイヤーを取得
			if (name.match(/^(プレイヤー|操作キャラ|player)$/)) {
				charas = [...charas, $gamePlayer];
			}
			// フォロワーを取得
			match = name.match(/^(フォロワー|フォロアー|隊列|隊列キャラ|follower)(\d*)$/);
			if (match) {
				const id = match[2] ? Number(match[2]) - 1 : 0;
				charas = id != null ? [...charas, $gamePlayer._followers._data[id]] : [...charas, ...$gamePlayer._followers._data];
			}
			// パーティを取得
			if (name.match(/^(パーティ|味方|味方全員|味方全体|party)$/)) {
				charas = [...charas, $gamePlayer, ...$gamePlayer._followers._data];
			}
			// 乗り物を取得
			match = name.match(/^(乗り物|乗物|乗機|vehicle)(\d*)$/);
			if (match) {
				const id = match[2] ? Number(match[2]) - 1 : 0;
				charas = id ? [...charas, $gameMap._vehicles[id]] : [...charas, ...$gameMap._vehicles];
			}
			// 全て取得
			if (name.match(/^(全て|すべて|全部|全体|all)$/)) {
				charas = [...$gameMap.events(), $gamePlayer, ...$gamePlayer._followers._data, ...$gameMap._vehicles];
			}
			// 選択なし
			if (name.match(/^(なし|無し|none)$/)) {
			}
		}
		charas = charas.filter((chara) => chara);
		return charas;
	}

	//- IDでのキャラリスト取得
	function getCharasById(ids, self) {
		if (!ids) {
			return [];
		}
		const idList = !ids ? [] : strToNumList(ids.toString());
		let charas = [];
		for (const id of idList) {
			// イベントを取得
			if (id > 0) {
				charas.push($gameMap.event(id));
			}
			// セルフを取得
			if (id == 0) {
				self = self || getPlcmEvent() || (charaSpritePlcm ? charaSpritePlcm._character : null) || $gamePlayer;
				if (self && !idList.includes(self._eventId)) {
					charas.push(self);
				}
			}
			// プレイヤーを取得
			if (id == -1) {
				charas = [...charas, $gamePlayer];
			}
			// フォロワーを取得
			if (id <= -10 && id >= -99) {
				charas = id == -10 ? [...charas, ...$gamePlayer._followers._data] : [...charas, $gamePlayer._followers._data[Math.abs(id) - 11]];
			}
			// 乗り物を取得
			if (id <= -100) {
				charas = id == -100 ? [...charas, ...$gameMap._vehicles] : [...charas, $gameMap._vehicles[Math.abs(id) - 101]];
			}
			// 全て取得
			if (id == -2) {
				charas = [...$gameMap.events(), $gamePlayer, ...$gamePlayer._followers._data, ...$gameMap._vehicles];
			}
		}
		charas = charas.filter((chara) => chara);
		return charas;
	}
})();
