//=============================================================================
// Ignideus777_LineClearPuzzle
//=============================================================================

/*:
 * @target MZ
 * @plugindesc A line-clearing block puzzle minigame with drag-and-drop and keyboard support.
 * @author Ignideus777
 * @version 2.3.0
 * 
 * @help
 * -Line clear 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:
 * - Click and hold a piece to pick it up.
 * - Drag the piece over the board and release to place it.
 *
 * Keyboard:
 * - Up/Down Arrows: Select one of the 4 pieces from the side panel.
 * - Enter/Z: Pick up the highlighted piece.
 * - Arrow Keys: Move the held piece around the screen.
 * - Enter/Z: Place the held piece on the board.
 * - X / Esc / Shift: Cancel holding a piece, returning it to the panel.
 * - Pause & Exit: Press CTRL to pause. Press CTRL again 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 defaultBoardWidth
 * @parent --- Board Settings ---
 * @text Default Board Width
 * @desc Default width of the board for fixed games.
 * @type number @min 5 @default 10
 *
 * @param defaultBoardHeight
 * @parent --- Board Settings ---
 * @text Default Board Height
 * @desc Default height of the board for fixed games.
 * @type number @min 5 @default 10
 *
 * @param --- Gameplay Settings ---
 *
 * @param defaultTimer
 * @parent --- Gameplay Settings ---
 * @text Default Timer (seconds)
 * @desc The default countdown timer for the game in seconds. 0 for no timer.
 * @type number @min 0 @default 60
 *
 * @param scoreVariable
 * @parent --- Gameplay Settings ---
 * @text Score Variable
 * @desc The game variable ID to store the final score in.
 * @type variable @default 1
 *
 * @param --- Random Game Settings ---
 *
 * @param randomMinWidth @text Min Board Width @parent --- Random Game Settings --- @type number @min 5 @default 6
 * @param randomMaxWidth @text Max Board Width @parent --- Random Game Settings --- @type number @min 5 @default 15
 * @param randomMinHeight @text Min Board Height @parent --- Random Game Settings --- @type number @min 5 @default 6
 * @param randomMaxHeight @text Max Board Height @parent --- Random Game Settings --- @type number @min 5 @default 15
 *
 * @param --- Piece Spawn Settings ---
 * @desc Set a spawn weight for each piece. Higher numbers are more common.
 *
 * @param chance_dot @text Dot (1) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_domino_h @text Domino H (2) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_domino_v @text Domino V (2) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_tri_h @text Tri H (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_tri_v @text Tri V (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_L_small @text Small L (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_L_small_r @text Small L Mirrored (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_gun_small @text Small J (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_gun_small_r @text Small J Mirrored (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_quad_h @text Quad H (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 5
 * @param chance_quad_v @text Quad V (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 5
 * @param chance_square @text Square (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 7
 * @param chance_T @text T-Shape (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_S @text S-Shape (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_Z @text Z-Shape (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_L @text L-Shape (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_J @text J-Shape (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_penta_h @text Penta H (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 3
 * @param chance_plus @text Plus (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 4
 * @param chance_U @text U-Shape (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 4
 *
 * @param --- Display Settings ---
 *
 * @param backgroundImage @text Background Image @parent --- Display Settings --- @desc If blank, background is 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 --- @type boolean @default true
 * @param outlineThickness @text Outline Thickness @parent --- Board Outline Settings --- @type number @min 1 @default 3
 * @param outlineColor @text Outline Color @parent --- Board Outline Settings --- @type string @default #CCCCCC
 * @param outlineAlpha @text Outline Opacity @parent --- Board Outline Settings --- @type number @decimals 2 @min 0 @max 1 @default 1.0
 *
 * @param --- Sound Settings ---
 *
 * @param pickupSound @parent --- Sound Settings --- @text Pickup Piece Sound @type file @dir audio/se/ @default Cursor1
 * @param placeSound @parent --- Sound Settings --- @text Place Piece Sound @type file @dir audio/se/ @default Equip1
 * @param cantPlaceSound @parent --- Sound Settings --- @text Cannot Place Sound @type file @dir audio/se/ @default Buzzer1
 * @param lineClearSound @parent --- Sound Settings --- @text Line Clear Sound @type file @dir audio/se/ @default Item3
 * @param cancelSound @parent --- Sound Settings --- @text Cancel/Pause Sound @type file @dir audio/se/ @default Cancel2
 *
 * @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.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textScoreOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Score" Label Y Offset
 * @desc Vertical offset for the Score text.
 * @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.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textTimeOffsetY
 * @parent --- Text & UI Settings ---
 * @text "Time" Label Y Offset
 * @desc Vertical offset for the Time text.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textPaused
 * @parent --- Text & UI Settings ---
 * @text "Paused" Message
 * @desc Message shown when the game is paused.
 * @type string
 * @default Paused
 *
 * @param textPausedSub
 * @parent --- Text & UI Settings ---
 * @text "Paused Sub-message"
 * @desc Sub-message shown when the game is paused.
 * @type string
 * @default CTRL: Exit | X / R-Click: Resume
 *
 * @param textCenterMessageOffsetX
 * @parent --- Text & UI Settings ---
 * @text Center Message X Offset
 * @desc Horizontal offset for the paused message.
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textCenterMessageOffsetY
 * @parent --- Text & UI Settings ---
 * @text Center Message Y Offset
 * @desc Vertical offset for the paused message.
 * @type number
 * @min -9999
 * @default 0
 *
 * @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 textEndNoMoves
 * @parent --- Text & UI Settings ---
 * @text "No Moves" Message
 * @desc Message shown when there are no valid moves left.
 * @type string
 * @default No more moves!
 *
 * @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 startGame
 * @text Start Game
 * @desc Starts a puzzle with specific settings. Leave blank for defaults.
 *
 * @arg width @text Board Width @type number @min 5 @default
 * @arg height @text Board Height @type number @min 5 @default
 * @arg timer @text Timer (seconds) @desc 0 for no timer. @type number @min 0 @default
 * 
 * @arg pieceChances
 * @text Custom Piece Spawn Chances
 * @desc Override default spawn weights. -1 to use default for a piece.
 * @type struct<PieceChances>
 * @default
 * 
 * @command startRandomGame
 * @text Start Random Game
 * @desc Starts a puzzle with randomized settings. Leave blank for defaults.
 * 
 * @arg minWidth @text Min Board Width @type number @min 5 @default
 * @arg maxWidth @text Max Board Width @type number @min 5 @default
 * @arg minHeight @text Min Board Height @type number @min 5 @default
 * @arg maxHeight @text Max Board Height @type number @min 5 @default
 * @arg timer @text Timer (seconds) @desc 0 for no timer. @type number @min 0 @default
 */
/*~struct~PieceChances:
 * @param dot @text Dot (1) @type number @min -1 @default -1
 * @param domino_h @text Domino H (2) @type number @min -1 @default -1
 * @param domino_v @text Domino V (2) @type number @min -1 @default -1
 * @param tri_h @text Tri H (3) @type number @min -1 @default -1
 * @param tri_v @text Tri V (3) @type number @min -1 @default -1
 * @param L_small @text Small L (3) @type number @min -1 @default -1
 * @param L_small_r @text Small J (3) @type number @min -1 @default -1
 * @param gun_small @text Small Gun (3) @type number @min -1 @default -1
 * @param gun_small_r @text Small Mirrored Gun (3) @type number @min -1 @default -1
 * @param quad_h @text Quad H (4) @type number @min -1 @default -1
 * @param quad_v @text Quad V (4) @type number @min -1 @default -1
 * @param square @text Square (4) @type number @min -1 @default -1
 * @param T @text T-Shape (4) @type number @min -1 @default -1
 * @param S @text S-Shape (4) @type number @min -1 @default -1
 * @param Z @text Z-Shape (4) @type number @min -1 @default -1
 * @param L @text L-Shape (4) @type number @min -1 @default -1
 * @param J @text J-Shape (4) @type number @min -1 @default -1
 * @param penta_h @text Penta H (5) @type number @min -1 @default -1
 * @param plus @text Plus (5) @type number @min -1 @default -1
 * @param U @text U-Shape (5) @type number @min -1 @default -1
 */
 
/*:ja
 * @target MZ
 * @plugindesc ドラッグ＆ドロップとキーボード操作に対応した、ラインを消すブロックパズルミニゲームです。
 * @author Ignideus777
 * @version 2.3.0
 * 
 * @help
 * - RPGツクールMZ用のラインクリアミニゲームです。
 * - PIXIグラフィックスのみを使用しています。
 * - 画像ファイルは不要です。
 * - マウスとキーボード操作に対応しています。
 * - デフォルト設定のままでも簡単にセットアップできます。
 * - ゲームごとに設定をランダム化できます。
 *
 * --- 操作とゲームプレイ ---
 * マウス:
 * - ピースをクリックして長押しすると持ち上げます。
 * - ピースをボード上にドラッグし、離すと配置します。
 *
 * キーボード:
 * - ↑↓キー: サイドパネルから4つのピースのうち1つを選択します。
 * - 決定/Zキー: ハイライトされたピースを持ち上げます。
 * - 矢印キー: 持っているピースを画面上で動かします。
 * - 決定/Zキー: 持っているピースをボードに配置します。
 * - X / Esc / Shiftキー: ピースを持つのをキャンセルし、パネルに戻します。
 * - ポーズと終了: Ctrlキーでポーズします。もう一度Ctrlキーを押すと終了します。
 *
 * --- 表示 ---
 * 「背景画像」パラメータが空の場合、背景は透明になり、
 * 下のマップやシーンが表示されます。
 *
 * --- サウンドエフェクト ---
 * すべての効果音はプラグインパラメータでカスタマイズできます。
 * サウンドが空欄の場合、デフォルトのエンジンサウンドが使用されます。
 * 
 * @param --- Board Settings ---
 * @text --- ボード設定 ---
 *
 * @param defaultBoardWidth
 * @parent --- Board Settings ---
 * @text デフォルトのボードの幅
 * @desc 固定ゲームのボードのデフォルトの幅です。
 * @type number @min 5 @default 10
 *
 * @param defaultBoardHeight
 * @parent --- Board Settings ---
 * @text デフォルトのボードの高さ
 * @desc 固定ゲームのボードのデフォルトの高さです。
 * @type number @min 5 @default 10
 *
 * @param --- Gameplay Settings ---
 * @text --- ゲームプレイ設定 ---
 *
 * @param defaultTimer
 * @parent --- Gameplay Settings ---
 * @text デフォルトのタイマー(秒)
 * @desc ゲームのデフォルトのカウントダウンタイマー(秒)。0でタイマーなし。
 * @type number @min 0 @default 60
 *
 * @param scoreVariable
 * @parent --- Gameplay Settings ---
 * @text スコアを格納する変数
 * @desc 最終スコアを格納するゲーム変数のIDです。
 * @type variable @default 1
 *
 * @param --- Random Game Settings ---
 * @text --- ランダムゲーム設定 ---
 *
 * @param randomMinWidth @text 最小ボード幅 @parent --- Random Game Settings --- @type number @min 5 @default 6
 * @param randomMaxWidth @text 最大ボード幅 @parent --- Random Game Settings --- @type number @min 5 @default 15
 * @param randomMinHeight @text 最小ボード高さ @parent --- Random Game Settings --- @type number @min 5 @default 6
 * @param randomMaxHeight @text 最大ボード高さ @parent --- Random Game Settings --- @type number @min 5 @default 15
 *
 * @param --- Piece Spawn Settings ---
 * @text --- ピース出現設定 ---
 * @desc 各ピースの出現の重みを設定します。数値が高いほど出現しやすくなります。
 *
 * @param chance_dot @text 点 (1) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_domino_h @text ドミノ 横 (2) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_domino_v @text ドミノ 縦 (2) @parent --- Piece Spawn Settings --- @type number @min 0 @default 10
 * @param chance_tri_h @text 3マス 横 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_tri_v @text 3マス 縦 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_L_small @text 小さいL字 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_L_small_r @text 小さいL字 反転 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_gun_small @text 小さいJ字 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_gun_small_r @text 小さいJ字 反転 (3) @parent --- Piece Spawn Settings --- @type number @min 0 @default 8
 * @param chance_quad_h @text 4マス 横 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 5
 * @param chance_quad_v @text 4マス 縦 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 5
 * @param chance_square @text 正方形 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 7
 * @param chance_T @text T字 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_S @text S字 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_Z @text Z字 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_L @text L字 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_J @text J字 (4) @parent --- Piece Spawn Settings --- @type number @min 0 @default 6
 * @param chance_penta_h @text 5マス 横 (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 3
 * @param chance_plus @text 十字 (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 4
 * @param chance_U @text U字 (5) @parent --- Piece Spawn Settings --- @type number @min 0 @default 4
 *
 * @param --- Display Settings ---
 * @text --- 表示設定 ---
 *
 * @param backgroundImage @text 背景画像 @parent --- Display Settings --- @desc 空欄の場合、背景は透明になります。png形式のファイルである必要があります。 @type file @dir img/pictures/ @default
 *
 * @param --- Board Outline Settings ---
 * @text --- ボードの枠線設定 ---
 *
 * @param enableOutline @text ボードの枠線を表示 @parent --- Board Outline Settings --- @type boolean @default true
 * @param outlineThickness @text 枠線の太さ @parent --- Board Outline Settings --- @type number @min 1 @default 3
 * @param outlineColor @text 枠線の色 @parent --- Board Outline Settings --- @type string @default #CCCCCC
 * @param outlineAlpha @text 枠線の不透明度 @parent --- Board Outline Settings --- @type number @decimals 2 @min 0 @max 1 @default 1.0
 *
 * @param --- Sound Settings ---
 * @text --- サウンド設定 ---
 *
 * @param pickupSound @parent --- Sound Settings --- @text ピースを持ち上げる音 @type file @dir audio/se/ @default Cursor1
 * @param placeSound @parent --- Sound Settings --- @text ピースを配置する音 @type file @dir audio/se/ @default Equip1
 * @param cantPlaceSound @parent --- Sound Settings --- @text 配置できない時の音 @type file @dir audio/se/ @default Buzzer1
 * @param lineClearSound @parent --- Sound Settings --- @text ライン消去音 @type file @dir audio/se/ @default Item3
 * @param cancelSound @parent --- Sound Settings --- @text キャンセル/ポーズ音 @type file @dir audio/se/ @default Cancel2
 *
 * @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 textPausedSub
 * @parent --- Text & UI Settings ---
 * @text 「ポーズ中」サブメッセージ
 * @desc ゲームが一時停止したときに表示されるサブメッセージ。
 * @type string
 * @default CTRL: 終了 | X / 右クリック: 再開
 *
 * @param textCenterMessageOffsetX
 * @parent --- Text & UI Settings ---
 * @text 中央メッセージXオフセット
 * @desc ポーズメッセージの水平オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textCenterMessageOffsetY
 * @parent --- Text & UI Settings ---
 * @text 中央メッセージYオフセット
 * @desc ポーズメッセージの垂直オフセット。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndTimeUp
 * @parent --- Text & UI Settings ---
 * @text 「時間切れ」メッセージ
 * @desc 時間切れになったときのメッセージ。
 * @type string
 * @default 時間切れ！
 *
 * @param textEndNoMoves
 * @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 startGame
 * @text ゲーム開始
 * @desc 指定した設定でパズルを開始します。空欄でデフォルト値を使用。
 *
 * @arg width @text ボードの幅 @type number @min 5 @default
 * @arg height @text ボードの高さ @type number @min 5 @default
 * @arg timer @text タイマー(秒) @desc 0でタイマーなし。 @type number @min 0 @default
 * 
 * @arg pieceChances
 * @text ピース出現率のカスタム設定
 * @desc デフォルトの出現の重みを上書きします。-1でピースのデフォルト値を使用。
 * @type struct<PieceChances>
 * @default
 * 
 * @command startRandomGame
 * @text ランダムゲーム開始
 * @desc ランダムな設定でパズルを開始します。空欄でデフォルト値を使用。
 * 
 * @arg minWidth @text 最小ボード幅 @type number @min 5 @default
 * @arg maxWidth @text 最大ボード幅 @type number @min 5 @default
 * @arg minHeight @text 最小ボード高さ @type number @min 5 @default
 * @arg maxHeight @text 最大ボード高さ @type number @min 5 @default
 * @arg timer @text タイマー(秒) @desc 0でタイマーなし。 @type number @min 0 @default
 */
/*~struct~PieceChances:ja
 * @param dot @text 点 (1) @type number @min -1 @default -1
 * @param domino_h @text ドミノ 横 (2) @type number @min -1 @default -1
 * @param domino_v @text ドミノ 縦 (2) @type number @min -1 @default -1
 * @param tri_h @text 3マス 横 (3) @type number @min -1 @default -1
 * @param tri_v @text 3マス 縦 (3) @type number @min -1 @default -1
 * @param L_small @text 小さいL字 (3) @type number @min -1 @default -1
 * @param L_small_r @text 小さいL字 反転 (3) @type number @min -1 @default -1
 * @param gun_small @text 小さいJ字 (3) @type number @min -1 @default -1
 * @param gun_small_r @text 小さいJ字 反転 (3) @type number @min -1 @default -1
 * @param quad_h @text 4マス 横 (4) @type number @min -1 @default -1
 * @param quad_v @text 4マス 縦 (4) @type number @min -1 @default -1
 * @param square @text 正方形 (4) @type number @min -1 @default -1
 * @param T @text T字 (4) @type number @min -1 @default -1
 * @param S @text S字 (4) @type number @min -1 @default -1
 * @param Z @text Z字 (4) @type number @min -1 @default -1
 * @param L @text L字 (4) @type number @min -1 @default -1
 * @param J @text J字 (4) @type number @min -1 @default -1
 * @param penta_h @text 5マス 横 (5) @type number @min -1 @default -1
 * @param plus @text 十字 (5) @type number @min -1 @default -1
 * @param U @text U字 (5) @type number @min -1 @default -1
 */ 

/*:zh_CN
 * @target MZ
 * @plugindesc 一款支持拖拽和键盘操作的行列消除方块益智小游戏。
 * @author Ignideus777
 * @version 2.3.0
 * 
 * @help
 * - RPG Maker MZ 的行列消除小游戏。
 * - 仅使用 PIXI 图形渲染。
 * - 无需任何图像文件。
 * - 支持鼠标和键盘操作。
 * - 可保留默认设置以轻松启动。
 * - 每次小游戏都可以是随机的。
 *
 * --- 操作与玩法 ---
 * 鼠标:
 * - 按住并拖动一个方块来拾取它。
 * - 将方块拖到棋盘上并释放以放置它。
 *
 * 键盘:
 * - 上/下方向键: 从侧边面板选择4个方块中的一个。
 * - Enter/Z: 拾取高亮的方块。
 * - 方向键: 在屏幕上移动持有的方块。
 * - Enter/Z: 将持有的方块放置在棋盘上。
 * - X / Esc / Shift: 取消持有方块，将其返回面板。
 * - 暂停与退出: 按 CTRL 暂停。再次按 CTRL 退出。
 *
 * --- 显示 ---
 * 如果“背景图片”参数为空，背景将是透明的，会显示下方的地图或场景。
 *
 * --- 音效 ---
 * 您可以在插件参数中自定义所有音效。如果留空，将使用引擎的默认音效。
 * 
 * @param --- 棋盘设置 ---
 *
 * @param defaultBoardWidth
 * @parent --- 棋盘设置 ---
 * @text 默认棋盘宽度
 * @desc 固定游戏的默认棋盘宽度。
 * @type number @min 5 @default 10
 *
 * @param defaultBoardHeight
 * @parent --- 棋盘设置 ---
 * @text 默认棋盘高度
 * @desc 固定游戏的默认棋盘高度。
 * @type number @min 5 @default 10
 *
 * @param --- 玩法设置 ---
 *
 * @param defaultTimer
 * @parent --- 玩法设置 ---
 * @text 默认计时器 (秒)
 * @desc 游戏的默认倒计时器（秒）。0为无计时器。
 * @type number @min 0 @default 60
 *
 * @param scoreVariable
 * @parent --- 玩法设置 ---
 * @text 分数变量
 * @desc 用于存储最终分数的游戏变量ID。
 * @type variable @default 1
 *
 * @param --- 随机游戏设置 ---
 *
 * @param randomMinWidth @text 最小棋盘宽度 @parent --- 随机游戏设置 --- @type number @min 5 @default 6
 * @param randomMaxWidth @text 最大棋盘宽度 @parent --- 随机游戏设置 --- @type number @min 5 @default 15
 * @param randomMinHeight @text 最小棋盘高度 @parent --- 随机游戏设置 --- @type number @min 5 @default 6
 * @param randomMaxHeight @text 最大棋盘高度 @parent --- 随机游戏设置 --- @type number @min 5 @default 15
 *
 * @param --- 方块生成设置 ---
 * @desc 为每个方块设置生成权重。数值越高越常见。
 *
 * @param chance_dot @text 点 (1) @parent --- 方块生成设置 --- @type number @min 0 @default 10
 * @param chance_domino_h @text 横向双格 (2) @parent --- 方块生成设置 --- @type number @min 0 @default 10
 * @param chance_domino_v @text 纵向双格 (2) @parent --- 方块生成设置 --- @type number @min 0 @default 10
 * @param chance_tri_h @text 横向三格 (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_tri_v @text 纵向三格 (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_L_small @text 小L形 (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_L_small_r @text 小L形(镜像) (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_gun_small @text 小J形 (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_gun_small_r @text 小J形(镜像) (3) @parent --- 方块生成设置 --- @type number @min 0 @default 8
 * @param chance_quad_h @text 横向四格 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 5
 * @param chance_quad_v @text 纵向四格 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 5
 * @param chance_square @text 正方形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 7
 * @param chance_T @text T形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 6
 * @param chance_S @text S形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 6
 * @param chance_Z @text Z形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 6
 * @param chance_L @text L形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 6
 * @param chance_J @text J形 (4) @parent --- 方块生成设置 --- @type number @min 0 @default 6
 * @param chance_penta_h @text 横向五格 (5) @parent --- 方块生成设置 --- @type number @min 0 @default 3
 * @param chance_plus @text 十字形 (5) @parent --- 方块生成设置 --- @type number @min 0 @default 4
 * @param chance_U @text U形 (5) @parent --- 方块生成设置 --- @type number @min 0 @default 4
 *
 * @param --- 显示设置 ---
 *
 * @param backgroundImage @text 背景图片 @parent --- 显示设置 --- @desc 如果留空，背景将是透明的。必须是png格式。 @type file @dir img/pictures/ @default
 *
 * @param --- 棋盘边框设置 ---
 *
 * @param enableOutline @text 启用棋盘边框 @parent --- 棋盘边框设置 --- @type boolean @default true
 * @param outlineThickness @text 边框粗细 @parent --- 棋盘边框设置 --- @type number @min 1 @default 3
 * @param outlineColor @text 边框颜色 @parent --- 棋盘边框设置 --- @type string @default #CCCCCC
 * @param outlineAlpha @text 边框不透明度 @parent --- 棋盘边框设置 --- @type number @decimals 2 @min 0 @max 1 @default 1.0
 *
 * @param --- 音效设置 ---
 *
 * @param pickupSound @parent --- 音效设置 --- @text 拾取方块音效 @type file @dir audio/se/ @default Cursor1
 * @param placeSound @parent --- 音效设置 --- @text 放置方块音效 @type file @dir audio/se/ @default Equip1
 * @param cantPlaceSound @parent --- 音效设置 --- @text 无法放置音效 @type file @dir audio/se/ @default Buzzer1
 * @param lineClearSound @parent --- 音效设置 --- @text 消除行音效 @type file @dir audio/se/ @default Item3
 * @param cancelSound @parent --- 音效设置 --- @text 取消/暂停音效 @type file @dir audio/se/ @default Cancel2
 *
 * @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 textPausedSub
 * @parent --- 文本与UI设置 ---
 * @text “已暂停”副信息
 * @desc 游戏暂停时显示的副信息。
 * @type string
 * @default CTRL: 退出 | X / 鼠标右键: 继续
 *
 * @param textCenterMessageOffsetX
 * @parent --- 文本与UI设置 ---
 * @text 中心信息 X 偏移
 * @desc 暂停信息的水平偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textCenterMessageOffsetY
 * @parent --- 文本与UI设置 ---
 * @text 中心信息 Y 偏移
 * @desc 暂停信息的垂直偏移量。
 * @type number
 * @min -9999
 * @default 0
 *
 * @param textEndTimeUp
 * @parent --- 文本与UI设置 ---
 * @text “时间到”信息
 * @desc 计时器用尽时显示的信息。
 * @type string
 * @default 时间到！
 *
 * @param textEndNoMoves
 * @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 startGame
 * @text 开始游戏
 * @desc 使用特定设置开始谜题。留空则使用默认值。
 *
 * @arg width @text 棋盘宽度 @type number @min 5 @default
 * @arg height @text 棋盘高度 @type number @min 5 @default
 * @arg timer @text 计时器 (秒) @desc 0为无计时器。 @type number @min 0 @default
 * 
 * @arg pieceChances
 * @text 自定义方块生成概率
 * @desc 覆盖默认的生成权重。-1表示对该方块使用默认值。
 * @type struct<PieceChances>
 * @default
 * 
 * @command startRandomGame
 * @text 开始随机游戏
 * @desc 使用随机设置开始谜题。留空则使用默认值。
 * 
 * @arg minWidth @text 最小棋盘宽度 @type number @min 5 @default
 * @arg maxWidth @text 最大棋盘宽度 @type number @min 5 @default
 * @arg minHeight @text 最小棋盘高度 @type number @min 5 @default
 * @arg maxHeight @text 最大棋盘高度 @type number @min 5 @default
 * @arg timer @text 计时器 (秒) @desc 0为无计时器。 @type number @min 0 @default
 */
 
/*~struct~PieceChances:zh_CN
 * @param dot @text 点 (1) @type number @min -1 @default -1
 * @param domino_h @text 横向双格 (2) @type number @min -1 @default -1
 * @param domino_v @text 纵向双格 (2) @type number @min -1 @default -1
 * @param tri_h @text 横向三格 (3) @type number @min -1 @default -1
 * @param tri_v @text 纵向三格 (3) @type number @min -1 @default -1
 * @param L_small @text 小L形 (3) @type number @min -1 @default -1
 * @param L_small_r @text 小J形 (3) @type number @min -1 @default -1
 * @param gun_small @text 小枪形 (3) @type number @min -1 @default -1
 * @param gun_small_r @text 小枪形(镜像) (3) @type number @min -1 @default -1
 * @param quad_h @text 横向四格 (4) @type number @min -1 @default -1
 * @param quad_v @text 纵向四格 (4) @type number @min -1 @default -1
 * @param square @text 正方形 (4) @type number @min -1 @default -1
 * @param T @text T形 (4) @type number @min -1 @default -1
 * @param S @text S形 (4) @type number @min -1 @default -1
 * @param Z @text Z形 (4) @type number @min -1 @default -1
 * @param L @text L形 (4) @type number @min -1 @default -1
 * @param J @text J形 (4) @type number @min -1 @default -1
 * @param penta_h @text 横向五格 (5) @type number @min -1 @default -1
 * @param plus @text 十字形 (5) @type number @min -1 @default -1
 * @param U @text U形 (5) @type number @min -1 @default -1
 */

(() => {
    'use strict';

    const PLUGIN_NAME = "Ignideus777_LineClearPuzzle";
    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'),
        pausedSub: parseLocalizableText(params.textPausedSub, 'CTRL: Exit | X / R-Click: Resume'),
        centerMessageOffsetX: Number(params.textCenterMessageOffsetX || 0),
        centerMessageOffsetY: Number(params.textCenterMessageOffsetY || 0),
        endTimeUp: parseLocalizableText(params.textEndTimeUp, "Time's Up!"),
        endNoMoves: parseLocalizableText(params.textEndNoMoves, 'No more moves!'),
        endScoreLabel: parseLocalizableText(params.textEndScoreLabel, 'Final Score: '),
        endContinue: parseLocalizableText(params.textEndContinue, 'Press any to continue'),
        endGameOffsetX: Number(params.textEndGameOffsetX || 0),
        endGameOffsetY: Number(params.textEndGameOffsetY || 0),
    };

    const DefaultSettings = {
        width: Number(params.defaultBoardWidth) || 10,
        height: Number(params.defaultBoardHeight) || 10,
        timer: Number(params.defaultTimer) || 60
    };
    const RandomSettings = {
        minW: Number(params.randomMinWidth) || 6, maxW: Number(params.randomMaxWidth) || 15,
        minH: Number(params.randomMinHeight) || 6, maxH: Number(params.randomMaxHeight) || 15,
        timer: Number(params.defaultTimer) || 60
    };

    const scoreVariableId = Number(params.scoreVariable) || 1;
    const backImage = params.backgroundImage;

    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 Sounds = {
        pickup: params.pickupSound || 'Cursor1', place: params.placeSound || 'Equip1',
        cantPlace: params.cantPlaceSound || 'Buzzer1', lineClear: params.lineClearSound || 'Item3',
        cancel: params.cancelSound || 'Cancel2'
    };
    
    const playSound = (soundName) => AudioManager.playSe({ name: soundName, pan: 0, pitch: 100, volume: 90 });
    const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    const getArgAsNumber = (arg, defaultValue) => (arg !== "" && arg !== undefined) ? Number(arg) : defaultValue;
    
    const PIECE_SHAPES = {
        dot: { shape: [[1]], color: 0 }, domino_h: { shape: [[1, 1]], color: 1 },
        domino_v: { shape: [[1], [1]], color: 1 }, tri_h: { shape: [[1, 1, 1]], color: 2 },
        tri_v: { shape: [[1], [1], [1]], color: 2 }, L_small: { shape: [[1, 0], [1, 1]], color: 6 },
        L_small_r: { shape: [[0, 1], [1, 1]], color: 6 }, gun_small: { shape: [[1, 1], [1, 0]], color: 6 },
        gun_small_r: { shape: [[1, 1], [0, 1]], color: 6 }, quad_h: { shape: [[1, 1, 1, 1]], color: 3 },
        quad_v: { shape: [[1], [1], [1], [1]], color: 3 }, square: { shape: [[1, 1], [1, 1]], color: 5 },
        T: { shape: [[1, 1, 1], [0, 1, 0]], color: 1 }, S: { shape: [[0, 1, 1], [1, 1, 0]], color: 2 },
        Z: { shape: [[1, 1, 0], [0, 1, 1]], color: 3 }, L: { shape: [[1, 0], [1, 0], [1, 1]], color: 6 },
        J: { shape: [[0, 1], [0, 1], [1, 1]], color: 0 }, penta_h: { shape: [[1, 1, 1, 1, 1]], color: 4 },
        plus: { shape: [[0, 1, 0], [1, 1, 1], [0, 1, 0]], color: 5 }, U: { shape: [[1, 0, 1], [1, 1, 1]], color: 4 }
    };
    const PIECE_KEYS = Object.keys(PIECE_SHAPES);

    const DefaultPieceWeights = {};
    for (const key of PIECE_KEYS) { DefaultPieceWeights[key] = Number(params[`chance_${key}`]) || 10; }

    PluginManager.registerCommand(PLUGIN_NAME, "startGame", function(args) {
        let pieceWeights = { ...DefaultPieceWeights };
        if (args.pieceChances) {
            try {
                const customChances = JSON.parse(args.pieceChances);
                for (const key in customChances) {
                    const chance = Number(customChances[key]);
                    if (pieceWeights.hasOwnProperty(key) && chance > -1) pieceWeights[key] = chance;
                }
            } catch (e) { console.error("LineClearPuzzle: Invalid pieceChances struct data.", e); }
        }
        const gameParams = {
            width: getArgAsNumber(args.width, DefaultSettings.width),
            height: getArgAsNumber(args.height, DefaultSettings.height),
            timer: getArgAsNumber(args.timer, DefaultSettings.timer),
            pieceWeights
        };
        SceneManager.push(Scene_LineClearPuzzle);
        SceneManager.prepareNextScene(gameParams);
    });

    PluginManager.registerCommand(PLUGIN_NAME, "startRandomGame", function(args) {
        const minW = getArgAsNumber(args.minWidth, RandomSettings.minW);
        const maxW = getArgAsNumber(args.maxWidth, RandomSettings.maxW);
        const minH = getArgAsNumber(args.minHeight, RandomSettings.minH);
        const maxH = getArgAsNumber(args.maxHeight, RandomSettings.maxH);

        const gameParams = {
            width: getRandomInt(Math.min(minW, maxW), Math.max(minW, maxW)),
            height: getRandomInt(Math.min(minH, maxH), Math.max(minH, maxH)),
            timer: getArgAsNumber(args.timer, RandomSettings.timer),
            pieceWeights: { ...DefaultPieceWeights }
        };
        SceneManager.push(Scene_LineClearPuzzle);
        SceneManager.prepareNextScene(gameParams);
    });
    
    const BLOCK_SIZE = 36;
    const BLOCK_SPACING = 2;
    const PIECE_AREA_WIDTH = 180;
    const GAP_BETWEEN_AREAS = 40;
    const KEYBOARD_MOVE_SPEED = 8;
    const PIECE_COLORS = [0xff6666, 0x66ff66, 0x6666ff, 0xffff66, 0xff66ff, 0x66ffff, 0xffa500];

    class Scene_LineClearPuzzle extends Scene_Base {
        prepare(params) {
            this._gameParams = params;
            this.initializeState();
        }

        initializeState() {
            this._boardWidth = this._gameParams.width; this._boardHeight = this._gameParams.height;
            this._pieceWeights = this._gameParams.pieceWeights;
            this._board = Array(this._boardHeight).fill(0).map(() => Array(this._boardWidth).fill(0));
            this._boardSprites = []; this._score = 0; this._gameActive = true;
            this._isProcessing = false; this._isPaused = false;
            this._hasTimer = this._gameParams.timer > 0;
            this._timer = this._gameParams.timer * 60;
            this._currentPieces = []; this._pieceSlots = [];
            this._isDragging = false; this._draggedPiece = null; this._draggedPieceIndex = -1;
            this._keyboardSelectedPieceIndex = 0; this._isKeyboardControllingCursor = false;
        }

        create() {
            super.create();
            this.createBackground(); this.createBoard(); this.createPieceSelectionArea();
            this.createKeyboardIndicator(); this.createGhostPiece(); this.createDragContainer();
            this.createUI(); this.startGame();
        }

        startGame() {
            this.drawBoard();
            if (this._hasTimer) this.updateTimerText();
            this.generateNewPieces();
            this.updateKeyboardIndicator();
        }

        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);
        }

        createUI() {
            const style = new PIXI.TextStyle({ fontFamily: "GameFont", fontSize: 36, fill: "#ffffff", stroke: "#000000", strokeThickness: 5 });
            this._scoreText = new PIXI.Text("", style);
            this._scoreText.x = 20 + uiTexts.scoreOffsetX;
            this._scoreText.y = 20 + uiTexts.scoreOffsetY;
            this.addChild(this._scoreText);
            this.updateScoreText();

            if (this._hasTimer) {
                this._timerText = new PIXI.Text("", style);
                this._timerText.anchor.set(1, 0);
                this._timerText.x = Graphics.width - 20 + uiTexts.timeOffsetX;
                this._timerText.y = 20 + uiTexts.timeOffsetY;
                this.addChild(this._timerText);
                this.updateTimerText();
            }

            this._pauseOverlay = new PIXI.Graphics();
            this._pauseOverlay.beginFill(0x000000, 0.7).drawRect(0, 0, Graphics.width, Graphics.height).endFill();
            this._pauseOverlay.visible = false; this.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.centerMessageOffsetX;
            this._pauseText.y = Graphics.height / 2 - 40 + uiTexts.centerMessageOffsetY;
            this._pauseOverlay.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.pausedSub, exitStyle);
            this._pauseExitText.anchor.set(0.5);
            this._pauseExitText.x = Graphics.width / 2 + uiTexts.centerMessageOffsetX;
            this._pauseExitText.y = this._pauseText.y + 80;
            this._pauseOverlay.addChild(this._pauseExitText);
        }
        
        updateScoreText() { this._scoreText.text = `${uiTexts.scoreLabel}${this._score}`; }
        updateTimerText() { this._timerText.text = `${uiTexts.timeLabel}${Math.ceil(this._timer / 60)}`; }
        
        createBoard() {
            const boardWidthPx = this._boardWidth * (BLOCK_SIZE + BLOCK_SPACING) - BLOCK_SPACING;
            const totalContentWidth = boardWidthPx + GAP_BETWEEN_AREAS + PIECE_AREA_WIDTH;
            const startX = (Graphics.width - totalContentWidth) / 2;
            const totalHeight = this._boardHeight * (BLOCK_SIZE + BLOCK_SPACING) - BLOCK_SPACING;
            this._boardContainer = new PIXI.Container();
            this._boardContainer.x = startX; this._boardContainer.y = (Graphics.height - totalHeight) / 2;
            this.addChild(this._boardContainer);
            if (BoardOutline.enabled) {
                const borderPadding = BLOCK_SPACING * 2;
                const outline = new PIXI.Graphics();
                outline.lineStyle(BoardOutline.thickness, BoardOutline.color, BoardOutline.alpha);
                outline.drawRect( -borderPadding, -borderPadding, boardWidthPx + borderPadding * 2, totalHeight + borderPadding * 2 );
                this._boardContainer.addChild(outline);
            }
        }
        
        createPieceSelectionArea() {
            const boardWidthPx = this._boardWidth * (BLOCK_SIZE + BLOCK_SPACING) - BLOCK_SPACING;
            const totalContentWidth = boardWidthPx + GAP_BETWEEN_AREAS + PIECE_AREA_WIDTH;
            const startX = (Graphics.width - totalContentWidth) / 2;
            this._pieceSelectionContainer = new PIXI.Container();
            this._pieceSelectionContainer.x = startX + boardWidthPx + GAP_BETWEEN_AREAS + PIECE_AREA_WIDTH / 2;
            this._pieceSelectionContainer.y = Graphics.height / 2;
            this.addChild(this._pieceSelectionContainer);
            for (let i = 0; i < 4; i++) {
                const slot = new PIXI.Container();
                slot.x = 0; slot.y = (i - 1.5) * 120;
                const bg = new PIXI.Graphics();
                bg.beginFill(0x000000, 0.5).drawRoundedRect(-80, -60, 160, 120, 10).endFill();
                slot.addChild(bg);
                this._pieceSelectionContainer.addChild(slot); this._pieceSlots.push(slot);
            }
        }

        createKeyboardIndicator() {
            this._keyboardIndicator = new PIXI.Graphics();
            this._keyboardIndicator.lineStyle(4, 0xffff00, 1).drawRoundedRect(-82, -62, 164, 124, 12);
            this._pieceSelectionContainer.addChild(this._keyboardIndicator);
        }

        resumeGame() {
            this._isPaused = false;
            this._pauseOverlay.visible = false;
            playSound(Sounds.pickup);
        }

        update() {
            super.update();
            if (Input.isTriggered('control')) { this.handlePause(); return; }
            if (this._isPaused) {
                if (Input.isTriggered('cancel') || TouchInput.isCancelled()) this.resumeGame();
                return;
            }
            if (!this._gameActive || this._isProcessing) return;
            this.updateTimer();
            if (this._isDragging) this.handleHeldPieceInput();
            else this.handlePieceSelectionInput();
        }

        handlePause() {
            if (this._isPaused) this.quitGame(true);
            else if (this._gameActive) {
                this._isPaused = true; this._pauseOverlay.visible = true;
                playSound(Sounds.cancel);
                if (this._isDragging) this.cancelDrag();
            }
        }
        
        quitGame(force = false) {
            playSound(Sounds.cancel); $gameVariables.setValue(scoreVariableId, this._score);
            SceneManager.pop();
        }

        updateTimer() {
            if (!this._hasTimer || !this._gameActive) return;
            if (this._timer > 0) { this._timer--; this.updateTimerText(); } 
            else { this.endGame(uiTexts.endTimeUp); }
        }
        
        drawBoard() {
            this._boardContainer.children = this._boardContainer.children.filter(c => c !== this._ghostPiece);
            const outline = this._boardContainer.children[0];
            this._boardContainer.removeChildren();
            if (outline) this._boardContainer.addChild(outline);
            this._boardContainer.addChild(this._ghostPiece);

            this._boardSprites = [];
            for (let y = 0; y < this._boardHeight; y++) {
                this._boardSprites[y] = [];
                for (let x = 0; x < this._boardWidth; x++) {
                    const blockSprite = new PIXI.Graphics();
                    blockSprite.x = x * (BLOCK_SIZE + BLOCK_SPACING); blockSprite.y = y * (BLOCK_SIZE + BLOCK_SPACING);
                    this.drawBlock(blockSprite, this._board[y][x]);
                    this._boardContainer.addChild(blockSprite); this._boardSprites[y][x] = blockSprite;
                }
            }
        }
        
        drawBlock(sprite, type, colorIndex = 0, alpha = 1.0) {
            sprite.clear();
            if (type === 0) sprite.beginFill(0x333333, 0.7).drawRoundedRect(0, 0, BLOCK_SIZE, BLOCK_SIZE, 4);
            else {
                const color = PIECE_COLORS[colorIndex % PIECE_COLORS.length];
                sprite.beginFill(color, alpha).lineStyle(2, 0x000000, 0.75).drawRoundedRect(0, 0, BLOCK_SIZE, BLOCK_SIZE, 5);
            }
            sprite.endFill();
        }
        
        createGhostPiece() {
            this._ghostPiece = new PIXI.Container();
            this._ghostPiece.visible = false; this._boardContainer.addChild(this._ghostPiece);
        }

        createDragContainer() {
            this._draggedPieceContainer = new PIXI.Container();
            this._draggedPieceContainer.visible = false; this.addChild(this._draggedPieceContainer);
        }

        handlePieceSelectionInput() {
            if (TouchInput.isTriggered()) {
                this._isKeyboardControllingCursor = false;
                for (let i = 0; i < 4; i++) {
                    if (this._currentPieces[i] && this.isPointInContainer(TouchInput, this._pieceSlots[i])) {
                        this.pickupPiece(i); playSound(Sounds.pickup); break;
                    }
                }
            }
            let selectionChanged = false;
            if (Input.isTriggered('down')) {
                this._keyboardSelectedPieceIndex = this.findNextAvailablePiece(this._keyboardSelectedPieceIndex, 1);
                selectionChanged = true;
            }
            if (Input.isTriggered('up')) {
                this._keyboardSelectedPieceIndex = this.findNextAvailablePiece(this._keyboardSelectedPieceIndex, -1);
                selectionChanged = true;
            }
            if (selectionChanged) { SoundManager.playCursor(); this.updateKeyboardIndicator(); }
            if (Input.isTriggered('ok')) {
                if (this._currentPieces[this._keyboardSelectedPieceIndex]) {
                    this.pickupPiece(this._keyboardSelectedPieceIndex, true); playSound(Sounds.pickup);
                } else { playSound(Sounds.cantPlace); }
            }
        }

        findNextAvailablePiece(startIndex, direction) {
            const numPieces = this._currentPieces.length;
            if (this._currentPieces.every(p => p === null)) return startIndex;
            let nextIndex = (startIndex + direction + numPieces) % numPieces;
            while (this._currentPieces[nextIndex] === null) nextIndex = (nextIndex + direction + numPieces) % numPieces;
            return nextIndex;
        }
        
        handleHeldPieceInput() {
            if (this._isKeyboardControllingCursor) {
                if (Input.isPressed('left')) TouchInput._x -= KEYBOARD_MOVE_SPEED;
                if (Input.isPressed('right')) TouchInput._x += KEYBOARD_MOVE_SPEED;
                if (Input.isPressed('up')) TouchInput._y -= KEYBOARD_MOVE_SPEED;
                if (Input.isPressed('down')) TouchInput._y += KEYBOARD_MOVE_SPEED;
                TouchInput._x = TouchInput._x.clamp(0, Graphics.width); TouchInput._y = TouchInput._y.clamp(0, Graphics.height);
            }
            this._draggedPieceContainer.x = TouchInput.x; this._draggedPieceContainer.y = TouchInput.y;
            this.updateGhostPiece();
            
            if (TouchInput.isReleased() || (Input.isTriggered('ok') && this._isKeyboardControllingCursor)) this.placeHeldPiece();
            if ((Input.isTriggered('shift') || Input.isTriggered('cancel')) && this._isKeyboardControllingCursor) {
                this.cancelDrag(); playSound(Sounds.cancel);
            }
        }
        
        pickupPiece(index, isKeyboard = false) {
            if (!this._currentPieces[index]) return;
            this._isDragging = true; this._draggedPiece = this._currentPieces[index];
            this._draggedPieceIndex = index; this._isKeyboardControllingCursor = isKeyboard;
            if (isKeyboard) { TouchInput._x = Graphics.width / 2; TouchInput._y = Graphics.height / 2; }
            this._pieceSlots[index].visible = false;
            this.drawPieceInContainer(this._draggedPiece, this._draggedPieceContainer);
            this._draggedPieceContainer.x = TouchInput.x; this._draggedPieceContainer.y = TouchInput.y;
            this.updateGhostPiece();
            this._draggedPieceContainer.visible = true; this._ghostPiece.visible = true;
            this.updateKeyboardIndicator();
        }

        placeHeldPiece() {
            const dropCoords = this.getDropCoordinates();
            if (this.canPlacePiece(this._draggedPiece, dropCoords.x, dropCoords.y)) {
                this.placePieceOnBoard(this._draggedPiece, dropCoords.x, dropCoords.y);
                playSound(Sounds.place);
                let blocksInPiece = 0;
                this._draggedPiece.shape.forEach(row => row.forEach(cell => { if (cell === 1) blocksInPiece++; }));
                this._score += blocksInPiece * 10; this.updateScoreText();
                this._currentPieces[this._draggedPieceIndex] = null;
                this.checkForLineClears();
            } else {
                this._pieceSlots[this._draggedPieceIndex].visible = true;
                playSound(Sounds.cantPlace);
            }
            this.resetDragState();
        }
        
        cancelDrag() { this._pieceSlots[this._draggedPieceIndex].visible = true; this.resetDragState(); }

        resetDragState() {
            this._isDragging = false; this._draggedPiece = null; this._draggedPieceIndex = -1;
            this._isKeyboardControllingCursor = false; this._draggedPieceContainer.visible = false;
            this._ghostPiece.visible = false; this.updateKeyboardIndicator();
        }

        updateKeyboardIndicator() {
            if (!this._gameActive || this._isDragging) { this._keyboardIndicator.visible = false; return; }
            const hasAnyPieces = this._currentPieces.some(p => p !== null);
            if (!hasAnyPieces) { this._keyboardIndicator.visible = false; return; }
            if (this._currentPieces[this._keyboardSelectedPieceIndex] === null) {
                this._keyboardSelectedPieceIndex = this.findNextAvailablePiece(this._keyboardSelectedPieceIndex, 1);
            }
            const targetSlot = this._pieceSlots[this._keyboardSelectedPieceIndex];
            this._keyboardIndicator.visible = true;
            this._keyboardIndicator.x = targetSlot.x; this._keyboardIndicator.y = targetSlot.y;
        }

        isPointInContainer(point, container) {
            const localPoint = container.toLocal(new PIXI.Point(point.x, point.y));
            return container.getLocalBounds().contains(localPoint.x, localPoint.y);
        }
        
        getRandomWeightedPiece() {
            const totalWeight = Object.values(this._pieceWeights).reduce((sum, weight) => sum + weight, 0);
            if (totalWeight <= 0) return PIECE_KEYS[0];
            let random = Math.random() * totalWeight;
            for (const key in this._pieceWeights) {
                random -= this._pieceWeights[key];
                if (random <= 0) return key;
            }
            return PIECE_KEYS[0];
        }

        generateNewPieces() {
            this._currentPieces = [];
            for (let i = 0; i < 4; i++) {
                const randomKey = this.getRandomWeightedPiece();
                this._currentPieces.push(JSON.parse(JSON.stringify(PIECE_SHAPES[randomKey])));
                this.drawPieceInSlot(this._currentPieces[i], i);
                this._pieceSlots[i].visible = true;
            }
            this.updateKeyboardIndicator();
            if (this.isGameOver()) this.endGame(uiTexts.endNoMoves);
        }
        
        drawPieceInSlot(pieceData, slotIndex) {
            const slot = this._pieceSlots[slotIndex];
            if (slot.children.length > 1) slot.removeChildAt(1);
            if (!pieceData) return;
            const pieceContainer = new PIXI.Container(); const shape = pieceData.shape; const tempBlockSize = 24;
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x] === 1) {
                        const block = new PIXI.Graphics(); this.drawBlock(block, 1, pieceData.color);
                        block.scale.set(tempBlockSize / BLOCK_SIZE);
                        block.x = x * tempBlockSize; block.y = y * tempBlockSize;
                        pieceContainer.addChild(block);
                    }
                }
            }
            pieceContainer.x = -pieceContainer.width / 2; pieceContainer.y = -pieceContainer.height / 2;
            slot.addChild(pieceContainer);
        }

        drawPieceInContainer(pieceData, container) {
            container.removeChildren(); if (!pieceData) return;
            const shape = pieceData.shape;
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x] === 1) {
                        const block = new PIXI.Graphics(); this.drawBlock(block, 1, pieceData.color);
                        block.x = x * BLOCK_SIZE; block.y = y * BLOCK_SIZE;
                        container.addChild(block);
                    }
                }
            }
            container.pivot.x = container.width / 2; container.pivot.y = container.height / 2;
        }
        
        updateGhostPiece() {
            if (!this._draggedPiece) return;
            const dropCoords = this.getDropCoordinates(); const gridX = dropCoords.x; const gridY = dropCoords.y;
            this._ghostPiece.removeChildren();
            const canPlace = this.canPlacePiece(this._draggedPiece, gridX, gridY); const shape = this._draggedPiece.shape;
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x] === 1) {
                        const ghostBlock = new PIXI.Graphics();
                        const color = canPlace ? this._draggedPiece.color : 0; const alpha = canPlace ? 0.6 : 0.8;
                        this.drawBlock(ghostBlock, 1, color, alpha);
                        ghostBlock.x = (gridX + x) * (BLOCK_SIZE + BLOCK_SPACING); ghostBlock.y = (gridY + y) * (BLOCK_SIZE + BLOCK_SPACING);
                        this._ghostPiece.addChild(ghostBlock);
                    }
                }
            }
        }

        getDropCoordinates() {
            const touchPos = new PIXI.Point(TouchInput.x, TouchInput.y);
            const pivot = this._draggedPieceContainer.pivot;
            const localPos = this._boardContainer.toLocal(new PIXI.Point(touchPos.x - pivot.x, touchPos.y - pivot.y));
            return { x: Math.round(localPos.x / (BLOCK_SIZE + BLOCK_SPACING)), y: Math.round(localPos.y / (BLOCK_SIZE + BLOCK_SPACING)) };
        }
        
        canPlacePiece(pieceData, startX, startY) {
            if (!pieceData) return false;
            const shape = pieceData.shape;
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x] === 1) {
                        const boardX = startX + x; const boardY = startY + y;
                        if (boardX < 0 || boardX >= this._boardWidth || boardY < 0 || boardY >= this._boardHeight) return false;
                        if (this._board[boardY][boardX] !== 0) return false;
                    }
                }
            }
            return true;
        }
        
        placePieceOnBoard(pieceData, startX, startY) {
            const shape = pieceData.shape;
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x] === 1) {
                        const boardX = startX + x; const boardY = startY + y;
                        this._board[boardY][boardX] = { colorIndex: pieceData.color };
                        this.drawBlock(this._boardSprites[boardY][boardX], 1, pieceData.color);
                    }
                }
            }
        }
        
        checkForLineClears() {
            this._isProcessing = true; let clearedRows = []; let clearedCols = [];
            for (let y = 0; y < this._boardHeight; y++) { if (this._board[y].every(cell => cell !== 0)) clearedRows.push(y); }
            for (let x = 0; x < this._boardWidth; x++) {
                let isFull = true;
                for (let y = 0; y < this._boardHeight; y++) { if (this._board[y][x] === 0) { isFull = false; break; } }
                if (isFull) clearedCols.push(x);
            }
            const totalLines = clearedRows.length + clearedCols.length;
            if (totalLines > 0) {
                let lineBonus = totalLines * 100; if (totalLines > 1) lineBonus += (totalLines - 1) * 100;
                this._score += lineBonus; this.updateScoreText(); playSound(Sounds.lineClear);
                this.animateAndClearLines(clearedRows, clearedCols, () => this.finishTurn());
            } else { this.finishTurn(); }
        }
        
        animateAndClearLines(rows, cols, callback) {
            const allSprites = [];
            rows.forEach(y => { for (let x = 0; x < this._boardWidth; x++) allSprites.push(this._boardSprites[y][x]); });
            cols.forEach(x => { for (let y = 0; y < this._boardHeight; y++) { if (!rows.includes(y)) allSprites.push(this._boardSprites[y][x]); } });
            let flashes = 4;
            const flashInterval = setInterval(() => {
                allSprites.forEach(s => s.visible = !s.visible);
                flashes--;
                if (flashes <= 0) { clearInterval(flashInterval); allSprites.forEach(s => s.visible = true); this.clearLines(rows, cols); callback(); }
            }, 100);
        }

        clearLines(rows, cols) {
             rows.forEach(y => { for (let x = 0; x < this._boardWidth; x++) { this._board[y][x] = 0; this.drawBlock(this._boardSprites[y][x], 0); } });
             cols.forEach(x => { for (let y = 0; y < this._boardHeight; y++) { if (this._board[y][x] !== 0) { this._board[y][x] = 0; this.drawBlock(this._boardSprites[y][x], 0); } } });
        }
        
        finishTurn() {
             if (this._currentPieces.every(p => p === null)) this.generateNewPieces();
             else { this.updateKeyboardIndicator(); if (this.isGameOver()) this.endGame(uiTexts.endNoMoves); }
            this._isProcessing = false;
        }

        isGameOver() {
            const remainingPieces = this._currentPieces.filter(p => p !== null);
            if (remainingPieces.length === 0) return false;
            for (const piece of remainingPieces) {
                for (let y = 0; y < this._boardHeight; y++) {
                    for (let x = 0; x < this._boardWidth; x++) { if (this.canPlacePiece(piece, x, y)) return false; }
                }
            }
            return true;
        }

        endGame(message) {
            if (!this._gameActive) return;
            this._gameActive = false; 
            this._ghostPiece.visible = 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.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 
            });
            
            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.addChild(endText);
            
            this._waitingForInput = true;
            this._endGameHandler = () => {
                if (this._waitingForInput) {
                    if (Input.isTriggered('ok') || Input.isTriggered('cancel') || TouchInput.isTriggered()) {
                        this._waitingForInput = false;
                        document.removeEventListener('keydown', this._endGameHandler);
                        document.removeEventListener('mousedown', this._endGameHandler);
                        SceneManager.pop();
                    }
                }
            };
            
            document.addEventListener('keydown', this._endGameHandler);
            document.addEventListener('mousedown', this._endGameHandler);
            this.update = this._endGameHandler;
        }
    }
})();