//============================================================================= // RPG Maker MZ - Extra Log Window // // 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 * @base TextResource * @orderAfter PluginCommonBase * @orderAfter DebugSwitch * @orderAfter TextResource * * @param logWaitVariable * @text ログ更新速度変数 * @desc ログの更新速度を変数に紐づける場合に指定できます。 * 指定しない場合でも速度はコマンドで変更可能です。 * @type variable * @default 0 * * @param windows * @text ウィンドウ * @desc 表示するウィンドウの設定 * @type struct[] * @default [] * * @command showWindow * @text ウィンドウの表示 * @desc ウィンドウの表示をします * * @arg windowName * @text 対象ウィンドウの名前 * @desc 表示対象のウィンドウを指定します。 * 指定しない場合現在のシーンの全てのウィンドウを表示します * @type string * @default * * @command hideWindow * @text ウィンドウの消去 * @desc ウィンドウの消去をします * * @arg windowName * @text 対象ウィンドウの名前 * @desc 表示対象のウィンドウを指定します。 * 指定しない場合現在のシーンの全てのウィンドウを消去します * @type string * @default * * @command addLog * @text ログの追加 * @desc ログを追加します。対象指定が無い場合共有ログに追加されます。 * * @arg windowName * @text 追加対象のウィンドウ名 * @desc 追加対象のウィンドウを指定します。 * 指定しない場合共有ログに追加されます。 * @type string * @default * * @arg label * @text ログ内容(ラベル) * @desc 追加するログのリソースラベルを指定します。 * @type string * @default * * @arg text * @text ログ内容 * @desc 追加するログの内容を指定します。 * @type string * @default * * @arg icon * @text アイコン * @desc 追加するログに表示するアイコンを指定します。 * @dir img/pictures * @type file * @default * * @command clearLog * @text ログのクリア * @desc ログをクリアします。対象指定が無い場合共有ログがクリアされます。 * * @arg windowName * @text 対象のウィンドウ名 * @desc 対象のウィンドウを指定します。 * 指定しない場合共有ログがクリアされます。 * @type string * @default * * @command setLogWaitSpeed * @text ログ更新速度設定 * @desc ログ表示時間を設定します。値を大きくすると早く消えます。 * 変数を紐づけている場合変数も更新されます。 * * @arg speed * @text ログ更新速度 * @desc ログの更新速度です。値を大きくすると早く消えます。 * @type number * @min 1 * @max 60 * @default 10 * * * @help * * LogWindow * Version: 0.0.3 * * シーンにログ用のウィンドウを追加します。 * */ /*~struct~WindowData: * * @param name * @text ウィンドウの名前 * @desc ウィンドウの名前です。個別に表示制御をしたいときの制御等に使用します。 * 指定しない場合、[シーン名_連番]になります。 * * @param targetScene * @text 対象シーン * @desc 追加対象のシーンです。オリジナルのシーンを対象にする場合はシーンクラス名を直接記入します。 * @type select * @default Scene_Title * @option タイトル * @value Scene_Title * @option マップ * @value Scene_Map * @option ゲームオーバー * @value Scene_Gameover * @option バトル * @value Scene_Battle * @option メインメニュー * @value Scene_Menu * @option アイテム * @value Scene_Item * @option スキル * @value Scene_Skill * @option 装備 * @value Scene_Equip * @option ステータス * @value Scene_Status * @option オプション * @value Scene_Options * @option セーブ * @value Scene_Save * @option ロード * @value Scene_Load * @option ゲーム終了 * @value Scene_End * @option ショップ * @value Scene_Shop * @option 名前入力 * @value Scene_Name * @option デバッグ * @value Scene_Debug * * @param logLineCount * @text 一度に表示できるログの数 * @desc 一度に表示可能なログの最大行です。 * これ以上のログは自動的に削除されます。 * ウィンドウの高さはこれにより自動決定されます。 * @type number * @default 20 * @min 1 * * @param windowWidth * @text ログウィンドウの幅 * @type number * @default 300 * @min 1 * * @param horizontalPos * @text 横位置 * @desc ウィンドウの横位置です。 * @type select * @default left * @option 左 * @value left * @option 中央 * @value center * @option 右 * @value right * * @param virticalPos * @text 縦位置 * @desc ウィンドウの縦位置です。 * @type select * @default top * @option 上 * @value top * @option 中央 * @value center * @option 下 * @value bottom * * @param offsetX * @text 横位置オフセット * @desc ウィンドウの横位置オフセットです。 * ウィンドウ位置を微調整したいときのみ使用して下さい。 * @type number * @default 0 * * @param offsetY * @text 縦位置オフセット * @desc ウィンドウの縦位置オフセットです。 * ウィンドウ位置を微調整したいときのみ使用して下さい。 * @type number * @default 0 * * @param backgroundColor * @text ログの背景色 * @desc 背景の色と透明度をRGBAのCSSカラーで指定します。 * デフォルトは透明です。 * @type string * @default #00000000 * * @param font * @text フォント * @desc 表示する文字のフォントです。 * 指定しない場合システムの標準フォントを使用します。 * @type file * @default * @dir fonts * * @param fontSize * @text 文字サイズ * @desc ウィンドウに表示する文字のサイズです。 * @type number * @default 18 * @min 8 * @max 128 * * @param fontStyle * @text 文字修飾 * @desc 表示する文字の修飾です。 * @type select * @option 通常 * @value normal * @oprion 太字 * @value bold * @option 斜体 * @value italic * */ (() => { "use strict"; const pluginName = "LogWindow"; const script = document.currentScript; const param = PluginManagerEx.createParameter(script); /******************************************************************************************************** * 共有基盤 ********************************************************************************************************/ const log = (text) => { if (DebugSwitch.isEnable()) { console.log(text); } }; //======================================================================== // 各シーンでのローディング用 //======================================================================== const _Scene_Base_create = Scene_Base.prototype.create; Scene_Base.prototype.create = function() { _Scene_Base_create.apply(this, arguments); const currentSceneName = PluginManagerEx.findClassName(this); if (!!$gameTemp && !!$gameTemp.logWindowController) { $gameTemp.logWindowController.buildWindows(currentSceneName); } }; const _Scene_Base_start = Scene_Base.prototype.start; Scene_Base.prototype.start = function() { _Scene_Base_start.apply(this, arguments); if (!!$gameTemp && !!$gameTemp.logWindowController) { const sceneName = PluginManagerEx.findClassName(this); Logger.d(`[log] add window: scene = ${sceneName}, windows = ${$gameTemp.logWindowController.getWindows(sceneName).length}`); $gameTemp.logWindowController.getWindows(sceneName).forEach(window => { Logger.d(`[log] add window: name = ${window.name()}`); this.addChild(window); }); } }; const _Scene_Base_prototype_stop = Scene_Base.prototype.stop; Scene_Base.prototype.stop = function() { if (!!$gameTemp && !!$gameTemp.logWindowController) { const sceneName = PluginManagerEx.findClassName(this); $gameTemp.logWindowController.getWindows(sceneName).forEach(window => { Logger.d(`[log] remove window: name = ${window.name()}`); this.removeChild(window); }); } _Scene_Base_prototype_stop.apply(this, arguments); }; //======================================================================== // ログウィンドウ管理クラスの準備 //======================================================================== const _Game_Temp_initialize = Game_Temp.prototype.initialize; Game_Temp.prototype.initialize = function() { _Game_Temp_initialize.apply(this, arguments); this.logWindowController = new LogWindowController(); }; const _Game_System_onBeforeSave = Game_System.prototype.onBeforeSave; Game_System.prototype.onBeforeSave = function() { _Game_System_onBeforeSave.apply(this, arguments); this._saveLogData = $gameTemp.logWindowController.buildSaveData(); }; const _Game_System_onAfterLoad = Game_System.prototype.onAfterLoad; Game_System.prototype.onAfterLoad = function() { _Game_System_onAfterLoad.apply(this, arguments); $gameTemp.logWindowController.applySaveData(this._saveLogData); }; //======================================================================== // ログウィンドウ管理クラス //======================================================================== class LogWindowController { constructor() { this._commonLogLines = []; this._windows = {}; this._logWaitSpeed = 5; this.visible = {}; this.windowItems = {}; this._scenes = {}; } buildSaveData() { return { commonLogLines: this._commonLogLines, logWaitSpeed: this._logWaitSpeed, visible: this.visible, windowItems: this.windowItems, }; } applySaveData(data) { this._commonLogLines = data.commonLogLines; this._logWaitSpeed = data.logWaitSpeed; this.visible = data.visible; this.windowItems = data.windowItems; } setLogWaitSpeed(value) { if (param.logWaitVariable) { $gameVariables.setValue(param.logWaitVariable, value); } this._logWaitSpeed = value; } getWaitSpeed() { if (param.logWaitVariable) { this._logWaitSpeed = $gameVariables.value().clamp(1, 60); } const multi = (ConfigManager.getItemValue?.('logSpeed') ?? 100) / 100; return Math.trunc(this._logWaitSpeed * 60 * multi); } getWindows(sceneName) { if (!!this._scenes[sceneName]) { return this._scenes[sceneName]; } else { return []; } } hasWindow(sceneName, windowName) { if (!this._scenes[sceneName]) { return this._scenes[sceneName].some(window => { return window.name() === windowName; }); } return false; } buildWindows(sceneName) { this._scenes[sceneName] = param.windows .map(p => p._parameter) .filter(p => p.targetScene === sceneName) .map(windowParam => { return this._createWindow(windowParam); }) .filter(window => !!window); } addWindow(sceneName, windowParam) { if (!!windowParam) { if (!this._scenes[sceneName]) { this._scenes[sceneName] = this.buildWindows(sceneName); } this._scenes[sceneName].push(this._createWindow(windowParam)); } } _createWindow(windowParam) { if (!!windowParam) { const count = Object.values(this._windows).filter(window => { return windowParam.targetScene === window.scene; }).length; if (!windowParam.name || windowParam.name.length === 0) { windowParam.name = windowParam.targetScene + '_' + count; } const window = new Window_ExtraLog(windowParam); this._windows[windowParam.name] = window; if (this.visible[windowParam.name] === false) { this.visible[windowParam.name] = false; window.hide(); } else { this.visible[windowParam.name] = true; window.visible = true; } return window; } return null; } findWindow(windowName) { return this._windows[windowName]; } getAllWindow() { return Object.values(this._windows); } addLog(label, icon, targetWindow) { this.addLogByText(TextResource.format(label), icon, targetWindow); } addLogByText(text, icon, targetWindow) { if (!!targetWindow && targetWindow.length > 0) { // ターゲット指定あり this._windows[targetWindow]?.addLog(text, icon); } else if (!!SceneManager._scene) { // 現シーンのすべてのウィンドウに this.getWindows(SceneManager._scene.constructor.name) .forEach(window => window.addLog(text, icon)); } } clearLog(targetWindow) { if (targetWindow && targetWindow.length > 0) { // ターゲット指定あり this._windows[targetWindow]?.clearLog(); } else { // ターゲット指定無し this._windows.forEach(window => window.clearLog()); } } getWindowParam(windowName) { return param.windows.map(p => p._parameter).find(p => p.name === windowName); } } const parseRgbaHexColorCode = (colorText) => { if (colorText.matchAll('^#[0-9A-F]{6}([0-9A-F]{2})?$')) { if (colorText.matchAll('^#[0-9A-F]{8}$')) { // RGBA構文 const color = colorText.substring(0, 7); const opacity = parseInt(colorText.substring(7), 16) return { color: color, opacity: opacity, } } else { // RGB構文 return { color: colorText, opacity: 255, }; } } }; //======================================================================== // ログウィンドウクラス //======================================================================== class Window_ExtraLog extends Window_Selectable { static LINE_HEIGHT = 44; static DesideWindowRect = (parameter) => { const lineHeight = Window_ExtraLog.LINE_HEIGHT; const width = parameter.windowWidth; const height = (lineHeight * (parameter.logLineCount + 1)); let x = 0; switch (parameter.horizontalPos) { case 'left': x = -8; break; case 'center': x = (Graphics.boxWidth - width) / 2; break; case 'right': x = (Graphics.boxWidth - width) + 8; break; } // switch x += parameter.offsetX; let y = 0; switch (parameter.verticalPos) { case 'top': y = -8; break; case 'center': y = (Graphics.boxHeight - height) / 2; break; case 'bottom': x = Graphics.boxHeight - height + 8; break; } // switch y += parameter.offsetY; return new Rectangle(x, y, width, height); } //======================================================================== // コンストラクタ //======================================================================== constructor(parameter) { super(Window_ExtraLog.DesideWindowRect(parameter)); const rect = Window_ExtraLog.DesideWindowRect(parameter); if (!$gameTemp.logWindowController.windowItems[parameter.name]) { $gameTemp.logWindowController.windowItems[parameter.name] = []; } this.opacity = 0; this.param = parameter; this._topIndex = 0; this._padding = 0; this._state = 'neutral'; this._offset = 0; this._logQueue = []; this._items = []; this._frameCount = 0; this.contents = new Bitmap(this.contentsWidth(), this.contentsHeight()); this.contentsBack = new Bitmap(rect.width, rect.height); this.zIndex = 50; } //======================================================================== // 追加メソッド //======================================================================== name() { return this.param.name; } scene() { return this.param.targetScene; } lineUpdateDuration() { return 15; } needsUpdate() { if (this._logQueue.length > 0) { return true; } return false; } addLog(text, icon) { log(`Log Added: text = ${text}`); const postLog = (iconPicture, line) => { this._logQueue.push({ text: line, opacity: 255, state: 'add', frame: 0, waitFrame: 0, offsetX: 0, offsetY: 0, icon: iconPicture, }); }; if (!!icon && icon.length > 0) { ImageManager.loadPictureAsync(icon).then(picture => { text.split('\n').reverse().forEach(line => { postLog(picture, line); }) }); } else { text.split('\n').reverse().forEach(line => { postLog(null, line); }) } } clearLog() { this._items.forEach(item => { item.state = 'remove'; }); } //======================================================================== // 親クラスの処理変更・抑止したい系 //======================================================================== setCursorRect(x, y, width, height) { // nothing to do } processOk() { // nothing to do } maxItems() { return this.param.logLineCount + 1; } drawItemBackground(index) { const backgroundColor = this.param.backgroundColor; const bitmap = this.contentsBack; const item = this.item(index); const rect = this.itemRect(index); const colorCode = parseRgbaHexColorCode(backgroundColor); const itemOpacity = !!item ? item.opacity : 0; bitmap.paintOpacity = Math.min(colorCode.opacity, itemOpacity); bitmap.fillRect(rect.x, rect.y, rect.width, rect.height, colorCode.color); bitmap.paintOpacity = 255; } itemRect(index) { const rect = super.itemRect(index); const item = this.item(index); if (!!item) { rect.x += item.offsetX; rect.y += item.offsetY; } return rect; } drawItem(index) { const item = this.item(index); if (!!item) { const rect = this.itemRect(index); const textMergin = 22; const bitmap = new Bitmap(rect.width - textMergin, rect.height); bitmap.fontSize = this.param.fontSize; this.contents.paintOpacity = item.opacity; if (!!item.icon) { const iw = item.icon.width; const ih = item.icon.height; const isize = rect.height - 4; bitmap.blt(item.icon, 0, 0, iw, ih, 0, 2, isize, isize); const textSx = isize + 4; bitmap.drawText(item.text, textSx, 0, rect.width - textMergin - textSx, rect.height, 'left'); } else { bitmap.drawText(item.text, 0, 0, rect.width - textMergin, rect.height, 'left'); } this.contents.blt(bitmap, 0, 0, rect.width, rect.height, rect.x + textMergin, rect.y - this._offset); bitmap.destroy(); } } isCursorMovable() { return false; } rowSpacing() { return 6; } lineHeight() { return Window_ExtraLog.LINE_HEIGHT; } item(index) { if (index < 0 || index >= this._items.length) { return null; } return this._items[index]; } itemPadding() { return 0; } updatePadding() { // nothing to do } //======================================================================== // フレーム毎更新処理 //======================================================================== update() { // if (!this.needsUpdate()) { // return; // } const lineHeight = this.lineHeight(); const duration = this.lineUpdateDuration(); const maxCount = this.param.logLineCount; const itemWidth = this.itemWidth(); const eraceDuration = Math.max($gameTemp.logWindowController.getWaitSpeed(), 5); if (this._state === 'neutral') { if (this._logQueue.length > 0) { const firstItem = this._logQueue.shift(); this._items.splice(0, 0, firstItem); this._state = 'ensure'; } } this._items.forEach((item, index) => { if (item.state === 'add') { if (this._state === 'ensure') { item.offsetX = ((itemWidth / duration) * item.frame) - itemWidth; if (item.frame > duration) { item.offsetX = 0; item.state = 'active'; item.waitFrame = 0; this._state = 'neutral'; } else { item.frame++; } } } else if (item.state === 'active' || item.state === 'remove') { if (this._state === 'ensure') { if (item.offsetY >= 0) { item.offsetY -= lineHeight; } item.offsetY += (lineHeight / duration); if (item.frame > duration) { item.offsetY = 0; } else { item.frame++; } } else if (this._state === 'neutral' && item.offsetY < 0) { item.offsetY = 0; } if (item.state === 'active') { item.waitFrame++; if (item.waitFrame >= eraceDuration) { item.state = 'remove'; item.waitFrame = 0; item.frame = 0; } if (index >= maxCount) { item.state = 'remove'; item.frame = 0; } } else { item.offsetY += (lineHeight / duration); item.opacity -= (256 / duration); if (item.opacity < 0) { item.opacity = 0; } if (item.frame > duration) { item.opacity = 0; item.state = 'removed'; } else { item.frame++ ; } } } }); this.refresh(); this._items = this._items.filter(item => (item.state !== 'removed')); } paint() { if (this.contents) { this.contents.clear(); this.contentsBack.clear(); this.drawAllItems(); } } } //======================================================================== // プラグインコマンド //======================================================================== // ウィンドウ名からウィンドウを検索 const findWindow = (target, callback) => { if ($gameTemp.logWindowController) { if (target && target.length > 0) { const window = $gameTemp.logWindowController.findWindow(target); if (window) { if (callback && typeof callback === 'function') { callback(window); } } } else { $gameTemp.logWindowController.getAllWindow().forEach(window => { if (callback && typeof callback === 'function') { callback(window); } }); } } }; PluginManagerEx.registerCommand(script, 'showWindow', args => { findWindow(args.windowName, window => { window.show(); $gameTemp.logWindowController.visible[args.windowName] = true; }); }); PluginManagerEx.registerCommand(script, 'hideWindow', args => { findWindow(args.windowName, window => { window.hide(); $gameTemp.logWindowController.visible[args.windowName] = false; }); }); PluginManagerEx.registerCommand(script, 'addLog', args => { if ($gameTemp.logWindowController) { if (!!args.label && args.label !== '') { $gameTemp.logWindowController.addLog(args.label, args.icon, args.windowName); } else { $gameTemp.logWindowController.addLogByText(args.text, args.icon, args.windowName); } } }); PluginManagerEx.registerCommand(script, 'clearLog', args => { findWindow(args.windowName, window => { $gameTemp.logWindowController.clearLog(window); }); }); PluginManagerEx.registerCommand(script, 'setLogWaitSpeed', args => { if ($gameTemp.logWindowController) { $gameTemp.logWindowController.setLogWaitSpeed(args.speed); } }); })();