//=============================================================================
// Ignideus777_BlockPuzzle.js
//=============================================================================

/*:
 * @target MZ
 * @plugindesc A feature-rich Block Puzzle minigame.
 * @author Ignideus777
 * @version 2.3.0
 *
 * @help
 * -Block puzzle minigame for RPG maker MZ.
 * -Uses only PIXI graphics.
 * -No image files needed
 * -Support Mouse and Keyboard.
 * -Can leave default for easy set up.
 * -Each minigame can be randomized.
 *
 * --- Controls And Gameplay ---
 * - Mouse: Hover to highlight matching blocks, Click to select.
 * - Keyboard: Use Arrow Keys to move, Enter/Z to select.
 * - Pause: Press X, Esc, or Cancel (NumPad 0).
 * - Quit: While paused, press CTRL to exit.
 *
 * --- Display ---
 * If the "Custom Background" parameter is left empty, the background will be
 * transparent, showing the map or scene underneath.
 *
 * --- Sound Effects ---
 * You can customize all sound effects in the plugin parameters. If a sound
 * is left blank, a default engine sound will be used.
 *
 * @param --- Board Settings ---
 *
 * @param defaultWidth
 * @text Default Board Width
 * @parent --- Board Settings ---
 * @type number @min 3 @default 10
 *
 * @param defaultHeight
 * @text Default Board Height
 * @parent --- Board Settings ---
 * @type number @min 3 @default 12
 *
 * @param --- General Settings ---
 *
 * @param defaultColors
 * @text Default Color Count
 * @parent --- General Settings ---
 * @type number @min 2 @max 15 @default 3
 *
 * @param defaultTimer
 * @text Default Timer (seconds)
 * @parent --- General Settings ---
 * @type number @min 10 @default 90
 *
 * @param scoreVariable
 * @text Score Variable
 * @parent --- General Settings ---
 * @type variable @default 1
 *
 * @param allClearSwitch
 * @text All Clear Switch
 * @parent --- General Settings ---
 * @desc Switch to turn ON when player clears all blocks
 * @type switch
 * @default 1 
 *
 *
 * @param backgroundImage
 * @text Background Image
 * @parent --- General Settings ---
 * @desc If blank, background will be transparent. Must png file type. 
 * @type file
 * @dir img/pictures/
 * @default
 *
 * @param --- Board Outline Settings ---
 *
 * @param enableOutline
 * @text Enable Board Outline
 * @parent --- Board Outline Settings ---
 * @desc If true, an outline will be drawn around the board.
 * @type boolean
 * @default true
 *
 * @param outlineThickness
 * @text Outline Thickness
 * @parent --- Board Outline Settings ---
 * @desc The thickness of the board outline in pixels.
 * @type number
 * @min 1
 * @default 3
 *
 * @param outlineColor
 * @text Outline Color
 * @parent --- Board Outline Settings ---
 * @desc The color of the outline in hex format (e.g., #CCCCCC).
 * @type string
 * @default #CCCCCC
 *
 * @param outlineAlpha
 * @text Outline Opacity
 * @parent --- Board Outline Settings ---
 * @desc The transparency of the outline. 0.0 is invisible, 1.0 is opaque.
 * @type number
 * @decimals 2
 * @min 0
 * @max 1
 * @default 1.0
 *
 * @param --- Sound Effects ---
 * @desc Set custom sound effects. If left blank, default sounds are used.
 *
 * @param seSelect @text Select SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Decision2
 * @param seBuzzer @text Buzzer SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Buzzer1
 * @param seCursor @text Cursor SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Cursor3
 * @param sePause @text Pause SE @parent --- Sound Effects --- @type file @dir audio/se/ @default
 * @param seQuit @text Quit SE @parent --- Sound Effects --- @type file @dir audio/se/ @default
 * @param seNoMoves @text No Moves SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Decision4
 * @param seAllClear @text All Clear SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Victory1
 *
 * @param --- Text & UI Settings ---
 * @desc Customize the text displayed in the game and their positions.
 *
 * @param textScoreLabel
 * @parent --- Text & UI Settings ---
 * @text "Score" Label
 * @desc The text for the "Score" label.
 * @type string
 * @default Score: 
 *
 * @param textScoreOffsetX
 * @parent --- Text & UI Settings ---
 * @text "Score" Label X Offset
 * @desc Horizontal offset for the Score text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textScoreOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Score" Label Y Offset
 * @desc Vertical offset for the Score text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeLabel
 * @parent --- Text & UI Settings ---
 * @text "Time" Label
 * @desc The text for the "Time" label.
 * @type string
 * @default Time: 
 *
 * @param textTimeOffsetX
 * @parent --- Text & UI Settings ---
 * @text "Time" Label X Offset
 * @desc Horizontal offset for the Time text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Time" Label Y Offset
 * @desc Vertical offset for the Time text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPaused
 * @parent --- Text & UI Settings ---
 * @text "Paused" Text
 * @desc The text shown when the game is paused.
 * @type string
 * @default Paused
 *
 * @param textPausedOffsetX
 * @parent --- Text & UI Settings ---
 * @text "Paused" Text X Offset
 * @desc Horizontal offset for the "Paused" text.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Paused" Text Y Offset
 * @desc Vertical offset for the "Paused" text.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExit
 * @parent --- Text & UI Settings ---
 * @text "Paused Exit" Text
 * @desc The exit instruction when paused.
 * @type string
 * @default Press CTRL to exit
 *
 * @param textPausedExitOffsetX
 * @parent --- Text & UI Settings ---
 * @text "Paused Exit" Text X Offset
 * @desc Horizontal offset for the "Paused Exit" text.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExitOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Paused Exit" Text Y Offset
 * @desc Vertical offset for the "Paused Exit" text.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndAllClear
 * @parent --- Text & UI Settings ---
 * @text "All Clear" Message
 * @desc Message shown when the board is cleared.
 * @type string
 * @default ALL CLEAR!
 *
 * @param textEndNoMoves
 * @parent --- Text & UI Settings ---
 * @text "No Moves" Message
 * @desc Message shown when there are no moves left.
 * @type string
 * @default No moves left!
 *
 * @param textEndTimeUp
 * @parent --- Text & UI Settings ---
 * @text "Time's Up" Message
 * @desc Message shown when the timer runs out.
 * @type string
 * @default Time's Up!
 *
 * @param textEndScoreLabel
 * @parent --- Text & UI Settings ---
 * @text "Final Score" Label
 * @desc The "Final Score" text on the end screen.
 * @type string
 * @default Final Score: 
 *
 * @param textEndContinue
 * @parent --- Text & UI Settings ---
 * @text "Continue" Prompt
 * @desc The prompt to continue on the end screen.
 * @type string
 * @default Press any to continue
 *
 * @param textEndGameOffsetX
 * @parent --- Text & UI Settings ---
 * @text End Game Text X Offset
 * @desc Horizontal offset for the entire end game text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndGameOffsetY
 * @parent --- Text & UI Settings ---
 * @text End Game Text Y Offset
 * @desc Vertical offset for the entire end game text block.
 * @type number
 * @min -9999
 * @default 0
 *
 * @command startFixedPuzzle
 * @text Start Fixed Puzzle
 * @desc Starts the puzzle with a fixed board size and color count.
 *
 * @arg width @text Board Width @type number @default 0 @min 0
 * @arg height @text Board Height @type number @default 0 @min 0
 * @arg colors @text Color Count @type number @default 0 @min 0 @max 10
 * @arg timer @text Timer (seconds) @type number @default 0 @min 0
 *
 * @command startRandomPuzzle
 * @text Start Random Puzzle
 * @desc Starts the puzzle with randomized settings.
 *
 * @arg minWidth @text Min Width @type number @default 6 @min 3
 * @arg maxWidth @text Max Width @type number @default 18 @min 3
 * @arg minHeight @text Min Height @type number @default 6 @min 3
 * @arg maxHeight @text Max Height @type number @default 14 @min 3
 * @arg minColors @text Min Colors @type number @default 2 @min 2 @max 15
 * @arg maxColors @text Max Colors @type number @default 5 @min 2 @max 15
 * @arg timer @text Timer (seconds) @type number @default 0 @min 0
 */
 
/*:ja
 * @target MZ
 * @plugindesc 機能豊富なブロックパズルミニゲームです。
 * @author Ignideus777
 * @version 2.3.1
 *
 * @help
 * - RPGツクールMZ用のブロックパズルミニゲームです。
 * - PIXIグラフィックスのみを使用しています。
 * - 画像ファイルは不要です。
 * - マウスとキーボード操作に対応しています。
 * - デフォルト設定のままでも簡単にセットアップできます。
 * - ゲームごとに設定をランダム化できます。
 *
 * --- 操作とゲームプレイ ---
 * - マウス: カーソルを合わせると同じ色のブロックがハイライトされ、クリックで選択します。
 * - キーボード: 矢印キーで移動し、決定/Zキーで選択します。
 * - ポーズ: Xキー、Escキー、またはキャンセルキー(テンキー0)を押します。
 * - 終了: ポーズ中にCtrlキーを押すと終了します。
 *
 * --- 表示 ---
 * 「背景画像」パラメータが空の場合、背景は透明になり、
 * 下のマップやシーンが表示されます。
 *
 * --- サウンドエフェクト ---
 * すべての効果音はプラグインパラメータでカスタマイズできます。
 * サウンドが空欄の場合、デフォルトのエンジンサウンドが使用されます。
 *
 * @param --- Board Settings ---
 * @text --- ボード設定 ---
 *
 * @param defaultWidth
 * @text デフォルトのボードの幅
 * @parent --- Board Settings ---
 * @type number @min 3 @default 10
 *
 * @param defaultHeight
 * @text デフォルトのボードの高さ
 * @parent --- Board Settings ---
 * @type number @min 3 @default 12
 *
 * @param --- General Settings ---
 * @text --- 基本設定 ---
 *
 * @param defaultColors
 * @text デフォルトの色数
 * @parent --- General Settings ---
 * @type number @min 2 @max 15 @default 3
 *
 * @param defaultTimer
 * @text デフォルトのタイマー(秒)
 * @parent --- General Settings ---
 * @type number @min 10 @default 90
 *
 * @param scoreVariable
 * @text スコアを格納する変数
 * @parent --- General Settings ---
 * @type variable @default 1
 *
 * @param allClearSwitch
 * @text 全消し時にONにするスイッチ
 * @parent --- General Settings ---
 * @desc プレイヤーが全ブロックを消した時にONにするスイッチです。
 * @type switch
 * @default 1 
 *
 *
 * @param backgroundImage
 * @text 背景画像
 * @parent --- General Settings ---
 * @desc 空欄の場合、背景は透明になります。png形式のファイルである必要があります。
 * @type file
 * @dir img/pictures/
 * @default
 *
 * @param --- Board Outline Settings ---
 * @text --- ボードの枠線設定 ---
 *
 * @param enableOutline
 * @text ボードの枠線を表示
 * @parent --- Board Outline Settings ---
 * @desc trueの場合、ボードの周りに枠線が描画されます。
 * @type boolean
 * @default true
 *
 * @param outlineThickness
 * @text 枠線の太さ
 * @parent --- Board Outline Settings ---
 * @desc ボードの枠線の太さをピクセル単位で指定します。
 * @type number
 * @min 1
 * @default 3
 *
 * @param outlineColor
 * @text 枠線の色
 * @parent --- Board Outline Settings ---
 * @desc 枠線の色を16進数カラーコードで指定します (例: #CCCCCC)。
 * @type string
 * @default #CCCCCC
 *
 * @param outlineAlpha
 * @text 枠線の不透明度
 * @parent --- Board Outline Settings ---
 * @desc 枠線の不透明度です。0.0で透明、1.0で不透明になります。
 * @type number
 * @decimals 2
 * @min 0
 * @max 1
 * @default 1.0
 *
 * @param --- Sound Effects ---
 * @text --- サウンドエフェクト ---
 * @desc カスタム効果音を設定します。空欄の場合、デフォルトのサウンドが使用されます。
 *
 * @param seSelect @text 選択SE @parent --- Sound Effects --- @type file @dir audio/se/ @default Decision2
 * @param seBuzzer @text ブザーSE @parent --- Sound Effects --- @type file @dir audio/se/ @default Buzzer1
 * @param seCursor @text カーソルSE @parent --- Sound Effects --- @type file @dir audio/se/ @default Cursor3
 * @param sePause @text ポーズSE @parent --- Sound Effects --- @type file @dir audio/se/ @default
 * @param seQuit @text 終了SE @parent --- Sound Effects --- @type file @dir audio/se/ @default
 * @param seNoMoves @text 手詰まりSE @parent --- Sound Effects --- @type file @dir audio/se/ @default Decision4
 * @param seAllClear @text 全消しSE @parent --- Sound Effects --- @type file @dir audio/se/ @default Victory1
 *
 * @param --- Text & UI Settings ---
 * @text --- テキストとUI設定 ---
 * @desc ゲーム内に表示されるテキストとその位置をカスタマイズします。
 *
 * @param textScoreLabel
 * @parent --- Text & UI Settings ---
 * @text 「スコア」ラベル
 * @desc 「スコア」のラベルテキスト。
 * @type string
 * @default スコア: 
 *
 * @param textScoreOffsetX
 * @parent --- Text & UI Settings ---
 * @text 「スコア」Xオフセット
 * @desc スコアテキストブロックの水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textScoreOffsetY
 * @parent --- Text & UI Settings ---
 * @text 「スコア」Yオフセット
 * @desc スコアテキストブロックの垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeLabel
 * @parent --- Text & UI Settings ---
 * @text 「時間」ラベル
 * @desc 「時間」のラベルテキスト。
 * @type string
 * @default 時間: 
 *
 * @param textTimeOffsetX
 * @parent --- Text & UI Settings ---
 * @text 「時間」Xオフセット
 * @desc 時間テキストブロックの水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeOffsetY
 * @parent --- Text & UI Settings ---
 * @text 「時間」Yオフセット
 * @desc 時間テキストブロックの垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPaused
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ」テキスト
 * @desc ゲームが一時停止したときに表示されるテキスト。
 * @type string
 * @default ポーズ
 *
 * @param textPausedOffsetX
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ」Xオフセット
 * @desc 「ポーズ」テキストの水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedOffsetY
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ」Yオフセット
 * @desc 「ポーズ」テキストの垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExit
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ中終了」テキスト
 * @desc 一時停止中の終了案内。
 * @type string
 * @default CTRLキーで終了
 *
 * @param textPausedExitOffsetX
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ中終了」Xオフセット
 * @desc 「ポーズ中終了」テキストの水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExitOffsetY
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ中終了」Yオフセット
 * @desc 「ポーズ中終了」テキストの垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndAllClear
 * @parent --- Text & UI Settings ---
 * @text 「全消し」メッセージ
 * @desc ボードを全消ししたときのメッセージ。
 * @type string
 * @default 全消し！
 *
 * @param textEndNoMoves
 * @parent --- Text & UI Settings ---
 * @text 「手詰まり」メッセージ
 * @desc 手詰まりになったときのメッセージ。
 * @type string
 * @default 手詰まりです！
 *
 * @param textEndTimeUp
 * @parent --- Text & UI Settings ---
 * @text 「時間切れ」メッセージ
 * @desc 時間切れになったときのメッセージ。
 * @type string
 * @default 時間切れ！
 *
 * @param textEndScoreLabel
 * @parent --- Text & UI Settings ---
 * @text 「最終スコア」ラベル
 * @desc 終了画面の「最終スコア」テキスト。
 * @type string
 * @default 最終スコア: 
 *
 * @param textEndContinue
 * @parent --- Text & UI Settings ---
 * @text 「続行」プロンプト
 * @desc 終了画面の続行プロンプト。
 * @type string
 * @default 何かキーを押して続行
 *
 * @param textEndGameOffsetX
 * @parent --- Text & UI Settings ---
 * @text 終了テキストXオフセット
 * @desc 終了画面テキストブロック全体の水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndGameOffsetY
 * @parent --- Text & UI Settings ---
 * @text 終了テキストYオフセット
 * @desc 終了画面テキストブロック全体の垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @command startFixedPuzzle
 * @text 固定パズル開始
 * @desc 固定されたボードサイズと色数でパズルを開始します。
 *
 * @arg width @text ボードの幅 @type number @default 0 @min 0
 * @arg height @text ボードの高さ @type number @default 0 @min 0
 * @arg colors @text 色数 @type number @default 0 @min 0 @max 10
 * @arg timer @text タイマー(秒) @type number @default 0 @min 0
 *
 * @command startRandomPuzzle
 * @text ランダムパズル開始
 * @desc ランダムな設定でパズルを開始します。
 *
 * @arg minWidth @text 最小幅 @type number @default 6 @min 3
 * @arg maxWidth @text 最大幅 @type number @default 18 @min 3
 * @arg minHeight @text 最小高さ @type number @default 6 @min 3
 * @arg maxHeight @text 最大高さ @type number @default 14 @min 3
 * @arg minColors @text 最小色数 @type number @default 2 @min 2 @max 15
 * @arg maxColors @text 最大色数 @type number @default 5 @min 2 @max 15
 * @arg timer @text タイマー(秒) @type number @default 0 @min 0
 */ 

/*:zh_CN
 * @target MZ
 * @plugindesc 一款功能丰富的方块消除小游戏。
 * @author Ignideus777
 * @version 2.3.0
 *
 * @help
 * - RPG Maker MZ 的方块消除小游戏插件。
 * - 仅使用 PIXI 图形渲染。
 * - 无需任何图像文件。
 * - 支持鼠标和键盘操作。
 * - 可保留默认设置以轻松启动。
 * - 每次小游戏都可以是随机的。
 *
 * --- 操作与玩法 ---
 * - 鼠标: 悬停高亮同色方块，点击选择。
 * - 键盘: 使用方向键移动，Enter/Z 选择。
 * - 暂停: 按 X、Esc 或 取消键 (小键盘 0)。
 * - 退出: 暂停时按 CTRL 退出。
 *
 * --- 显示 ---
 * 如果“背景图片”参数为空，背景将是透明的，会显示下方的地图或场景。
 *
 * --- 音效 ---
 * 您可以在插件参数中自定义所有音效。如果留空，将使用引擎的默认音效。
 *
 * @param --- 棋盘设置 ---
 *
 * @param defaultWidth
 * @text 默认棋盘宽度
 * @parent --- 棋盘设置 ---
 * @type number @min 3 @default 10
 *
 * @param defaultHeight
 * @text 默认棋盘高度
 * @parent --- 棋盘设置 ---
 * @type number @min 3 @default 12
 *
 * @param --- 通用设置 ---
 *
 * @param defaultColors
 * @text 默认颜色数量
 * @parent --- 通用设置 ---
 * @type number @min 2 @max 15 @default 3
 *
 * @param defaultTimer
 * @text 默认计时器 (秒)
 * @parent --- 通用设置 ---
 * @type number @min 10 @default 90
 *
 * @param scoreVariable
 * @text 分数变量
 * @parent --- 通用设置 ---
 * @type variable @default 1
 *
 * @param allClearSwitch
 * @text 全清开关
 * @parent --- 通用设置 ---
 * @desc 当玩家消除所有方块时，此开关将设为ON。
 * @type switch
 * @default 1 
 *
 *
 * @param backgroundImage
 * @text 背景图片
 * @parent --- 通用设置 ---
 * @desc 如果留空，背景将是透明的。必须是png格式。
 * @type file
 * @dir img/pictures/
 * @default
 *
 * @param --- 棋盘边框设置 ---
 *
 * @param enableOutline
 * @text 启用棋盘边框
 * @parent --- 棋盘边框设置 ---
 * @desc 如果为 true，将在棋盘周围绘制边框。
 * @type boolean
 * @default true
 *
 * @param outlineThickness
 * @text 边框粗细
 * @parent --- 棋盘边框设置 ---
 * @desc 边框的厚度（单位：像素）。
 * @type number
 * @min 1
 * @default 3
 *
 * @param outlineColor
 * @text 边框颜色
 * @parent --- 棋盘边框设置 ---
 * @desc 边框的颜色，使用十六进制格式（例如：#CCCCCC）。
 * @type string
 * @default #CCCCCC
 *
 * @param outlineAlpha
 * @text 边框不透明度
 * @parent --- 棋盘边框设置 ---
 * @desc 边框的透明度。0.0为完全透明，1.0为完全不透明。
 * @type number
 * @decimals 2
 * @min 0
 * @max 1
 * @default 1.0
 *
 * @param --- 音效 ---
 * @desc 设置自定义音效。如果留空，则使用默认音效。
 *
 * @param seSelect @text 选择 音效 @parent --- 音效 --- @type file @dir audio/se/ @default Decision2
 * @param seBuzzer @text 错误/超时 音效 @parent --- 音效 --- @type file @dir audio/se/ @default Buzzer1
 * @param seCursor @text 光标移动 音效 @parent --- 音效 --- @type file @dir audio/se/ @default Cursor3
 * @param sePause @text 暂停 音效 @parent --- 音效 --- @type file @dir audio/se/ @default
 * @param seQuit @text 退出 音效 @parent --- 音效 --- @type file @dir audio/se/ @default
 * @param seNoMoves @text 无解 音效 @parent --- 音效 --- @type file @dir audio/se/ @default Decision4
 * @param seAllClear @text 全清 音效 @parent --- 音效 --- @type file @dir audio/se/ @default Victory1
 *
 * @param --- 文本与UI设置 ---
 * @desc 自定义游戏中显示的文本及其位置。
 *
 * @param textScoreLabel
 * @parent --- 文本与UI设置 ---
 * @text “分数”标签
 * @desc “分数”标签的文本。
 * @type string
 * @default 分数: 
 *
 * @param textScoreOffsetX
 * @parent --- 文本与UI设置 ---
 * @text “分数”标签 X 偏移
 * @desc 分数文本块的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textScoreOffsetY
 * @parent --- 文本与UI设置 ---
 * @text “分数”标签 Y 偏移
 * @desc 分数文本块的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeLabel
 * @parent --- 文本与UI设置 ---
 * @text “时间”标签
 * @desc “时间”标签的文本。
 * @type string
 * @default 时间: 
 *
 * @param textTimeOffsetX
 * @parent --- 文本与UI设置 ---
 * @text “时间”标签 X 偏移
 * @desc 时间文本块的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeOffsetY
 * @parent --- 文本与UI设置 ---
 * @text “时间”标签 Y 偏移
 * @desc 时间文本块的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPaused
 * @parent --- 文本与UI设置 ---
 * @text “已暂停”文本
 * @desc 游戏暂停时显示的文本。
 * @type string
 * @default 已暂停
 *
 * @param textPausedOffsetX
 * @parent --- 文本与UI设置 ---
 * @text “已暂停”文本 X 偏移
 * @desc “已暂停”文本的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedOffsetY
 * @parent --- 文本与UI设置 ---
 * @text “已暂停”文本 Y 偏移
 * @desc “已暂停”文本的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExit
 * @parent --- 文本与UI设置 ---
 * @text “暂停时退出”提示文本
 * @desc 暂停时显示的退出说明。
 * @type string
 * @default 按 CTRL 退出
 *
 * @param textPausedExitOffsetX
 * @parent --- 文本与UI设置 ---
 * @text “暂停时退出”提示文本 X 偏移
 * @desc “暂停时退出”提示文本的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPausedExitOffsetY
 * @parent --- 文本与UI设置 ---
 * @text “暂停时退出”提示文本 Y 偏移
 * @desc “暂停时退出”提示文本的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndAllClear
 * @parent --- 文本与UI设置 ---
 * @text “全清”信息
 * @desc 消除所有方块后显示的信息。
 * @type string
 * @default 全部清除！
 *
 * @param textEndNoMoves
 * @parent --- 文本与UI设置 ---
 * @text “无解”信息
 * @desc 棋盘上没有可消除的方块时显示的信息。
 * @type string
 * @default 无路可走！
 *
 * @param textEndTimeUp
 * @parent --- 文本与UI设置 ---
 * @text “时间到”信息
 * @desc 计时器用尽时显示的信息。
 * @type string
 * @default 时间到！
 *
 * @param textEndScoreLabel
 * @parent --- 文本与UI设置 ---
 * @text “最终得分”标签
 * @desc 结束画面上的“最终得分”文本。
 * @type string
 * @default 最终得分: 
 *
 * @param textEndContinue
 * @parent --- 文本与UI设置 ---
 * @text “继续”提示
 * @desc 结束画面上的继续提示。
 * @type string
 * @default 按任意键继续
 *
 * @param textEndGameOffsetX
 * @parent --- 文本与UI设置 ---
 * @text 结束游戏文本 X 偏移
 * @desc 整个结束游戏文本块的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndGameOffsetY
 * @parent --- 文本与UI设置 ---
 * @text 结束游戏文本 Y 偏移
 * @desc 整个结束游戏文本块的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @command startFixedPuzzle
 * @text 开始固定谜题
 * @desc 使用固定的棋盘尺寸和颜色数量开始谜题。
 *
 * @arg width @text 棋盘宽度 @type number @default 0 @min 0
 * @arg height @text 棋盘高度 @type number @default 0 @min 0
 * @arg colors @text 颜色数量 @type number @default 0 @min 0 @max 10
 * @arg timer @text 计时器 (秒) @type number @default 0 @min 0
 *
 * @command startRandomPuzzle
 * @text 开始随机谜题
 * @desc 使用随机设置开始谜题。
 *
 * @arg minWidth @text 最小宽度 @type number @default 6 @min 3
 * @arg maxWidth @text 最大宽度 @type number @default 18 @min 3
 * @arg minHeight @text 最小高度 @type number @default 6 @min 3
 * @arg maxHeight @text 最大高度 @type number @default 14 @min 3
 * @arg minColors @text 最少颜色 @type number @default 2 @min 2 @max 15
 * @arg maxColors @text 最多颜色 @type number @default 5 @min 2 @max 15
 * @arg timer @text 计时器 (秒) @type number @default 0 @min 0
 */

(() => {
    const PLUGIN_NAME = "Ignideus777_BlockPuzzle";

    const params = PluginManager.parameters(PLUGIN_NAME);
    
    const parseLocalizableText = (paramString, fallback) => {
        const str = paramString || fallback;
        if (!str || !str.includes('#{')) {
            return str;
        }
        const locale = ($gameSystem ? $gameSystem.locale() : navigator.language).slice(0, 2);
        const entries = str.split('|');
        const langMap = new Map();
        let defaultText = '';

        for (const entry of entries) {
            const match = entry.match(/^(\w{2})#\{(.+?)\}$/);
            if (match) {
                const lang = match[1];
                const text = match[2];
                langMap.set(lang, text);
                if (!defaultText) defaultText = text;
            } else if (!defaultText) {
                defaultText = entry;
            }
        }
        return langMap.get(locale) || defaultText || str;
    };
    
    const uiTexts = {
        scoreLabel: parseLocalizableText(params.textScoreLabel, 'Score: '),
        scoreOffsetX: Number(params.textScoreOffsetX || 0),
        scoreOffsetY: Number(params.textScoreOffsetY || 0),
        timeLabel: parseLocalizableText(params.textTimeLabel, 'Time: '),
        timeOffsetX: Number(params.textTimeOffsetX || 0),
        timeOffsetY: Number(params.textTimeOffsetY || 0),
        paused: parseLocalizableText(params.textPaused, 'Paused'),
        pausedOffsetX: Number(params.textPausedOffsetX || 0),
        pausedOffsetY: Number(params.textPausedOffsetY || 0),
        pausedExit: parseLocalizableText(params.textPausedExit, 'Press CTRL to exit'),
        pausedExitOffsetX: Number(params.textPausedExitOffsetX || 0),
        pausedExitOffsetY: Number(params.textPausedExitOffsetY || 0),
        endAllClear: parseLocalizableText(params.textEndAllClear, 'ALL CLEAR!'),
        endNoMoves: parseLocalizableText(params.textEndNoMoves, 'No moves left!'),
        endTimeUp: parseLocalizableText(params.textEndTimeUp, "Time's Up!"),
        endScoreLabel: parseLocalizableText(params.textEndScoreLabel, 'Final Score: '),
        endContinue: parseLocalizableText(params.textEndContinue, 'Press any to continue'),
        endGameOffsetX: Number(params.textEndGameOffsetX || 0),
        endGameOffsetY: Number(params.textEndGameOffsetY || 0),
    };

    // ... (The rest of the Block Puzzle plugin code remains unchanged)
    const backImage = params.backgroundImage;
    const defaultTimer = Number(params.defaultTimer) || 90;
    const defaultWidth = Number(params.defaultWidth) || 10;
    const defaultHeight = Number(params.defaultHeight) || 12;
    const defaultColors = Number(params.defaultColors) || 6;
    const scoreVariableId = Number(params.scoreVariable) || 1;
	const allClearSwitchId = Number(params.allClearSwitch) || 1;

    const BoardOutline = {
        enabled: params['enableOutline'] === 'true',
        thickness: Number(params['outlineThickness'] || 3),
        color: PIXI.utils.string2hex(params['outlineColor'] || '#CCCCCC'),
        alpha: Number(params['outlineAlpha'] || 1.0)
    };

    const SoundSettings = {
        select: params.seSelect || '', buzzer: params.seBuzzer || '',
        cursor: params.seCursor || '', pause: params.sePause || '',
        quit: params.seQuit || '', noMoves: params.seNoMoves || '',
        allClear: params.seAllClear || ''
    };

    const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

    const playCustomSound = (customSeName, fallbackSeFunction) => {
        if (customSeName) {
            AudioManager.playSe({ name: customSeName, volume: 90, pitch: 100, pan: 0 });
        } else if (fallbackSeFunction) {
            fallbackSeFunction.call(SoundManager);
        }
    };

    PluginManager.registerCommand(PLUGIN_NAME, "startFixedPuzzle", function(args) {
        const gameParams = {
            width: Number(args.width) || defaultWidth,
            height: Number(args.height) || defaultHeight,
            colors: Number(args.colors) || defaultColors,
            timer: Number(args.timer) || defaultTimer
        };
        SceneManager.push(Scene_BlockPuzzle);
        SceneManager.prepareNextScene(gameParams);
    });

    PluginManager.registerCommand(PLUGIN_NAME, "startRandomPuzzle", function(args) {
        const minW = Number(args.minWidth) || defaultWidth; const maxW = Number(args.maxWidth) || minW;
        const minH = Number(args.minHeight) || defaultHeight; const maxH = Number(args.maxHeight) || minH;
        const minC = Number(args.minColors) || defaultColors; const maxC = Number(args.maxColors) || minC;
        const gameParams = {
            width: randomInt(minW, maxW), height: randomInt(minH, maxH),
            colors: randomInt(minC, maxC), timer: Number(args.timer) || defaultTimer
        };
        SceneManager.push(Scene_BlockPuzzle);
        SceneManager.prepareNextScene(gameParams);
    });

    const BLOCK_SIZE = 36;
    const BLOCK_SPACING = 2;
    const COLORS = [0xff6666, 0x66ff66, 0x6666ff, 0xffff66, 0xff66ff, 0x66ffff, 0xffa500, 0x8a2be2, 0xa9a9a9, 0x8b4513];
    const BORDER_PADDING = 10;
    const BOARD_BG_COLOR = 0x000000;
    
    class Scene_BlockPuzzle extends Scene_Base {
        
        prepare(params) { this._gameParams = params; }

        create() {
            super.create();
            this.createBackground(); this.createBoardBorder(); this.createGameContainer();
            this.createParticleContainer(); this.createUiContainer(); this.createCursor();
            this.startGame();
        }

        startGame() {
            this._boardWidth = this._gameParams.width; this._boardHeight = this._gameParams.height;
            this._numColors = this._gameParams.colors; this._activeColors = COLORS.slice(0, this._numColors);
            this._timer = this._gameParams.timer * 60; this._score = 0;
            this._gameActive = true; this._isPaused = false; this._isProcessing = false;
            this._board = []; this._highlightedBlocks = [];
            this._cursorX = 0; this._cursorY = 0; this._mouseMode = false;
            this._lastHoveredX = -1; this._lastHoveredY = -1;
            this.setupBoard(); this.updateScoreText(); this.updateTimerText();
            this.updateCursorPosition(0, 0); this.setupMouseEvents();
        }

createBackground() {
    this._backgroundSprite = new Sprite();
    if (backImage && backImage.trim() !== '') {
        this._backgroundSprite.bitmap = ImageManager.loadPicture(backImage);
    } else {
        this._backgroundSprite.bitmap = SceneManager.backgroundBitmap();
    }
    this.addChild(this._backgroundSprite);
}
        
        createBoardBorder() {
            const totalBlockWidth = this._gameParams.width * (BLOCK_SIZE + BLOCK_SPACING);
            const totalBlockHeight = this._gameParams.height * (BLOCK_SIZE + BLOCK_SPACING);
            const borderWidth = totalBlockWidth + BORDER_PADDING * 2 - BLOCK_SPACING;
            const borderHeight = totalBlockHeight + BORDER_PADDING * 2 - BLOCK_SPACING;
            this._boardBorder = new PIXI.Graphics();
            this._boardBorder.x = (Graphics.width - borderWidth) / 2;
            this._boardBorder.y = (Graphics.height - borderHeight) / 2 + 30;
            this._boardBorder.beginFill(BOARD_BG_COLOR, 0.5);
            if (BoardOutline.enabled) {
                this._boardBorder.lineStyle(BoardOutline.thickness, BoardOutline.color, BoardOutline.alpha);
            }
            this._boardBorder.drawRoundedRect(0, 0, borderWidth, borderHeight, 10);
            this._boardBorder.endFill();
            this.addChild(this._boardBorder);
        }

        createGameContainer() {
            this._gameContainer = new PIXI.Container();
            this._gameContainer.x = this._boardBorder.x + BORDER_PADDING;
            this._gameContainer.y = this._boardBorder.y + BORDER_PADDING;
            this.addChild(this._gameContainer);
        }

        createParticleContainer() {
            this._particleContainer = new PIXI.Container();
            this.addChild(this._particleContainer);
        }

        createUiContainer() {
            this._uiContainer = new PIXI.Container();
            this.addChild(this._uiContainer);

            const style = new PIXI.TextStyle({ fontFamily: "GameFont", fontSize: 32, fill: "#ffffff", stroke: "#000000", strokeThickness: 4 });
            
            this._scoreLabel = new PIXI.Text(uiTexts.scoreLabel, style);
            this._scoreLabel.x = 20 + uiTexts.scoreOffsetX;
            this._scoreLabel.y = 20 + uiTexts.scoreOffsetY;
            this._uiContainer.addChild(this._scoreLabel);

            this._scoreValue = new PIXI.Text("0", style);
            this._scoreValue.x = this._scoreLabel.x + this._scoreLabel.width;
            this._scoreValue.y = this._scoreLabel.y;
            this._uiContainer.addChild(this._scoreValue);

            this._timerLabel = new PIXI.Text(uiTexts.timeLabel, style);
            this._timerLabel.y = 20 + uiTexts.timeOffsetY;
            this._uiContainer.addChild(this._timerLabel);

            this._timerValue = new PIXI.Text("0", style);
            this._timerValue.y = this._timerLabel.y;
            this._uiContainer.addChild(this._timerValue);
            
            // Position timer block at the right edge
            const timerTotalWidth = this._timerLabel.width + this._timerValue.width;
            this._timerLabel.x = Graphics.width - 20 - timerTotalWidth - uiTexts.timeOffsetX;
            this._timerValue.x = this._timerLabel.x + this._timerLabel.width;

            // Pause Screen
            this._pauseOverlay = new PIXI.Graphics();
            this._pauseOverlay.beginFill(0x000000, 0.7).drawRect(0, 0, Graphics.width, Graphics.height).endFill();
            this._pauseOverlay.visible = false; this._uiContainer.addChild(this._pauseOverlay);

            const pauseStyle = new PIXI.TextStyle({ fontFamily: "GameFont", fontSize: 72, fill: "white", align: "center", stroke: "black", strokeThickness: 8 });
            this._pauseText = new PIXI.Text(uiTexts.paused, pauseStyle);
            this._pauseText.anchor.set(0.5);
            this._pauseText.x = Graphics.width / 2 + uiTexts.pausedOffsetX;
            this._pauseText.y = Graphics.height / 2 - 40 + uiTexts.pausedOffsetY;
            this._pauseText.visible = false; this._uiContainer.addChild(this._pauseText);

            const exitStyle = new PIXI.TextStyle({ fontFamily: "GameFont", fontSize: 32, fill: "#cccccc", align: "center", stroke: "black", strokeThickness: 4 });
            this._pauseExitText = new PIXI.Text(uiTexts.pausedExit, exitStyle);
            this._pauseExitText.anchor.set(0.5);
            this._pauseExitText.x = Graphics.width / 2 + uiTexts.pausedExitOffsetX;
            this._pauseExitText.y = this._pauseText.y + 80 + uiTexts.pausedExitOffsetY;
            this._pauseExitText.visible = false; this._uiContainer.addChild(this._pauseExitText);
        }

        createCursor() {
            this._cursor = new PIXI.Graphics();
            this._cursor.lineStyle(4, 0xFFFFFF, 1);
            this._cursor.drawRect(-2, -2, BLOCK_SIZE + 4, BLOCK_SIZE + 4);
            this._gameContainer.addChild(this._cursor);
        }

        setupBoard() {
            for (let x = 0; x < this._boardWidth; x++) {
                this._board[x] = [];
                for (let y = 0; y < this._boardHeight; y++) this.createBlock(x, y);
            }
        }

        createBlock(x, y) {
            const colorIndex = Math.floor(Math.random() * this._activeColors.length);
            const block = new PIXI.Graphics(); this.drawBlock(block, colorIndex, false);
            block.x = x * (BLOCK_SIZE + BLOCK_SPACING); block.y = y * (BLOCK_SIZE + BLOCK_SPACING);
            block.gridX = x; block.gridY = y; block.colorIndex = colorIndex; block.isHighlighted = false;
            this._board[x][y] = block; this._gameContainer.addChild(block);
        }

        drawBlock(block, colorIndex, isHighlighted) {
            block.clear();
            const color = this._activeColors[colorIndex];
            if (isHighlighted) {
                block.beginFill(this.brightenColor(color, 0.3)); block.lineStyle(3, 0xFFFFFF, 0.8);
            } else {
                block.beginFill(color); block.lineStyle(2, 0x000000, 0.3);
            }
            block.drawRoundedRect(0, 0, BLOCK_SIZE, BLOCK_SIZE, 5); block.endFill();
        }

        brightenColor(color, amount) {
            const r = Math.min(255, ((color >> 16) & 0xFF) + (255 * amount));
            const g = Math.min(255, ((color >> 8) & 0xFF) + (255 * amount));
            const b = Math.min(255, (color & 0xFF) + (255 * amount));
            return (r << 16) | (g << 8) | b;
        }

        setupMouseEvents() {
            this._gameContainer.interactive = true;
            this._gameContainer.hitArea = new PIXI.Rectangle(0, 0, this._boardWidth * (BLOCK_SIZE + BLOCK_SPACING), this._boardHeight * (BLOCK_SIZE + BLOCK_SPACING));
            this._gameContainer.on('pointermove', (event) => {
                if (this._isPaused || this._isProcessing || !this._gameActive) return;
                const localPos = event.data.getLocalPosition(this._gameContainer);
                const gridX = Math.floor(localPos.x / (BLOCK_SIZE + BLOCK_SPACING)); const gridY = Math.floor(localPos.y / (BLOCK_SIZE + BLOCK_SPACING));
                if (gridX >= 0 && gridX < this._boardWidth && gridY >= 0 && gridY < this._boardHeight) {
                    if (gridX !== this._lastHoveredX || gridY !== this._lastHoveredY) {
                        this._mouseMode = true; this._lastHoveredX = gridX; this._lastHoveredY = gridY;
                        this.onBlockHover(gridX, gridY);
                    }
                } else {
                    if (this._lastHoveredX !== -1 || this._lastHoveredY !== -1) {
                        this._lastHoveredX = -1; this._lastHoveredY = -1; this.clearHighlights();
                    }
                }
            });
            this._gameContainer.on('pointerout', () => { this._lastHoveredX = -1; this._lastHoveredY = -1; this.clearHighlights(); });
        }

        onBlockHover(x, y) {
            if (this._isPaused || this._isProcessing || !this._gameActive) return;
            if (!this._board[x] || !this._board[x][y]) return;
            this._mouseMode = true; this.updateCursorPosition(x, y); this.clearHighlights();
            const connectedBlocks = this.findConnectedBlocks(x, y);
            if (connectedBlocks.length >= 3) {
                this._highlightedBlocks = connectedBlocks;
                for (const block of connectedBlocks) { block.isHighlighted = true; this.drawBlock(block, block.colorIndex, true); }
            }
        }

        clearHighlights() {
            for (const block of this._highlightedBlocks) {
                if (block && block.parent) { block.isHighlighted = false; this.drawBlock(block, block.colorIndex, false); }
            }
            this._highlightedBlocks = [];
        }

        update() {
            super.update(); this.updateParticles();
            if (Input.isTriggered('cancel')) { this.togglePause(); }
            if (this._isPaused) { if (this.isQuitTriggered()) this.quitGame(); return; }
            if (!this._gameActive) return;
            if (!this._isProcessing) { this.handleKeyboardInput(); this.handleMouseInput(); }
            this.updateTimer();
        }

        updateParticles() {
            for (let i = this._particleContainer.children.length - 1; i >= 0; i--) {
                const p = this._particleContainer.children[i];
                p.x += p.vx; p.y += p.vy; p.life--; p.alpha = Math.min(1, p.life / 60);
                if (p.life <= 0) this._particleContainer.removeChild(p);
            }
        }
        
        handleKeyboardInput() {
            this._mouseMode = false;
            let moved = false;
            if (Input.isRepeated('down'))  { this.moveCursor('down'); moved = true; }
            if (Input.isRepeated('up'))    { this.moveCursor('up'); moved = true; }
            if (Input.isRepeated('right')) { this.moveCursor('right'); moved = true; }
            if (Input.isRepeated('left'))  { this.moveCursor('left'); moved = true; }
            if (moved) {
                playCustomSound(SoundSettings.cursor, SoundManager.playCursor);
                this.updateCursorPosition(this._cursorX, this._cursorY);
                this.onBlockHover(this._cursorX, this._cursorY);
            }
            if (Input.isTriggered('ok')) { this.onBlockSelect(this._cursorX, this._cursorY); }
        }

        moveCursor(direction) {
            const {x, y} = this.findNextBlock(this._cursorX, this._cursorY, direction);
            this._cursorX = x;
            this._cursorY = y;
        }

        findNextBlock(startX, startY, direction) {
            let x = startX;
            let y = startY;
            switch (direction) {
                case 'right': for (let i = x + 1; i < this._boardWidth; i++) { if (this._board[i][y]) return { x: i, y: y }; } break;
                case 'left': for (let i = x - 1; i >= 0; i--) { if (this._board[i][y]) return { x: i, y: y }; } break;
                case 'down': for (let i = y + 1; i < this._boardHeight; i++) { if (this._board[x][i]) return { x: x, y: i }; } break;
                case 'up': for (let i = y - 1; i >= 0; i--) { if (this._board[x][i]) return { x: x, y: i }; } break;
            }
            return { x: startX, y: startY }; // No move
        }

        handleMouseInput() {
            if (TouchInput.isTriggered()) {
                const touchPos = new PIXI.Point(TouchInput.x, TouchInput.y);
                const localPos = this._gameContainer.toLocal(touchPos);
                const gridX = Math.floor(localPos.x / (BLOCK_SIZE + BLOCK_SPACING));
                const gridY = Math.floor(localPos.y / (BLOCK_SIZE + BLOCK_SPACING));
                if (gridX >= 0 && gridX < this._boardWidth && gridY >= 0 && gridY < this._boardHeight) {
                    if (this._board[gridX] && this._board[gridX][gridY]) this.onBlockSelect(gridX, gridY);
                }
            }
        }

        togglePause() {
            if (!this._gameActive) return;
            this._isPaused = !this._isPaused;
            this._pauseOverlay.visible = this._isPaused; this._pauseText.visible = this._isPaused;
            this._pauseExitText.visible = this._isPaused;
            if (this._isPaused) { this.clearHighlights(); this._lastHoveredX = -1; this._lastHoveredY = -1; }
            playCustomSound(SoundSettings.pause, SoundManager.playOk);
        }

        isQuitTriggered() { return Input.isPressed('control'); }

        quitGame() {
            if (!this._gameActive) return; this._gameActive = false;
            playCustomSound(SoundSettings.quit, SoundManager.playCancel);
            $gameVariables.setValue(scoreVariableId, this._score); SceneManager.pop();
        }
        
        updateCursorPosition(x, y) {
            if (x >= 0 && x < this._boardWidth && y >= 0 && y < this._boardHeight) {
                this._cursorX = x; this._cursorY = y;
                this._cursor.x = x * (BLOCK_SIZE + BLOCK_SPACING);
                this._cursor.y = y * (BLOCK_SIZE + BLOCK_SPACING);
                this._cursor.visible = !!(this._board[x] && this._board[x][y]);
            } else { this._cursor.visible = false; }
        }

        onBlockSelect(x, y) {
            if (this._isProcessing || !(this._board[x] && this._board[x][y])) return;
            const group = this.findConnectedBlocks(x, y);
            if (group.length >= 3) {
                this._isProcessing = true;
                playCustomSound(SoundSettings.select, SoundManager.playOk);
                this.clearHighlights(); this.removeBlocks(group); this.applyGravity(); this.shiftColumns();
                this.revalidateCursorPosition();
                
                if (this._boardWidth === 0) {
                    this._score = Math.round(this._score * 1.5); 
                    this.updateScoreText();
                    this.createAllClearEffect(100); 
                    playCustomSound(SoundSettings.allClear, SoundManager.playVictory);
                    $gameSwitches.setValue(allClearSwitchId, true);
                    this.endGame('allClear'); 
                    return;
                }
                
                if (this.isGameFinished()) {
                    playCustomSound(SoundSettings.noMoves, SoundManager.playDefeat);
                    this.endGame('noMoves');
                }
                this._isProcessing = false;
            } else { playCustomSound(SoundSettings.buzzer, SoundManager.playBuzzer); }
        }

        revalidateCursorPosition() {
            if (this._boardWidth === 0) { this.updateCursorPosition(0, 0); return; }
            this._cursorX = Math.min(this._cursorX, this._boardWidth - 1);
            if (this._board[this._cursorX] && this._board[this._cursorX][this._cursorY]) return;

            for (let y = this._cursorY; y < this._boardHeight; y++) {
                if (this._board[this._cursorX] && this._board[this._cursorX][y]) {
                    this._cursorY = y; this.updateCursorPosition(this._cursorX, this._cursorY); return;
                }
            }
            for (let y = this._cursorY - 1; y >= 0; y--) {
                if (this._board[this._cursorX] && this._board[this._cursorX][y]) {
                    this._cursorY = y; this.updateCursorPosition(this._cursorX, this._cursorY); return;
                }
            }
            for (let x = 0; x < this._boardWidth; x++) for (let y = 0; y < this._boardHeight; y++) {
                if (this._board[x] && this._board[x][y]) {
                    this._cursorX = x; this._cursorY = y; this.updateCursorPosition(this._cursorX, this._cursorY); return;
                }
            }
        }

        updateTimer() {
            if (this._timer > 0) {
                this._timer--; this.updateTimerText();
            } else if (this._gameActive) { this.endGame('timeUp'); }
        }

        updateScoreText() {
            this._scoreValue.text = `${this._score}`;
            this._scoreValue.x = this._scoreLabel.x + this._scoreLabel.width;
        }
        updateTimerText() {
            const time = `${Math.ceil(this._timer / 60)}`;
            this._timerValue.text = time;
            const timerTotalWidth = this._timerLabel.width + this._timerValue.width;
            this._timerLabel.x = Graphics.width - 20 - timerTotalWidth + uiTexts.timeOffsetX;
            this._timerValue.x = this._timerLabel.x + this._timerLabel.width;
        }
        
        findConnectedBlocks(startX, startY) {
            const targetBlock = this._board[startX] && this._board[startX][startY];
            if (!targetBlock) return []; const targetColor = targetBlock.colorIndex;
            const queue = [{x: startX, y: startY}]; const group = []; const visited = new Set([`${startX},${startY}`]);
            while (queue.length > 0) {
                const {x, y} = queue.shift(); group.push(this._board[x][y]);
                const neighbors = [{x: x, y: y-1}, {x: x, y: y+1}, {x: x-1, y: y}, {x: x+1, y: y}];
                for (const n of neighbors) {
                    if (this._board[n.x] && this._board[n.x][n.y] && !visited.has(`${n.x},${n.y}`)) {
                        visited.add(`${n.x},${n.y}`); const neighborBlock = this._board[n.x][n.y];
                        if (neighborBlock.colorIndex === targetColor) queue.push({x: n.x, y: n.y});
                    }
                }
            }
            return group;
        }

        removeBlocks(group) {
            let points = group.length * 10 + (group.length - 3) * group.length * 5;
            this._score += points; this.updateScoreText();
            for (const block of group) { this._gameContainer.removeChild(block); this._board[block.gridX][block.gridY] = null; }
        }

        applyGravity() {
            for (let x = 0; x < this._boardWidth; x++) {
                let emptySlots = 0;
                for (let y = this._boardHeight - 1; y >= 0; y--) {
                    if (this._board[x][y] === null) { emptySlots++; }
                     else if (emptySlots > 0) {
                        const block = this._board[x][y];
                        this._board[x][y + emptySlots] = block;
                        block.gridY += emptySlots; block.y = block.gridY * (BLOCK_SIZE + BLOCK_SPACING);
                        this._board[x][y] = null;
                    }
                }
            }
        }
        
        shiftColumns() {
            const newBoard = []; let newWidth = 0;
            for (let x = 0; x < this._boardWidth; x++) {
                if (this._board[x].some(b => b !== null)) {
                    newBoard[newWidth] = [];
                    for (let y = 0; y < this._boardHeight; y++) {
                        const block = this._board[x][y];
                        if (block) { block.gridX = newWidth; block.x = newWidth * (BLOCK_SIZE + BLOCK_SPACING); }
                        newBoard[newWidth][y] = block;
                    }
                    newWidth++;
                }
            }
            this._board = newBoard; this._boardWidth = newWidth;
        }
        
        isGameFinished() {
            if (this._boardWidth === 0) return true;
            for (let x = 0; x < this._boardWidth; x++) for (let y = 0; y < this._boardHeight; y++) {
                if (this._board[x] && this._board[x][y] && this.findConnectedBlocks(x, y).length >= 3) return false;
            }
            return true;
        }

        createAllClearEffect(count) {
            for (let i = 0; i < count; i++) {
                const p = new PIXI.Graphics();
                p.beginFill(0xFFFF00, Math.random() * 0.5 + 0.5).drawCircle(0, 0, Math.random() * 2 + 1).endFill();
                p.x = Graphics.width / 2; p.y = Graphics.height / 2;
                p.vx = (Math.random() - 0.5) * (Math.random() * 8); p.vy = (Math.random() - 0.5) * (Math.random() * 8);
                p.life = 90 + Math.random() * 90; this._particleContainer.addChild(p);
            }
        }

        endGame(reasonKey) {
            if (!this._gameActive) return;
            this._gameActive = false; 
            $gameVariables.setValue(scoreVariableId, this._score);
            
            const overlay = new PIXI.Graphics();
            overlay.beginFill(0x000000, 0.7).drawRect(0, 0, Graphics.width, Graphics.height).endFill(); 
            this._uiContainer.addChild(overlay);
            
            const style = new PIXI.TextStyle({ 
                fontFamily: "GameFont", fontSize: 54, fill: "white", align: "center", 
                stroke: "black", strokeThickness: 8, dropShadow: true, 
                dropShadowColor: '#000000', dropShadowBlur: 4, 
                wordWrap: true, wordWrapWidth: Graphics.width - 40 
            });
            
            let message = '';
            switch (reasonKey) {
                case 'allClear': message = uiTexts.endAllClear; break;
                case 'noMoves': message = uiTexts.endNoMoves; break;
                case 'timeUp': message = uiTexts.endTimeUp; break;
            }

            const fullMessage = `${message}\n${uiTexts.endScoreLabel}${this._score}\n\n${uiTexts.endContinue}`;
            const endText = new PIXI.Text(fullMessage, style);
            endText.anchor.set(0.5); 
            endText.x = Graphics.width / 2 + uiTexts.endGameOffsetX; 
            endText.y = Graphics.height / 2 + uiTexts.endGameOffsetY;
            this._uiContainer.addChild(endText);
            
            this._waitingForInput = true;
            this._keyHandler = (event) => {
                if (this._waitingForInput && (event.key.toLowerCase() === 'z' || event.key === 'Enter' || event.key === ' ' || event.key.toLowerCase() === 'x' || event.key === 'Escape')) {
                    this.closeScene();
                }
            };
            this._mouseHandler = () => { if (this._waitingForInput) this.closeScene(); };
            
            document.addEventListener('keydown', this._keyHandler);
            document.addEventListener('mousedown', this._mouseHandler);
        }

        closeScene() {
            this._waitingForInput = false;
            document.removeEventListener('keydown', this._keyHandler);
            document.removeEventListener('mousedown', this._mouseHandler);
            SceneManager.pop();
        }
    }
})();