/*:
 * @target MZ
 * @plugindesc v8.0 CG Viewer + Gallery (zoom, pan, setting tool)
 * @author MEGAokie
 * @url 
 * @license MIT
 *
 * @param ---BasicSettings---
 * @text ── General Settings ─────────────
 * @type string
 * @default
 * @desc
 *
 * @param UnlockMode
 * @text CG Unlock Mode
 * @type select
 * @option Per-save slot, auto-save (saved immediately on UnlockCG; persists after Game Over)
 * @value slot
 * @option Per-save slot, manual save only (saved only when player saves manually)
 * @value slot-manual
 * @option Global shared, auto-save (saved immediately on UnlockCG; shared across all saves)
 * @value global
 * @option Global shared, manual save only (saved when player saves; shared across all saves)
 * @value global-manual
 * @default global
 * @desc How unlock data is stored when UnlockCG runs.
 * Auto-save modes keep unlock data even after Game Over or forced close.
 * Note: Auto-save does nothing if no save slot exists yet.
 * Note: Global modes save to a dedicated file (CGViewerMZ_unlocks).
 *
 * @param AllUnlocked
 * @text Unlock All (Debug)
 * @type boolean
 * @default false
 * @desc Debug: display all CGs as unlocked.
 *
 * @param ---GallerySettings---
 * @text ── Gallery ───────────────────────
 * @type string
 * @default
 * @desc
 *
 * @param ThumbCols
 * @text Thumbnail Columns
 * @type number
 * @min 1
 * @max 10
 * @default 3
 * @desc Number of thumbnail columns in the gallery.
 *
 * @param ThumbRows
 * @text Thumbnail Rows
 * @type number
 * @min 1
 * @max 10
 * @default 3
 * @desc Number of thumbnail rows in the gallery.
 *
 * @param ThumbPadding
 * @text Thumbnail Spacing (px)
 * @type number
 * @min 0
 * @max 40
 * @default 1
 * @desc Gap between thumbnail images in pixels.
 * Larger values increase spacing and shrink thumbnails.
 *
 * @param ThumbScaleMode
 * @text Thumbnail Scale Mode
 * @type select
 * @option Bilinear (Sharp)
 * @value bilinear
 * @option Area average (Smooth)
 * @value area
 * @default area
 * @desc Downscaling algorithm used when generating thumbnails.
 * bilinear: PixiJS GPU (same as MZ default). area: high-quality canvas resampling.
 *
 * @param GridMargin
 * @text Grid Margin (px)
 * @type number
 * @min 0
 * @max 60
 * @default 1
 * @desc Outer margin between the screen edge and thumbnails in pixels.
 * Larger values add more padding around the grid.
 *
 * @param ---DescSettings---
 * @text ── Description ─────────────────
 * @type string
 * @default
 * @desc
 *
 * @param DescSettings
 * @text Description Settings
 * @type struct<DescSettings>
 * @default {"height":"30","textColor":"#ffffff","outlineColor":"#000000","outlineWidth":"3","textAlign":"center"}
 * @desc Appearance settings for the description text shown below each thumbnail.
 * Click to open the detail settings.
 *
 * @param FocusColor
 * @text Focus Frame Color
 * @type string
 * @default #ffffff
 * @desc Color of the frame drawn around the focused thumbnail during keyboard navigation.
 * Use a color code (e.g. #ffffff=white, #ffff00=yellow, #00ff00=green).
 *
 * @param GalleryBackground
 * @text Gallery Background Image
 * @type file
 * @dir img/pictures/
 * @desc Background image for the gallery screen.
 * Defaults to black if left empty.
 *
 * @param GalleryBackgroundMode
 * @text Background Image Mode
 * @type select
 * @option Original size, centered
 * @value original
 * @option Stretch to fill
 * @value stretch
 * @option Tile
 * @value tile
 * @default original
 * @desc How the background image is displayed.
 *
 * @param ---UnlockedDisplay---
 * @text ── Locked Display ──────────────
 * @type string
 * @default
 * @desc
 *
 * @param LockedImage
 * @text Locked Slot Image
 * @type file
 * @dir img/pictures/
 * @desc Image shown for locked slots. Displays "LOCKED" text if left empty.
 * Applies to all folders and slots.
 *
 * @param ShowLockedDescription
 * @text Show Description When Locked
 * @type boolean
 * @default false
 * @desc Whether to show the description for locked CGs.
 * If true, the description is shown below "LOCKED".
 *
 * @param ---ViewerSettings---
 * @text ── Viewer Settings ────────────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerSettings
 * @text Viewer Settings
 * @type struct<ViewerSettings>
 * @default {"fadeDuration":"30","zoomEnabled":"true","minZoom":"10","maxZoom":"400","zoomSpeed":"10","panSpeed":"20","doubleClickTime":"300","doubleClickZoom":"200","enablePinch":"true"}
 * @desc Fade, zoom, and pan settings for the viewer.
 * Click to open the detail settings.
 *
 * @param ---TitleSettings---
 * @text ── Title Screen & Other ────────
 * @type string
 * @default
 * @desc
 *
 * @param ShowToolInTitle
 * @text Show Tool on Title Screen
 * @type boolean
 * @default true
 * @desc Show the "CG Setting Tool" command on the title screen (NW.js only).
 *
 * @param ToolCommandName
 * @text Tool Command Name
 * @type string
 * @default CG設定ツール
 * @desc Name of the command displayed on the title screen.
 *
 * @param EventButtonText
 * @text Event Button Label
 * @type string
 * @default イベント再生
 * @desc Label for the common event playback button in the viewer.
 *
 * @param TitleGalleryCommands
 * @text Title Screen Gallery Commands
 * @type struct<TitleGalleryCommand>[]
 * @default []
 * @desc Gallery commands shown on the title screen.
 * Multiple folders can be registered. Global modes only (slot modes show a notice screen).
 *
 * @param ---CounterSettings---
 * @text ── Unlock Counter ───────────────
 * @type string
 * @default
 * @desc
 *
 * @param CounterSettings
 * @text Unlock Counter
 * @type struct<CounterSettings>
 * @default {"show":"true","pos":"{\"x\":\"\",\"y\":\"\"}","fontSize":"18","textColor":"#ffffff","outlineColor":"#ffffff","outlineWidth":"0"}
 * @desc Shows "unlocked / total" in the gallery (e.g. 12 / 30).
 * Click to open the detail settings.
 *
 * @param ---ViewerButtonPos---
 * @text ── Viewer Button Positions ──────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerPrevButtonPos
 * @text Viewer "Prev" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Prev" button in the viewer (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param ViewerNextButtonPos
 * @text Viewer "Next" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Next" button in the viewer (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param ViewerListButtonPos
 * @text Viewer "List" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "List" button in the viewer (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param ViewerExitButtonPos
 * @text Viewer "Exit" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Exit" button in the viewer (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param ViewerEventButtonPos
 * @text Viewer "Play Event" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Play Event" button in the viewer (top-left corner as origin).
 * Leave empty for automatic placement.
 * Note: This button is only shown when the current CG has a common event assigned.
 *
 * @param ---GalleryButtonPos---
 * @text ── Gallery Button Positions ─────
 * @type string
 * @default
 * @desc
 *
 * @param GalleryPrevButtonPos
 * @text Gallery "Prev Page" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Prev Page" button in the gallery (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param GalleryNextButtonPos
 * @text Gallery "Next Page" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Next Page" button in the gallery (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param GalleryExitButtonPos
 * @text Gallery "Exit" Button Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the "Exit" button in the gallery (top-left corner as origin).
 * Leave empty for automatic placement.
 *
 * @param ---ViewerButtonImage---
 * @text ── Viewer Button Images ─────────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerPrevButtonImage
 * @text Viewer "Prev" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Prev" button. Leave empty to use the default text button.
 *
 * @param ViewerNextButtonImage
 * @text Viewer "Next" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Next" button. Leave empty to use the default text button.
 *
 * @param ViewerListButtonImage
 * @text Viewer "List" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "List" button. Leave empty to use the default text button.
 *
 * @param ViewerExitButtonImage
 * @text Viewer "Exit" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Exit" button. Leave empty to use the default text button.
 *
 * @param ViewerEventButtonImage
 * @text Viewer "Play Event" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Play Event" button. Leave empty to use the default text button.
 *
 * @param ---GalleryButtonImage---
 * @text ── Gallery Button Images ────────
 * @type string
 * @default
 * @desc
 *
 * @param GalleryPrevButtonImage
 * @text Gallery "Prev Page" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Prev Page" button. Leave empty to use the default text button.
 *
 * @param GalleryNextButtonImage
 * @text Gallery "Next Page" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Next Page" button. Leave empty to use the default text button.
 *
 * @param GalleryExitButtonImage
 * @text Gallery "Exit" Button Image
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc Image for the "Exit" button. Leave empty to use the default text button.
 *
 * @command OpenViewer
 * @text Open CG Viewer
 * @desc Opens the CG viewer (or gallery) for the specified folder.
 *
 * @arg folder
 * @text Folder Name
 * @type string
 * @default cg01
 * @desc Folder name under img/pictures/ (e.g. cg01)
 *
 * @arg startNumber
 * @text Start Number
 * @type string
 * @desc Leave empty to show the gallery list. Enter a number or \V[n] to start from that CG.
 *
 * @command UnlockCG
 * @text Unlock CG
 * @desc Marks the specified CG(s) as unlocked.
 *
 * @arg folder
 * @text Folder Name
 * @type string
 * @default cg01
 * @desc Folder name under img/pictures/ (e.g. cg01, chapter1/scene01)
 *
 * @arg numbers
 * @text Unlock Numbers
 * @type string
 * @default 1
 * @desc Numbers to unlock (e.g. 1-5,7,9 unlocks 1–5, 7, and 9)
 *
 * @command OpenSettingTool
 * @text Open Setting Tool
 * @desc Opens the CG Setting Tool (NW.js only).
 *
 * @command ShowCGPicture
 * @text Show CG as Picture
 * @desc Displays the specified CG as a picture on screen.
 *
 * @arg folder
 * @text Folder Name
 * @type string
 * @default cg01
 * @desc Folder name under img/pictures/ (e.g. cg01, chapter1/scene01). \V[n] supported.
 *
 * @arg number
 * @text CG Number
 * @type string
 * @default 1
 * @desc CG number to display. \V[n] supported.
 *
 * @arg pictureId
 * @text Picture ID
 * @type string
 * @default 1
 * @desc Target picture slot number. \V[n] supported.
 *
 * @arg origin
 * @text Origin
 * @type select
 * @option Top-left
 * @value 0
 * @option Center
 * @value 1
 * @default 1
 * @desc Origin point of the picture.
 *
 * @arg x
 * @text X Position
 * @type string
 * @desc X coordinate (leave empty for screen center). \V[n] supported.
 *
 * @arg y
 * @text Y Position
 * @type string
 * @desc Y coordinate (leave empty for screen center). \V[n] supported.
 *
 * @arg scaleX
 * @text Scale X (%)
 * @type string
 * @default 100
 * @desc Horizontal scale. \V[n] supported.
 *
 * @arg scaleY
 * @text Scale Y (%)
 * @type string
 * @default 100
 * @desc Vertical scale. \V[n] supported.
 *
 * @arg opacity
 * @text Opacity
 * @type string
 * @default 255
 * @desc Opacity 0–255. \V[n] supported.
 *
 * @arg blendMode
 * @text Blend Mode
 * @type select
 * @option Normal
 * @value 0
 * @option Additive
 * @value 1
 * @option Multiply
 * @value 2
 * @option Screen
 * @value 3
 * @default 0
 * @desc Blend mode for the picture.
 *
 * @help
 * ■ Overview
 * This plugin provides a CG viewer and gallery system.
 * CG lists are managed via data/CGList.json.
 *
 * ■ CGList.json Format
 * Simple format:
 * {
 *   "cg01": [1, 2, 3, 4, 5],
 *   "chapter1/cave1": [1, 2, 3]
 * }
 * Detailed format:
 * {
 *   "cg01": [
 *     { "number": 1, "description": "Caption text", "commonEventId": 5 }
 *   ]
 * }
 * Folder names are paths relative to img/pictures/.
 *
 * ■ Basic Usage
 * 1. Use the "UnlockCG" plugin command to unlock CGs.
 * 2. Use the "OpenViewer" plugin command to open the viewer.
 * 3. Use the "ShowCGPicture" plugin command to display a CG as a picture.
 *
 * ■ Key Features
 * - CG unlock system (choose behavior via the "CG Unlock Mode" parameter)
 *   · Per-slot auto-save: written to the current save slot immediately on UnlockCG
 *   · Per-slot manual save: saved only when the player saves manually
 *   · Global auto-save: written to a dedicated file immediately on UnlockCG
 *   · Global manual save: saved to a dedicated file when the player saves
 *   Auto-save modes preserve unlock data even after Game Over.
 *   Note: auto-save does nothing if no save slot exists yet.
 *   Note: global mode saves to CGViewerMZ_unlocks.rmmzsave in the save folder.
 * - CG viewer (browse unlocked CGs)
 * - Gallery (thumbnail grid view)
 * - Zoom / pan / scroll support
 * - Auto-preload (pre-fetches adjacent images for smooth browsing)
 * - \V[n] variable references in folder names, CG numbers, etc.
 *
 * ■ Controls
 * 【Viewer (when zoom is enabled)】
 * Keyboard:
 *   ← → : Previous / Next CG
 *   ↑ ↓  or  PageUp/Down : Zoom in / out
 *   Shift + Arrow keys : Pan image (when zoomed)
 *   Tab : Reset to 100%
 *   Enter / Z : Toggle fit ↔ 100%
 *   R : Play common event (if assigned)
 *   Backspace : Return to gallery list
 *   Esc : Exit
 *
 * Mouse:
 *   Scroll up : Zoom out
 *   Scroll down : Zoom in (centered on cursor)
 *   Left drag : Pan image (when zoomed)
 *   Double-click : Toggle zoom ↔ fit
 *   Single click : Show / hide menu (auto-distinguished from drag)
 *
 * Touch (smartphone / tablet):
 *   Pinch : Zoom in / out
 *   Drag : Pan image (when zoomed)
 *   Double-tap : Toggle zoom ↔ fit
 *   Single tap : Show / hide menu (auto-distinguished from drag)
 *
 * 【Gallery (thumbnail view)】
 * Keyboard:
 *   Arrow keys : Move focus
 *   Enter / Z : Open selected CG
 *   PageUp / Down : Change page
 *   Esc : Exit
 *
 * Mouse / Touch:
 *   Click thumbnail : Open CG
 *   Click button : Change page / Exit
 *
 * ■ Notes
 * - Images must be placed at img/pictures/(folder)/(number).png
 *   Example: img/pictures/cg01/1.png
 * - The zoom feature slightly increases memory usage.
 *
 * ■ Custom Thumbnail Images (optional)
 * You can provide separate thumbnail images for the gallery.
 *   Location: img/pictures/(folder)/thumb/(number).png
 *   Example: img/pictures/cg01/thumb/1.png
 * If a thumb/ image exists for a number, it is used as the thumbnail.
 * Otherwise the full-size image is used as-is (no thumb/ folder required).
 *
 * ■ Description Text
 * Set "Description Height" to 1 or more and add a "description" field
 * to each CG entry in CGList.json to show descriptions below thumbnails.
 * Font color, outline, and alignment are configured in the Description Settings.
 *
 * ■ Unlock Counter
 * Shows "unlocked / total" at the bottom of the gallery (e.g. 12 / 30).
 * Turn off "Show" in Unlock Counter settings to hide it.
 * Set X/Y in "Position" to move it to a custom location.
 *
 * ■ Button Positions
 * Each button in the viewer and gallery can be moved to a custom position.
 * Enter X/Y coordinates in the corresponding "Button Position" parameter.
 * Coordinates are relative to the button's top-left corner.
 * Leave empty to use the default automatic layout.
 * You can set only X or only Y independently.
 *
 * ■ Custom Button Images
 * Each button in the viewer and gallery can be replaced with a custom image.
 * Set the image file in the corresponding "Button Image" parameter.
 * Place images in the img/pictures/ folder.
 * Set "Width" and "Height" to 0 to use the image's original size.
 * Buttons without an image set will continue to use the default text button.
 * Button positions apply to image buttons the same way as text buttons.
 *
 * ■ Title Screen Gallery
 * Add entries to "Title Screen Gallery Commands" to launch the gallery
 * directly from the title screen.
 * · Global modes: selecting the command opens the gallery.
 * · Slot modes: selecting the command shows a notice screen instead.
 *
 * ■ External API
 * For custom title screens, you can open the gallery directly:
 *   Scene_CGGallery.open("folderName");
 *   Example: Scene_CGGallery.open("cg01");
 * Note: only works correctly in global unlock modes.
 *
 * ■ License
 * MIT License
 * Copyright (c) 2026 MEGAokie
 * Free to use, modify, and redistribute.
 * See https://opensource.org/licenses/MIT for details.
 *
 */

/*:ja
 * @target MZ
 * @plugindesc v8.0 CGビューワー＋ギャラリー（拡大機能・設定ツール付き）
 * @author MEGAokie
 * @url 
 * @license MIT
 *
 * @param ---BasicSettings---
 * @text ── 基本設定 ──────────────────
 * @type string
 * @default
 * @desc
 *
 * @param UnlockMode
 * @text CG開放管理モード
 * @type select
 * @option スロット個別・即時保存（UnlockCG実行時に開放状態が保存。ゲームオーバー後も消えない）
 * @value slot
 * @option スロット個別・手動セーブのみ（手動セーブ時にのみ開放状態が保存）
 * @value slot-manual
 * @option 全体共有・即時保存（UnlockCG実行時に開放状態が保存。全セーブデータで共有・ゲームオーバー後も消えない）
 * @value global
 * @option 全体共有・手動セーブのみ（手動セーブ時にのみ開放状態が保存。全セーブデータで共有）
 * @value global-manual
 * @default global
 * @desc UnlockCG実行時の開放情報の管理方法。
 * 即時保存モードはゲームオーバー・強制終了後も開放情報が維持されます。
 * ※即時保存モードは書き戻し先スロットが存在しない場合は保存されません。
 * ※全体共有モードはプラグイン専用ファイル（CGViewerMZ_unlocks）に保存されます。
 *
 * @param AllUnlocked
 * @text 全開放モード
 * @type boolean
 * @default false
 * @desc デバッグ用：全CGを開放状態で表示
 *
 * @param ---GallerySettings---
 * @text ── ギャラリー ────────────────
 * @type string
 * @default
 * @desc
 *
 * @param ThumbCols
 * @text サムネイル列数
 * @type number
 * @min 1
 * @max 10
 * @default 3
 * @desc ギャラリーのサムネイル列数
 *
 * @param ThumbRows
 * @text サムネイル行数
 * @type number
 * @min 1
 * @max 10
 * @default 3
 * @desc ギャラリーのサムネイル行数
 *
 * @param ThumbPadding
 * @text サムネイル間隔(px)
 * @type number
 * @min 0
 * @max 40
 * @default 1
 * @desc サムネイル画像と画像の間のすき間（ピクセル数）。
 * 数値を大きくすると画像同士の間隔が広くなり、その分サムネイルが小さくなります。
 *
 * @param ThumbScaleMode
 * @text サムネイル縮小方式
 * @type select
 * @option バイリニア（シャープ）
 * @value bilinear
 * @option エリア平均（なめらか）
 * @value area
 * @default area
 * @desc サムネイルをフルサイズ画像から生成する際の縮小アルゴリズム。
 * bilinear: PixiJS GPU処理（MZデフォルトと同等）。area: Canvas高品質リサンプリング。
 *
 * @param GridMargin
 * @text 外周の余白(px)
 * @type number
 * @min 0
 * @max 60
 * @default 1
 * @desc ギャラリー画面の外周（画面の端とサムネイルの間）の余白（ピクセル数）。
 * 数値を大きくすると外側の余白が広くなります。
 *
 * @param ---DescSettings---
 * @text ── 説明文の設定 ────────────
 * @type string
 * @default
 * @desc
 *
 * @param DescSettings
 * @text 説明文の設定
 * @type struct<DescSettings>
 * @default {"height":"30","textColor":"#ffffff","outlineColor":"#000000","outlineWidth":"3","textAlign":"center"}
 * @desc 各サムネイル下部に表示する説明文の見た目を設定します。
 * クリックして詳細を設定できます。
 *
 * @param FocusColor
 * @text フォーカス枠の色
 * @type string
 * @default #ffffff
 * @desc ギャラリーでキーボード操作時に選択中のサムネイルを囲む枠線の色。
 * 色コード形式で指定（例: #ffffff=白、#ffff00=黄色、#00ff00=緑）
 *
 * @param GalleryBackground
 * @text ギャラリー背景画像
 * @type file
 * @dir img/pictures/
 * @desc ギャラリー（一覧画面）の背景に表示する画像。
 * 空欄の場合は黒背景になります。
 *
 * @param GalleryBackgroundMode
 * @text 背景画像の表示方法
 * @type select
 * @option 元サイズで中央配置
 * @value original
 * @option 画面全体に引き伸ばし
 * @value stretch
 * @option タイル状に繰り返し
 * @value tile
 * @default original
 * @desc 背景画像の表示方法を選択します。
 *
 * @param ---UnlockedDisplay---
 * @text ── 未開放の表示 ─────────────
 * @type string
 * @default
 * @desc
 *
 * @param LockedImage
 * @text 未開放時の代替画像
 * @type file
 * @dir img/pictures/
 * @desc 未開放スロットに表示する画像。空欄の場合はLOCKEDテキストを表示します。
 * 全フォルダ・全スロット共通で使用されます。
 *
 * @param ShowLockedDescription
 * @text 未開放時も説明文表示
 * @type boolean
 * @default false
 * @desc 未開放のCGでも説明文を表示するか。
 * trueにすると「LOCKED」の下に説明文が表示されます。
 *
 * @param ---ViewerSettings---
 * @text ── ビューアーの設定 ──────────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerSettings
 * @text ビューアーの設定
 * @type struct<ViewerSettings>
 * @default {"fadeDuration":"30","zoomEnabled":"true","minZoom":"10","maxZoom":"400","zoomSpeed":"10","panSpeed":"20","doubleClickTime":"300","doubleClickZoom":"200","enablePinch":"true"}
 * @desc ビューアーのフェード時間・拡大縮小・移動に関する設定をまとめて行います。
 * クリックして詳細を設定できます。
 *
 * @param ---TitleSettings---
 * @text ── タイトル画面・その他 ──────
 * @type string
 * @default
 * @desc
 *
 * @param ShowToolInTitle
 * @text タイトル画面にツール表示
 * @type boolean
 * @default true
 * @desc タイトル画面に「CG設定ツール」コマンドを表示（NW.js環境のみ）
 *
 * @param ToolCommandName
 * @text ツールコマンド名
 * @type string
 * @default CG設定ツール
 * @desc タイトル画面に表示するコマンド名
 *
 * @param EventButtonText
 * @text イベント再生ボタン
 * @type string
 * @default イベント再生
 * @desc ビューアーに表示するコモンイベント再生ボタンのテキスト
 *
 * @param TitleGalleryCommands
 * @text タイトル画面ギャラリーコマンド
 * @type struct<TitleGalleryCommand>[]
 * @default []
 * @desc タイトル画面に表示するギャラリーコマンドの一覧。
 * 複数フォルダを登録すると複数コマンドが表示されます。global系モード専用（slot系は案内画面に遷移）。
 *
 * @param ---CounterSettings---
 * @text ── 開放済み枚数の表示 ─────────
 * @type string
 * @default
 * @desc
 *
 * @param CounterSettings
 * @text 開放済み枚数の表示
 * @type struct<CounterSettings>
 * @default {"show":"true","pos":"{\"x\":\"\",\"y\":\"\"}","fontSize":"18","textColor":"#ffffff","outlineColor":"#ffffff","outlineWidth":"0"}
 * @desc ギャラリー画面に「開放済み枚数 / 全枚数」を表示する設定です。
 * クリックして詳細を設定できます。例: 12 / 30
 *
 * @param ---ViewerButtonPos---
 * @text ── ビューアー　ボタンの位置 ──────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerPrevButtonPos
 * @text ビューアー「前」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ビューアーの「前」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param ViewerNextButtonPos
 * @text ビューアー「次」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ビューアーの「次」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param ViewerListButtonPos
 * @text ビューアー「一覧」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ビューアーの「一覧」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param ViewerExitButtonPos
 * @text ビューアー「終了」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ビューアーの「終了」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param ViewerEventButtonPos
 * @text ビューアー「イベント再生」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ビューアーの「イベント再生」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 * ※このボタンはCGにコモンイベントが設定されている場合のみ表示されます。
 *   コモンイベントのないCGに切り替えた場合は非表示になります。
 *
 * @param ---GalleryButtonPos---
 * @text ── ギャラリー　ボタンの位置 ──────
 * @type string
 * @default
 * @desc
 *
 * @param GalleryPrevButtonPos
 * @text ギャラリー「前ページ」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ギャラリーの「前ページ」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param GalleryNextButtonPos
 * @text ギャラリー「次ページ」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ギャラリーの「次ページ」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param GalleryExitButtonPos
 * @text ギャラリー「終了」ボタンの位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc ギャラリーの「終了」ボタンの位置（左上角が基準）。
 * 空欄にすると自動で配置されます。
 *
 * @param ---ViewerButtonImage---
 * @text ── ビューアー　ボタンの画像 ──────
 * @type string
 * @default
 * @desc
 *
 * @param ViewerPrevButtonImage
 * @text ビューアー「前」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ビューアーの「前」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param ViewerNextButtonImage
 * @text ビューアー「次」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ビューアーの「次」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param ViewerListButtonImage
 * @text ビューアー「一覧」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ビューアーの「一覧」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param ViewerExitButtonImage
 * @text ビューアー「終了」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ビューアーの「終了」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param ViewerEventButtonImage
 * @text ビューアー「イベント再生」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ビューアーの「イベント再生」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param ---GalleryButtonImage---
 * @text ── ギャラリー　ボタンの画像 ──────
 * @type string
 * @default
 * @desc
 *
 * @param GalleryPrevButtonImage
 * @text ギャラリー「前ページ」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ギャラリーの「前ページ」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param GalleryNextButtonImage
 * @text ギャラリー「次ページ」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ギャラリーの「次ページ」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @param GalleryExitButtonImage
 * @text ギャラリー「終了」ボタンの画像
 * @type struct<ButtonImage>
 * @default {"image":"","width":"0","height":"0"}
 * @desc ギャラリーの「終了」ボタンに使う画像。空欄にするとテキストボタンのまま。
 *
 * @command OpenViewer
 * @text CGビューアーを開く
 * @desc 指定したフォルダのCGビューアー（またはギャラリー）を開きます
 *
 * @arg folder
 * @text フォルダ名
 * @type string
 * @default cg01
 * @desc img/pictures/配下のフォルダ名（例: cg01）
 *
 * @arg startNumber
 * @text 開始番号
 * @type string
 * @desc 空欄にすると一覧表示。数字もしくは変数\V[n]の値にするとその番号のCGから表示が開始。
 *
 * @command UnlockCG
 * @text CGを開放する
 * @desc 指定したCGを開放状態にします
 *
 * @arg folder
 * @text フォルダ名
 * @type string
 * @default cg01
 * @desc img/pictures/配下のフォルダ名（例: cg01, chapter1/scene01）
 *
 * @arg numbers
 * @text 開放番号
 * @type string
 * @default 1
 * @desc 開放する番号（例: 1-5,7,9 で1～5番と7番と9番を開放）
 *
 * @command OpenSettingTool
 * @text CG設定ツールを開く
 * @desc CG設定ツールを開きます（NW.js環境のみ）
 *
 * @command ShowCGPicture
 * @text 指定CGをピクチャ表示
 * @desc 指定したCGをピクチャとして画面に表示します
 *
 * @arg folder
 * @text フォルダ名
 * @type string
 * @default cg01
 * @desc img/pictures/配下のフォルダ名（例: cg01, chapter1/scene01）。\V[n]で変数参照可能
 *
 * @arg number
 * @text CG番号
 * @type string
 * @default 1
 * @desc 表示するCG番号（\V[n]で変数参照可能）
 *
 * @arg pictureId
 * @text ピクチャ番号
 * @type string
 * @default 1
 * @desc 表示先のピクチャ番号（\V[n]で変数参照可能）
 *
 * @arg origin
 * @text 原点
 * @type select
 * @option 左上
 * @value 0
 * @option 中心
 * @value 1
 * @default 1
 * @desc ピクチャの原点位置
 *
 * @arg x
 * @text X座標
 * @type string
 * @desc X座標（空欄で画面中央、\V[n]で変数参照可能）
 *
 * @arg y
 * @text Y座標
 * @type string
 * @desc Y座標（空欄で画面中央、\V[n]で変数参照可能）
 *
 * @arg scaleX
 * @text 拡大率X(%)
 * @type string
 * @default 100
 * @desc 横方向の拡大率（\V[n]で変数参照可能）
 *
 * @arg scaleY
 * @text 拡大率Y(%)
 * @type string
 * @default 100
 * @desc 縦方向の拡大率（\V[n]で変数参照可能）
 *
 * @arg opacity
 * @text 不透明度
 * @type string
 * @default 255
 * @desc 不透明度 0～255（\V[n]で変数参照可能）
 *
 * @arg blendMode
 * @text 合成方法
 * @type select
 * @option 通常
 * @value 0
 * @option 加算
 * @value 1
 * @option 乗算
 * @value 2
 * @option スクリーン
 * @value 3
 * @default 0
 * @desc 合成方法
 *
 * @help
 * ■概要
 * CGビューアーとギャラリー機能を提供するプラグインです。
 * data/CGList.json でCGリストを管理します。
 *
 * ■CGList.jsonの形式
 * {
 *   "cg01": [1, 2, 3, 4, 5],
 *   "chapter1/cave1": [1, 2, 3]
 * }
 * または詳細形式:
 * {
 *   "cg01": [
 *     { "number": 1, "description": "説明", "commonEventId": 5 }
 *   ]
 * }
 * ※フォルダ名は img/pictures/ 配下のパス
 *
 * ■使用方法
 * 1. プラグインコマンド「UnlockCG」でCGを開放
 * 2. プラグインコマンド「OpenViewer」でビューアーを開く
 * 3. プラグインコマンド「ShowCGPicture」でCGをピクチャ表示
 *
 * ■主要機能
 * - CG解放システム（プラグインパラメーター「CG開放管理モード」で動作を選択）
 *   ・スロット個別・即時保存: UnlockCG実行時に現在スロットへ即時書き戻し
 *   ・スロット個別・手動セーブのみ: 手動セーブ時のみ保存（従来動作）
 *   ・全体共有・即時保存: UnlockCG実行時にプラグイン専用ファイルへ即時保存
 *   ・全体共有・手動セーブのみ: 手動セーブ時にプラグイン専用ファイルへ保存
 *   即時保存モードはゲームオーバー後も開放情報が維持されます。
 *   ※書き戻し先スロットが存在しない場合（一度もセーブしていない等）は保存されません。
 *   ※全体共有モードのファイルはsaveフォルダ内のCGViewerMZ_unlocks.rmmzsaveです。
 * - ビューアー（開放済みCGを閲覧）
 * - ギャラリー（サムネイル一覧表示）
 * - 拡大/縮小/移動機能
 * - 自動プリロード（前後の画像を先読みしてスムーズ表示）
 * - \V[n]による変数参照（フォルダ名、CG番号等）
 *
 * ■操作方法
 * 【ビューアー（拡大機能有効時）】
 * キーボード：
 *   ← → : 前/次のCG
 *   ↑ ↓ または PageUp/Down : 拡大/縮小
 *   Shift + 矢印キー : 画像移動（拡大時）
 *   Tab : 100%にリセット
 *   Enter/Z : フィット⇔100%切り替え
 *   R : コモンイベント再生（設定済みの場合）
 *   Backspace : 一覧に戻る
 *   Esc : 終了
 *
 * マウス：
 *   ホイール上回し : 縮小
 *   ホイール下回し : 拡大（カーソル位置中心）
 *   左ドラッグ : 画像移動（拡大時）
 *   ダブルクリック : 拡大⇔フィット切り替え
 *   シングルクリック : メニュー表示/非表示（ドラッグと自動判別）
 *
 * スマホ/タブレット：
 *   ピンチ : 拡大/縮小
 *   ドラッグ : 画像移動（拡大時）
 *   ダブルタップ : 拡大⇔フィット切り替え
 *   シングルタップ : メニュー表示/非表示（ドラッグと自動判別）
 *
 * 【ギャラリー（一覧画面）】
 * キーボード：
 *   矢印キー : フォーカス移動
 *   Enter/Z : 選択したCGを開く
 *   PageUp/Down : ページ切り替え
 *   Esc : 終了
 *
 * マウス/タッチ：
 *   サムネイルクリック : CGを開く
 *   ボタンクリック : ページ切り替え・終了
 *
 * ■注意事項
 * - 画像ファイルは img/pictures/（フォルダ名）/番号.png の形式で配置
 *   例: img/pictures/cg01/1.png
 * - 拡大機能により、メモリ使用量が若干増加します
 *
 * ■サムネイル独立指定（オプション）
 * ギャラリー一覧に表示するサムネイル専用画像を用意できます。
 * 　配置場所: img/pictures/（フォルダ名）/thumb/番号.png
 * 　例: img/pictures/cg01/thumb/1.png
 * thumb/ フォルダに画像がある番号はそちらをサムネイルとして使用し、
 * ない番号は元画像をそのままサムネイル表示します（従来どおり）。
 * ※ thumbフォルダを作らなくても問題ありません。
 *
 * ■説明文の表示設定
 * 「説明文エリアの高さ」を1以上に設定し、CGList.jsonの各CGに
 * descriptionフィールドを記述すると説明文が表示されます。
 * 文字色・枠色・文字揃えはプラグインパラメーターで設定できます。
 *
 * ■開放カウンター
 * ギャラリー画面の下部（ボタンの右側）に「開放済み枚数 / 全枚数」を表示します。
 * 例: 12 / 30
 * 「開放カウンターを表示する」をOFFにすると非表示になります。
 * 「開放カウンターの表示位置」でXY座標を指定すると好きな位置に移動できます。
 * 文字色・縁取りは「説明文の文字色」「説明文の枠色」「説明文の枠幅」と共通です。
 *
 * ■ボタンの表示位置
 * ビューアーとギャラリーの各ボタンを好きな位置に移動できます。
 * 「ビューアー「前」ボタンの位置」のようなパラメーターにX/Y座標を入力してください。
 * 座標の基準はボタンの「左上角」です。
 * 空欄にするとデフォルト（自動配置）のままになります。
 * X/Yどちらか一方だけ入力することもできます。
 *
 * ■ボタンの画像差し替え
 * ビューアーとギャラリーの各ボタンをオリジナル画像に変更できます。
 * 「ビューアー「前」ボタンの画像」のようなパラメーターに画像ファイルを指定してください。
 * 画像は img/pictures/ フォルダに置いてください。
 * 「表示幅」「表示高さ」を0にすると画像の元サイズで表示されます。
 * 画像を指定しないボタンは従来のテキストボタンのまま表示されます。
 * ボタンの位置はテキストボタンと同様に「ボタンの位置」パラメーターで調整できます。
 *
 * ■タイトル画面ギャラリー
 * プラグインパラメーター「タイトル画面ギャラリーコマンド」でコマンドを追加すると、
 * タイトル画面からギャラリーを直接起動できます。
 * ・global系モード: コマンド選択でギャラリーが開きます
 * ・slot系モード: コマンドは表示されますが、選択すると案内画面が表示されます
 *
 * ■外部連携API
 * 独自タイトル画面を使用している場合は、以下のstaticメソッドで直接ギャラリーを起動できます。
 *   Scene_CGGallery.open("フォルダ名");
 *   例: Scene_CGGallery.open("cg01");
 * ※global系モードでのみ正常に動作します。
 *
 * ■ライセンス
 * MIT License
 * Copyright (c) 2026 MEGAokie
 * 本プラグインは自由に使用・改変・再配布できます。
 * 詳細は https://opensource.org/licenses/MIT を参照してください。
 *
 */

/*~struct~TitleGalleryCommand:
 * @param label
 * @text Command Name
 * @type string
 * @default Gallery
 * @desc Command name displayed on the title screen.
 *
 * @param folder
 * @text Folder Name
 * @type string
 * @default cg01
 * @desc Gallery folder name under img/pictures/.
 */

/*~struct~ButtonPos:
 * @param x
 * @text X Position
 * @type string
 * @default
 * @desc X coordinate (pixels) from the left edge. Leave empty for automatic placement.
 *
 * @param y
 * @text Y Position
 * @type string
 * @default
 * @desc Y coordinate (pixels) from the top edge. Leave empty for automatic placement.
 */

/*~struct~DescSettings:
 * @param height
 * @text Description Area Height (px)
 * @type number
 * @min 0
 * @max 60
 * @default 30
 * @desc Height of the description area below each thumbnail in pixels.
 * Set to 0 to hide descriptions and maximize thumbnail size.
 *
 * @param textColor
 * @text Text Color
 * @type string
 * @default #ffffff
 * @desc Description text color. Use a color code (e.g. #ffffff=white, #ffff00=yellow).
 *
 * @param outlineColor
 * @text Outline Color
 * @type string
 * @default #000000
 * @desc Description outline color. Use a color code (e.g. #000000=black).
 *
 * @param outlineWidth
 * @text Outline Width (px)
 * @type number
 * @min 0
 * @max 10
 * @default 3
 * @desc Outline thickness in pixels. Set to 0 for no outline.
 *
 * @param textAlign
 * @text Text Alignment
 * @type select
 * @option Left
 * @value left
 * @option Center
 * @value center
 * @option Right
 * @value right
 * @default center
 * @desc Text alignment for the description.
 */

/*~struct~ViewerSettings:
 * @param fadeDuration
 * @text Fade Duration (frames)
 * @type number
 * @min 1
 * @max 120
 * @default 30
 * @desc Number of frames for the fade transition when switching CGs.
 * 1 frame ≈ 1/60 second. Larger values = slower fade.
 *
 * @param zoomEnabled
 * @text Enable Zoom
 * @type boolean
 * @default true
 * @desc Enable zoom, pan, and scroll in the viewer.
 *
 * @param minZoom
 * @text Minimum Zoom (%)
 * @type number
 * @min 10
 * @max 100
 * @default 10
 * @desc Minimum zoom level in percent. 100 means you cannot zoom out below actual size.
 *
 * @param maxZoom
 * @text Maximum Zoom (%)
 * @type number
 * @min 100
 * @max 1000
 * @default 400
 * @desc Maximum zoom level in percent.
 *
 * @param zoomSpeed
 * @text Zoom Speed (%)
 * @type number
 * @min 5
 * @max 50
 * @default 10
 * @desc Zoom change per mouse wheel step or key press (%).
 * Larger values zoom faster per action.
 *
 * @param panSpeed
 * @text Pan Speed (px)
 * @type number
 * @min 10
 * @max 100
 * @default 20
 * @desc Pixels moved per frame when panning with keyboard keys.
 *
 * @param doubleClickTime
 * @text Double-Click Time (ms)
 * @type number
 * @min 100
 * @max 500
 * @default 300
 * @desc Maximum interval in milliseconds to register a double-click.
 * Two clicks within this time count as a double-click.
 *
 * @param doubleClickZoom
 * @text Double-Click Zoom (%)
 * @type number
 * @min 100
 * @max 400
 * @default 200
 * @desc Zoom level applied on double-click in percent.
 *
 * @param enablePinch
 * @text Enable Pinch Gesture
 * @type boolean
 * @default true
 * @desc Enable pinch-in / pinch-out on smartphones and tablets.
 */

/*~struct~CounterSettings:
 * @param show
 * @text Show Counter
 * @type boolean
 * @default true
 * @desc Show "unlocked / total" in the gallery. Example: 12 / 30
 *
 * @param pos
 * @text Position
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc Position of the counter (top-left corner as origin).
 * Leave empty to auto-place to the right of the buttons.
 *
 * @param fontSize
 * @text Font Size (px)
 * @type number
 * @min 8
 * @max 60
 * @default 18
 * @desc Font size for the unlock counter in pixels.
 *
 * @param textColor
 * @text Text Color
 * @type string
 * @default #ffffff
 * @desc Counter text color (e.g. #ffffff=white, #ffff00=yellow).
 *
 * @param outlineColor
 * @text Outline Color
 * @type string
 * @default #ffffff
 * @desc Counter outline color (e.g. #ffffff=white, #000000=black).
 *
 * @param outlineWidth
 * @text Outline Width (px)
 * @type number
 * @min 0
 * @max 10
 * @default 0
 * @desc Counter outline thickness in pixels. Set to 0 for no outline.
 */

/*~struct~ButtonImage:
 * @param image
 * @text Button Image
 * @type file
 * @dir img/pictures/
 * @default
 * @desc Image file to use for the button (place in img/pictures/).
 * Leave empty to keep the default text button.
 *
 * @param width
 * @text Display Width (px)
 * @type number
 * @min 0
 * @default 0
 * @desc Display width of the button image in pixels. Set to 0 to use the image's original width.
 *
 * @param height
 * @text Display Height (px)
 * @type number
 * @min 0
 * @default 0
 * @desc Display height of the button image in pixels. Set to 0 to use the image's original height.
 */

/*~struct~TitleGalleryCommand:
 * @param label
 * @text コマンド名
 * @type string
 * @default ギャラリー
 * @desc タイトル画面に表示するコマンド名
 *
 * @param folder
 * @text フォルダ名
 * @type string
 * @default cg01
 * @desc 開くギャラリーのフォルダ名（img/pictures/配下のパス）
 */

/*~struct~ButtonPos:
 * @param x
 * @text X座標
 * @type string
 * @default
 * @desc 左端のX座標（ピクセル）。空欄にすると自動で配置されます。
 *
 * @param y
 * @text Y座標
 * @type string
 * @default
 * @desc 上端のY座標（ピクセル）。空欄にすると自動で配置されます。
 */

/*~struct~DescSettings:
 * @param height
 * @text 説明文エリアの高さ(px)
 * @type number
 * @min 0
 * @max 60
 * @default 30
 * @desc 各サムネイル下部に確保する説明文の表示エリアの高さ（ピクセル数）。
 * 説明文を使わない場合は0にするとサムネイルが最大サイズになります。
 *
 * @param textColor
 * @text 文字色
 * @type string
 * @default #ffffff
 * @desc 説明文の文字色。色コード形式で指定（例: #ffffff=白、#ffff00=黄色）
 *
 * @param outlineColor
 * @text 枠色
 * @type string
 * @default #000000
 * @desc 説明文の縁取り色。色コード形式で指定（例: #000000=黒）
 *
 * @param outlineWidth
 * @text 枠幅(px)
 * @type number
 * @min 0
 * @max 10
 * @default 3
 * @desc 説明文の縁取りの太さ（ピクセル数）。0で縁取りなし。
 *
 * @param textAlign
 * @text 文字揃え
 * @type select
 * @option 左揃え
 * @value left
 * @option 中央揃え
 * @value center
 * @option 右揃え
 * @value right
 * @default center
 * @desc 説明文の文字揃え
 */

/*~struct~ViewerSettings:
 * @param fadeDuration
 * @text フェード時間(フレーム)
 * @type number
 * @min 1
 * @max 120
 * @default 30
 * @desc CGが切り替わるときのフェード（徐々に消えて出てくる）にかかる時間。
 * 1フレーム = 約1/60秒。数値を大きくするとゆっくり切り替わります。
 *
 * @param zoomEnabled
 * @text 拡大機能を有効化
 * @type boolean
 * @default true
 * @desc ONにするとビューアーでCGを拡大・縮小・移動できます。
 *
 * @param minZoom
 * @text 最小拡大率(%)
 * @type number
 * @min 10
 * @max 100
 * @default 10
 * @desc 縮小できる最小の倍率（%）。100にすると等倍より小さくできなくなります。
 *
 * @param maxZoom
 * @text 最大拡大率(%)
 * @type number
 * @min 100
 * @max 1000
 * @default 400
 * @desc 拡大できる最大の倍率（%）。
 *
 * @param zoomSpeed
 * @text ズーム速度(%)
 * @type number
 * @min 5
 * @max 50
 * @default 10
 * @desc マウスホイールやキー操作でのズーム速度（%）。
 * 数値を大きくすると一度の操作で大きく拡大縮小されます。
 *
 * @param panSpeed
 * @text 移動速度(px)
 * @type number
 * @min 10
 * @max 100
 * @default 20
 * @desc キー操作で拡大中の画像を移動するときの速度（ピクセル/フレーム）。
 *
 * @param doubleClickTime
 * @text ダブルクリック時間(ms)
 * @type number
 * @min 100
 * @max 500
 * @default 300
 * @desc ダブルクリックと判定する間隔（ミリ秒）。
 * この時間内に2回クリックするとダブルクリックになります。
 *
 * @param doubleClickZoom
 * @text ダブルクリック拡大率(%)
 * @type number
 * @min 100
 * @max 400
 * @default 200
 * @desc ダブルクリック時にズームする拡大率（%）。
 *
 * @param enablePinch
 * @text ピンチ操作を有効化
 * @type boolean
 * @default true
 * @desc スマートフォン・タブレットでのピンチイン・ピンチアウト操作を有効にします。
 */

/*~struct~CounterSettings:
 * @param show
 * @text 表示する
 * @type boolean
 * @default true
 * @desc ONにすると「開放済み枚数 / 全枚数」を表示します。例: 12 / 30
 *
 * @param pos
 * @text 表示位置
 * @type struct<ButtonPos>
 * @default {"x":"","y":""}
 * @desc 開放済み枚数の表示位置（左上角が基準）。
 * 空欄にするとボタンの右側に自動配置されます。
 *
 * @param fontSize
 * @text 文字サイズ(px)
 * @type number
 * @min 8
 * @max 60
 * @default 18
 * @desc 開放済み枚数の文字サイズ（ピクセル）。
 *
 * @param textColor
 * @text 文字色
 * @type string
 * @default #ffffff
 * @desc 開放済み枚数の文字色（例: #ffffff=白、#ffff00=黄色）
 *
 * @param outlineColor
 * @text 枠色
 * @type string
 * @default #ffffff
 * @desc 開放済み枚数の縁取り色（例: #ffffff=白、#000000=黒）
 *
 * @param outlineWidth
 * @text 枠幅(px)
 * @type number
 * @min 0
 * @max 10
 * @default 0
 * @desc 開放済み枚数の縁取りの太さ（ピクセル数）。0で縁取りなし。
 */

/*~struct~ButtonImage:
 * @param image
 * @text ボタン画像
 * @type file
 * @dir img/pictures/
 * @default
 * @desc ボタンに使う画像ファイル（img/pictures/に置いてください）。
 * 空欄にするとテキストボタンのまま表示されます。
 *
 * @param width
 * @text 表示幅(px)
 * @type number
 * @min 0
 * @default 0
 * @desc ボタン画像の表示幅（ピクセル）。0にすると画像の元サイズを使用します。
 *
 * @param height
 * @text 表示高さ(px)
 * @type number
 * @min 0
 * @default 0
 * @desc ボタン画像の表示高さ（ピクセル）。0にすると画像の元サイズを使用します。
 */

(() => {
"use strict";

//=============================================================================
// 表示文字列（ここだけ書き換えれば言語変更できます）
// Display strings – edit here to localize
//=============================================================================
const STRINGS = {
    // ビューアーボタン
    btnPrev:        "◀ 前",
    btnNext:        "▶ 次",
    btnList:        "一覧",
    btnExit:        "終了",
    // ギャラリーボタン
    btnPrevPage:    "◀ 前ページ",
    btnNextPage:    "▶ 次ページ",
    btnGalleryExit: "終了",
    btnBack:        "戻る",
    // ゲーム内メッセージ
    noContent:      "開放済みのCGがありません",
    lockedLabel:    "LOCKED",
    // スロットモード案内（1要素＝1行。空文字列は空行）
    slotNotice: [
        "CG開放管理モードがスロット系の場合、開放状態は",
        "セーブスロット単位で管理されるため、",
        "タイトル画面からは起動できません。",
        "",
        "スロット系モードでギャラリーを表示するには、",
        "ゲーム内のマップイベントやメニューから",
        "プラグインコマンド [ OpenViewer ] で起動してください。",
        "",
        "タイトル画面からの起動を優先する場合は、",
        "CG開放管理モードをglobal系に変更する方法もあります。"
    ],
};

//=============================================================================
// 定数定義
//=============================================================================

const PLUGIN_NAME = "CGViewerMZ";

// UI定数
const UI = {
    VIEWER: {
        BUTTON_HEIGHT: 48,
        BUTTON_Y_OFFSET: 80,
        BUTTON_SCREEN_MARGIN: 16,  // 画面端からの余白
        BUTTON_MIN_GAP: 8,         // ボタン間の最小間隔
        BUTTON_MAX_GAP: 24,        // ボタン間の最大間隔
        BUTTON_PADDING: 20,        // ボタン内テキストの左右余白(片側)
        BUTTON_FONT_SIZE: 20,      // 基準フォントサイズ
        BUTTON_MIN_FONT_SIZE: 14   // 最小フォントサイズ
    },
    GALLERY: {
        BUTTON_WIDTH: 140,
        BUTTON_HEIGHT: 48,
        BUTTON_SPACING: 20,
        BUTTON_Y_OFFSET: 60
    }
};

// フェード状態
const FADE_STATE = {
    NONE: "none",
    OUT: "out",
    IN: "in"
};

//=============================================================================
// パラメータ取得
//=============================================================================

const getPluginName = () => {
    const script = document.currentScript;
    if (script && script.src) {
        const match = decodeURIComponent(script.src).match(/([^\/]+)\.js$/);
        if (match) return match[1];
    }
    return PLUGIN_NAME;
};

const pluginName = getPluginName();
const parameters = PluginManager.parameters(pluginName) || {};

const parseBoolean = (value) => {
    if (value === true || value === 1) return true;
    const str = String(value).toLowerCase();
    return str === "true" || str === "1";
};

const parseNumber = (value, defaultValue, min = -Infinity, max = Infinity) => {
    const num = Number(value);
    if (!Number.isFinite(num)) return defaultValue;
    return Math.max(min, Math.min(max, num));
};

/**
 * ButtonPosパラメーターをパースする
 * @param {string} paramName - パラメーター名
 * @returns {{ x: number|null, y: number|null }}
 *   null = 未指定（自動配置）、数値 = 明示指定
 */
const parseButtonPos = (paramName) => {
    try {
        const obj = JSON.parse(parameters[paramName] || "{}");
        const x = (obj.x !== undefined && String(obj.x).trim() !== "") ? Number(obj.x) : null;
        const y = (obj.y !== undefined && String(obj.y).trim() !== "") ? Number(obj.y) : null;
        return { x, y };
    } catch (e) {
        return { x: null, y: null };
    }
};

/**
 * ButtonImageパラメーターをパースする
 * @param {string} paramName - パラメーター名
 * @returns {{ image: string, width: number, height: number }}
 */
const parseButtonImage = (paramName) => {
    try {
        const obj = JSON.parse(parameters[paramName] || "{}");
        return {
            image:  String(obj.image  || ""),
            width:  Number(obj.width  || 0),
            height: Number(obj.height || 0),
        };
    } catch(e) {
        return { image: "", width: 0, height: 0 };
    }
};

// DescSettings struct パース
const _desc = (() => {
    try { return JSON.parse(parameters["DescSettings"] || "{}"); } catch(e) { return {}; }
})();

// ViewerSettings struct パース
const _viewer = (() => {
    try { return JSON.parse(parameters["ViewerSettings"] || "{}"); } catch(e) { return {}; }
})();

// CounterSettings struct パース（posはネストされたstruct<ButtonPos>）
const _counter = (() => {
    try { return JSON.parse(parameters["CounterSettings"] || "{}"); } catch(e) { return {}; }
})();

const _parseCounterPos = (posStr) => {
    try {
        const obj = JSON.parse(posStr || "{}");
        const x = (obj.x !== undefined && String(obj.x).trim() !== "") ? Number(obj.x) : null;
        const y = (obj.y !== undefined && String(obj.y).trim() !== "") ? Number(obj.y) : null;
        return { x, y };
    } catch(e) { return { x: null, y: null }; }
};

const CONFIG = {
    unlockMode: (parameters["UnlockMode"] || "global"),
    allUnlocked: parseBoolean(parameters["AllUnlocked"]),
    thumbCols: parseNumber(parameters["ThumbCols"], 3, 1, 10),
    thumbRows: parseNumber(parameters["ThumbRows"], 3, 1, 10),
    thumbPadding: parseNumber(parameters["ThumbPadding"], 1, 0, 40),
    thumbScaleMode: String(parameters["ThumbScaleMode"] || "area"),
    gridMargin: parseNumber(parameters["GridMargin"], 1, 0, 60),
    // 説明文の設定
    descHeight: parseNumber(_desc.height, 30, 0, 60),
    descTextColor: String(_desc.textColor || "#ffffff"),
    descOutlineColor: String(_desc.outlineColor || "#000000"),
    descOutlineWidth: parseNumber(_desc.outlineWidth, 3, 0, 10),
    descTextAlign: String(_desc.textAlign || "center"),
    focusColor: String(parameters["FocusColor"] || "#ffffff"),
    galleryBackground: String(parameters["GalleryBackground"] || ""),
    galleryBackgroundMode: String(parameters["GalleryBackgroundMode"] || "original"),
    lockedImage: String(parameters["LockedImage"] || ""),
    showLockedDescription: parseBoolean(parameters["ShowLockedDescription"]),
    // ビューアーの設定
    fadeDuration: parseNumber(_viewer.fadeDuration, 30, 1, 120),
    zoomEnabled: (_viewer.zoomEnabled === undefined) ? true : _viewer.zoomEnabled === "true",
    minZoom: parseNumber(_viewer.minZoom, 10, 10, 100) / 100,
    maxZoom: parseNumber(_viewer.maxZoom, 400, 100, 1000) / 100,
    zoomSpeed: parseNumber(_viewer.zoomSpeed, 10, 5, 50) / 100,
    panSpeed: parseNumber(_viewer.panSpeed, 20, 10, 100),
    doubleClickTime: parseNumber(_viewer.doubleClickTime, 300, 100, 500),
    doubleClickZoom: parseNumber(_viewer.doubleClickZoom, 200, 100, 400) / 100,
    enablePinch: (_viewer.enablePinch === undefined) ? true : _viewer.enablePinch === "true",
    // 設定ツール
    showToolInTitle: parseBoolean(parameters["ShowToolInTitle"] ?? "true"),
    toolCommandName: String(parameters["ToolCommandName"] || "CG設定ツール"),
    // コモンイベント再生
    eventButtonText: String(parameters["EventButtonText"] || "イベント再生"),
    // 開放済み枚数の表示
    showUnlockCounter: (_counter.show === undefined) ? true : _counter.show === "true",
    unlockCounterPos: _parseCounterPos(_counter.pos),
    counterFontSize: parseNumber(_counter.fontSize, 18, 8, 60),
    counterTextColor: String(_counter.textColor || "#ffffff"),
    counterOutlineColor: String(_counter.outlineColor || "#ffffff"),
    counterOutlineWidth: parseNumber(_counter.outlineWidth, 0, 0, 10),
    // ボタン位置（Viewer）
    viewerPrevButtonPos:  parseButtonPos("ViewerPrevButtonPos"),
    viewerNextButtonPos:  parseButtonPos("ViewerNextButtonPos"),
    viewerListButtonPos:  parseButtonPos("ViewerListButtonPos"),
    viewerExitButtonPos:  parseButtonPos("ViewerExitButtonPos"),
    viewerEventButtonPos: parseButtonPos("ViewerEventButtonPos"),
    // ボタン位置（Gallery）
    galleryPrevButtonPos: parseButtonPos("GalleryPrevButtonPos"),
    galleryNextButtonPos: parseButtonPos("GalleryNextButtonPos"),
    galleryExitButtonPos: parseButtonPos("GalleryExitButtonPos"),
    // ボタン画像（Viewer）
    viewerPrevButtonImage:  parseButtonImage("ViewerPrevButtonImage"),
    viewerNextButtonImage:  parseButtonImage("ViewerNextButtonImage"),
    viewerListButtonImage:  parseButtonImage("ViewerListButtonImage"),
    viewerExitButtonImage:  parseButtonImage("ViewerExitButtonImage"),
    viewerEventButtonImage: parseButtonImage("ViewerEventButtonImage"),
    // ボタン画像（Gallery）
    galleryPrevButtonImage: parseButtonImage("GalleryPrevButtonImage"),
    galleryNextButtonImage: parseButtonImage("GalleryNextButtonImage"),
    galleryExitButtonImage: parseButtonImage("GalleryExitButtonImage"),
};

//=============================================================================
// CGViewerMZ_Config.json の読み込み（起動時・プレビュー前）
//=============================================================================

// CONFIGオブジェクト確定直後に外部設定ファイルを読み込み、CONFIGを上書きする。
// NW.js環境のみ動作する（ブラウザ版では何もしない）。
if (Utils.isNwjs()) {
    _loadButtonConfig();
}

function _loadButtonConfig() {
    if (typeof require !== "function") return;
    try {
        const fs   = require("fs");
        const path = require("path");
        const configPath = path.join(process.cwd(), "data", "CGViewerMZ_Config.json");
        if (!fs.existsSync(configPath)) return; // ファイルなし = プラグインパラメータをそのまま使う

        const data = JSON.parse(fs.readFileSync(configPath, "utf8"));

        const g = data.gallery;
        if (g) {
            if (g.thumbCols             != null) CONFIG.thumbCols             = g.thumbCols;
            if (g.thumbRows             != null) CONFIG.thumbRows             = g.thumbRows;
            if (g.thumbPadding          != null) CONFIG.thumbPadding          = g.thumbPadding;
            if (g.gridMargin            != null) CONFIG.gridMargin            = g.gridMargin;
            if (g.thumbScaleMode        != null) CONFIG.thumbScaleMode        = g.thumbScaleMode;
            if (g.background            != null) CONFIG.galleryBackground     = g.background;
            if (g.backgroundMode        != null) CONFIG.galleryBackgroundMode = g.backgroundMode;
            if (g.focusColor            != null) CONFIG.focusColor            = g.focusColor;
            if (g.descHeight            != null) CONFIG.descHeight            = g.descHeight;
            if (g.descTextColor         != null) CONFIG.descTextColor         = g.descTextColor;
            if (g.descOutlineColor      != null) CONFIG.descOutlineColor      = g.descOutlineColor;
            if (g.descOutlineWidth      != null) CONFIG.descOutlineWidth      = g.descOutlineWidth;
            if (g.descTextAlign         != null) CONFIG.descTextAlign         = g.descTextAlign;
            if (g.lockedImage           != null) CONFIG.lockedImage           = g.lockedImage;
            if (g.showLockedDescription != null) CONFIG.showLockedDescription = g.showLockedDescription;
            if (g.counter) {
                const c = g.counter;
                if (c.show         != null) CONFIG.showUnlockCounter  = c.show;
                // null = 「自動」という正常な値のため、条件チェックなしで常に代入する
                CONFIG.unlockCounterPos = { x: c.x ?? null, y: c.y ?? null };
                if (c.fontSize     != null) CONFIG.counterFontSize     = c.fontSize;
                if (c.textColor    != null) CONFIG.counterTextColor    = c.textColor;
                if (c.outlineColor != null) CONFIG.counterOutlineColor = c.outlineColor;
                if (c.outlineWidth != null) CONFIG.counterOutlineWidth = c.outlineWidth;
            }
            if (g.buttons) {
                const b = g.buttons;
                if (b.prev) _applyButtonConfig(b.prev, "galleryPrev");
                if (b.next) _applyButtonConfig(b.next, "galleryNext");
                if (b.exit) _applyButtonConfig(b.exit, "galleryExit");
            }
        }

        const v = data.viewer;
        if (v) {
            if (v.fadeDuration    != null) CONFIG.fadeDuration    = v.fadeDuration;
            if (v.zoomEnabled     != null) CONFIG.zoomEnabled     = v.zoomEnabled;
            if (v.minZoom         != null) CONFIG.minZoom         = v.minZoom / 100;
            if (v.maxZoom         != null) CONFIG.maxZoom         = v.maxZoom / 100;
            if (v.zoomSpeed       != null) CONFIG.zoomSpeed       = v.zoomSpeed / 100;
            if (v.panSpeed        != null) CONFIG.panSpeed        = v.panSpeed;
            if (v.doubleClickTime != null) CONFIG.doubleClickTime = v.doubleClickTime;
            if (v.doubleClickZoom != null) CONFIG.doubleClickZoom = v.doubleClickZoom / 100;
            if (v.enablePinch     != null) CONFIG.enablePinch     = v.enablePinch;
            if (v.buttons) {
                const b = v.buttons;
                if (b.prev)  _applyButtonConfig(b.prev,  "viewerPrev");
                if (b.next)  _applyButtonConfig(b.next,  "viewerNext");
                if (b.list)  _applyButtonConfig(b.list,  "viewerList");
                if (b.exit)  _applyButtonConfig(b.exit,  "viewerExit");
                if (b.event) _applyButtonConfig(b.event, "viewerEvent");
            }
        }

        console.log(`[${PLUGIN_NAME}] CGViewerMZ_Config.json を読み込みました`);
    } catch (e) {
        console.error(`[${PLUGIN_NAME}] CGViewerMZ_Config.json の読み込みエラー:`, e);
    }
}

function _applyButtonConfig(btnData, configKey) {
    // 位置（x/y いずれかが定義されていれば更新。null = 自動は正常な値）
    if (btnData.x !== undefined || btnData.y !== undefined) {
        CONFIG[configKey + "ButtonPos"] = {
            x: btnData.x ?? null,
            y: btnData.y ?? null
        };
    }
    // 画像
    if (btnData.image !== undefined) {
        CONFIG[configKey + "ButtonImage"] = {
            image:  btnData.image  || "",
            width:  btnData.width  || 0,
            height: btnData.height || 0
        };
    }
}

// タイトルギャラリーコマンド一覧
const TITLE_GALLERY_COMMANDS = (() => {
    try {
        const raw = parameters["TitleGalleryCommands"] || "[]";
        const arr = JSON.parse(raw);
        return arr.map(item => {
            const obj = JSON.parse(item);
            return {
                label: String(obj.label || "ギャラリー"),
                folder: String(obj.folder || "cg01")
            };
        });
    } catch (e) {
        return [];
    }
})();

//=============================================================================
// JSONロード
//=============================================================================

// CGList.jsonをデータベースファイルとして登録し読み込む
// ※_DataManager_loadDatabase.call(this)の時点で元の_databaseFilesの走査は完了済み。
// ※pushした新エントリは自動ロードされないため、手動でloadDataFile()を呼ぶ必要がある。
// ※_databaseFilesへの登録はScene_Boot.isReady()の待機判定に必要。
const _DataManager_loadDatabase = DataManager.loadDatabase;
DataManager.loadDatabase = function() {
    _DataManager_loadDatabase.call(this);
    
    // CGList.jsonをデータベースファイルリストに追加
    const cgListEntry = { name: "$dataCGList", src: "CGList.json" };
    if (!this._databaseFiles.some(file => file.name === "$dataCGList")) {
        this._databaseFiles.push(cgListEntry);
        // 追加後に即座にロードを開始
        this.loadDataFile(cgListEntry.name, cgListEntry.src);
    }
};

// Rキーをコモンイベント再生用に登録
Input.keyMapper[82] = 'commonEvent';  // 82 = 'R'キー
// Backspaceキーをギャラリー一覧に戻る用に登録
Input.keyMapper[8] = 'galleryList';   // 8 = Backspaceキー

// Scene_Boot.isReady()をオーバーライド（安全策: onXhrErrorで処理済みのはずだが念のため）
const _Scene_Boot_isReady = Scene_Boot.prototype.isReady;
Scene_Boot.prototype.isReady = function() {
    const baseReady = _Scene_Boot_isReady.call(this);
    
    // CGList.jsonが何らかの理由で未設定の場合のフォールバック
    if (baseReady && !window.$dataCGList) {
        console.warn(`[${PLUGIN_NAME}] CGList.json not available. Initializing with empty data.`);
        window.$dataCGList = {};
    }
    
    return baseReady;
};

// DataManager.onXhrError()をオーバーライドしてCGList.jsonのエラーを抑制
const _DataManager_onXhrError = DataManager.onXhrError;
DataManager.onXhrError = function(name, src, url) {
    // CGList.jsonの読み込みエラーは空データで初期化（エラー画面を抑制）
    // ※ここで$dataCGList={}を設定しないとisDatabaseLoaded()がfalseのまま
    //   ゲームが起動画面で永久にフリーズする
    if (src === "CGList.json" || url.includes("CGList.json")) {
        console.warn(`[${PLUGIN_NAME}] CGList.json not found (${url}). This is normal if you haven't created it yet.`);
        window.$dataCGList = {};
        return;
    }
    
    // その他のファイルのエラーは通常通り処理
    _DataManager_onXhrError.call(this, name, src, url);
};

//=============================================================================
// CG開放情報グローバルキャッシュ
//=============================================================================

// global系モード専用の保存ファイルキー
const CG_GLOBAL_SAVE_KEY = "CGViewerMZ_unlocks";

// global系モードの開放情報キャッシュ（MZの$gameSystemリセットに影響されない）
let _cgGlobalCache = {};

// 起動時にglobal系ファイルを読み込む
const _Scene_Boot_onDatabaseLoaded = Scene_Boot.prototype.onDatabaseLoaded;
Scene_Boot.prototype.onDatabaseLoaded = function() {
    _Scene_Boot_onDatabaseLoaded.call(this);
    const mode = CONFIG.unlockMode;
    if (mode === "global" || mode === "global-manual") {
        StorageManager.loadObject(CG_GLOBAL_SAVE_KEY)
            .then(data => {
                if (data && data._cgUnlocks) {
                    // ★必ずディープコピー。参照代入禁止（後の書き込みで元データが壊れる）
                    _cgGlobalCache = JsonEx.makeDeepCopy(data._cgUnlocks);
                }
            })
            .catch(() => {}); // ファイル未存在（初回起動）は正常系
    }
};

//=============================================================================
// 入力クリア
//=============================================================================

if (!Scene_Base.prototype.cleanInput) {
    Scene_Base.prototype.cleanInput = function() {
        TouchInput.clear();
        Input.clear();
    };
}

//=============================================================================
// CG拡大/縮小コントローラー
//=============================================================================

/**
 * CG画像の拡大/縮小/移動を管理するクラス
 */
class CGZoomController {
    /**
     * @param {Sprite} sprite - 制御対象のスプライト
     * @param {number} fitScale - 画面フィットの拡大率
     */
    constructor(sprite, fitScale) {
        this._sprite = sprite;
        this._fitScale = fitScale;
        this._currentScale = fitScale;
        
        // ダブルクリック/タップ検出用
        this._lastClickTime = 0;
        this._lastClickX = 0;
        this._lastClickY = 0;
        
        // ドラッグ状態
        this._isDragging = false;
        this._dragStartX = 0;
        this._dragStartY = 0;
        this._dragStartSpriteX = 0;
        this._dragStartSpriteY = 0;
        
        // ゴム跳ね返り用
        this._bounceVelocityX = 0;
        this._bounceVelocityY = 0;
        
        // 画像の境界
        this._updateBounds();
    }
    
    /**
     * 画像の表示可能範囲を更新
     */
    _updateBounds() {
        if (!this._sprite || !this._sprite.bitmap) {
            this._bounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 };
            return;
        }
        
        const bitmap = this._sprite.bitmap;
        const scale = this._currentScale;
        const scaledWidth = bitmap.width * scale;
        const scaledHeight = bitmap.height * scale;
        
        // 画像が画面より小さい場合は中央固定
        if (scaledWidth <= Graphics.width) {
            this._bounds = {
                minX: Graphics.width / 2,
                maxX: Graphics.width / 2,
                minY: 0,
                maxY: 0
            };
        } else {
            this._bounds = {
                minX: Graphics.width - scaledWidth / 2,
                maxX: scaledWidth / 2,
                minY: 0,
                maxY: 0
            };
        }
        
        if (scaledHeight <= Graphics.height) {
            this._bounds.minY = Graphics.height / 2;
            this._bounds.maxY = Graphics.height / 2;
        } else {
            this._bounds.minY = Graphics.height - scaledHeight / 2;
            this._bounds.maxY = scaledHeight / 2;
        }
    }
    
    /**
     * 指定座標を中心に拡大/縮小
     * @param {number} centerX - 中心X座標（画面座標）
     * @param {number} centerY - 中心Y座標（画面座標）
     * @param {number} factor - 拡大率の倍率（1.1で10%拡大）
     */
    zoomAtPoint(centerX, centerY, factor) {
        if (!this._sprite || !this._sprite.bitmap) return;
        
        const oldScale = this._currentScale;
        const newScale = Math.max(
            CONFIG.minZoom,
            Math.min(CONFIG.maxZoom, oldScale * factor)
        );
        
        // 拡大率が変わらない場合は何もしない
        if (Math.abs(newScale - oldScale) < 0.001) {
            return;
        }
        
        // スプライトのローカル座標を計算
        const localX = (centerX - this._sprite.x) / oldScale;
        const localY = (centerY - this._sprite.y) / oldScale;
        
        // 拡大率を変更
        this._currentScale = newScale;
        this._sprite.scale.set(newScale, newScale);
        
        // 中心点が動かないように位置を調整
        this._sprite.x = centerX - localX * newScale;
        this._sprite.y = centerY - localY * newScale;
        
        // 境界を更新して位置を制約
        this._updateBounds();
        this._constrainPosition();
    }
    
    /**
     * 画面中央を中心に拡大/縮小
     * @param {number} factor - 拡大率の倍率
     */
    zoomAtCenter(factor) {
        this.zoomAtPoint(Graphics.width / 2, Graphics.height / 2, factor);
    }
    
    /**
     * 画像を移動
     * @param {number} dx - X方向の移動量（ピクセル）
     * @param {number} dy - Y方向の移動量（ピクセル）
     */
    pan(dx, dy) {
        if (!this._sprite) return;
        
        this._sprite.x += dx;
        this._sprite.y += dy;
        this._constrainPosition();
    }
    
    /**
     * 位置を境界内に制約（ゴム跳ね返り効果付き）
     */
    _constrainPosition() {
        if (!this._sprite) return;
        
        const { minX, maxX, minY, maxY } = this._bounds;
        
        // X座標の制約
        if (this._sprite.x < minX) {
            this._bounceVelocityX = (minX - this._sprite.x) * 0.3;
            this._sprite.x = minX;
        } else if (this._sprite.x > maxX) {
            this._bounceVelocityX = (maxX - this._sprite.x) * 0.3;
            this._sprite.x = maxX;
        } else {
            this._bounceVelocityX = 0;
        }
        
        // Y座標の制約
        if (this._sprite.y < minY) {
            this._bounceVelocityY = (minY - this._sprite.y) * 0.3;
            this._sprite.y = minY;
        } else if (this._sprite.y > maxY) {
            this._bounceVelocityY = (maxY - this._sprite.y) * 0.3;
            this._sprite.y = maxY;
        } else {
            this._bounceVelocityY = 0;
        }
    }
    
    /**
     * ゴム跳ね返り効果の更新
     */
    updateBounce() {
        if (Math.abs(this._bounceVelocityX) > 0.1 || Math.abs(this._bounceVelocityY) > 0.1) {
            this._sprite.x += this._bounceVelocityX;
            this._sprite.y += this._bounceVelocityY;
            
            // 減衰
            this._bounceVelocityX *= 0.8;
            this._bounceVelocityY *= 0.8;
            
            this._constrainPosition();
        }
    }
    
    /**
     * 100%にリセット
     */
    resetZoom() {
        if (!this._sprite) return;
        
        this._currentScale = 1.0;
        this._sprite.scale.set(1.0, 1.0);
        this._sprite.x = Graphics.width / 2;
        this._sprite.y = Graphics.height / 2;
        this._updateBounds();
    }
    
    /**
     * 画面フィットにリセット
     */
    resetToFit() {
        if (!this._sprite) return;
        
        this._currentScale = this._fitScale;
        this._sprite.scale.set(this._fitScale, this._fitScale);
        this._sprite.x = Graphics.width / 2;
        this._sprite.y = Graphics.height / 2;
        this._updateBounds();
    }
    
    /**
     * フィット⇔100%をトグル
     */
    toggleFitAnd100() {
        if (Math.abs(this._currentScale - 1.0) < 0.01) {
            this.resetToFit();
        } else {
            this.resetZoom();
        }
    }
    
    /**
     * ダブルクリック/タップ処理
     * @param {number} x - クリック座標X
     * @param {number} y - クリック座標Y
     * @returns {boolean} ダブルクリックと判定されたか
     */
    handleDoubleClick(x, y) {
        const now = Date.now();
        const timeDiff = now - this._lastClickTime;
        const distX = x - this._lastClickX;
        const distY = y - this._lastClickY;
        const distance = Math.sqrt(distX * distX + distY * distY);
        
        // ダブルクリック判定
        const isDouble = timeDiff < CONFIG.doubleClickTime && distance < 30;
        
        if (isDouble) {
            // 現在の拡大率に応じて動作を変更
            if (this._currentScale >= CONFIG.doubleClickZoom * 0.9) {
                // 拡大済み → フィットに戻る
                this.resetToFit();
            } else {
                // 拡大
                const targetScale = CONFIG.doubleClickZoom;
                const factor = targetScale / this._currentScale;
                this.zoomAtPoint(x, y, factor);
            }
            
            this._lastClickTime = 0; // リセット
            return true;
        }
        
        this._lastClickTime = now;
        this._lastClickX = x;
        this._lastClickY = y;
        return false;
    }
    
    /**
     * ドラッグ開始
     * @param {number} x - 開始座標X
     * @param {number} y - 開始座標Y
     */
    startDrag(x, y) {
        this._isDragging = true;
        this._dragStartX = x;
        this._dragStartY = y;
        this._dragStartSpriteX = this._sprite.x;
        this._dragStartSpriteY = this._sprite.y;
    }
    
    /**
     * ドラッグ更新
     * @param {number} x - 現在座標X
     * @param {number} y - 現在座標Y
     */
    updateDrag(x, y) {
        if (!this._isDragging) return;
        
        const dx = x - this._dragStartX;
        const dy = y - this._dragStartY;
        
        this._sprite.x = this._dragStartSpriteX + dx;
        this._sprite.y = this._dragStartSpriteY + dy;
        
        this._constrainPosition();
    }
    
    /**
     * ドラッグ終了
     */
    endDrag() {
        this._isDragging = false;
    }
    
    /**
     * 現在の拡大率を取得
     * @returns {number} 拡大率（0.1 = 10%, 1.0 = 100%）
     */
    getScale() {
        return this._currentScale;
    }
    
    /**
     * フィット拡大率を取得
     * @returns {number} フィット拡大率
     */
    getFitScale() {
        return this._fitScale;
    }
    
    /**
     * ドラッグ中か判定
     * @returns {boolean}
     */
    isDragging() {
        return this._isDragging;
    }
}

//=============================================================================
// CGデータマネージャー
//=============================================================================

class CGDataManager {
    /**
     * 指定フォルダのCG番号リストを取得
     * @param {string} folder フォルダ名
     * @returns {number[]} CG番号の配列
     */
    static listNumbers(folder) {
        const data = window.$dataCGList;
        if (!data) {
            console.warn(`[${PLUGIN_NAME}] CGList.json not loaded yet.`);
            return [];
        }
        
        const list = data[folder];
        if (!list) {
            console.warn(`[${PLUGIN_NAME}] Folder "${folder}" not found in CGList.json. Available: ${Object.keys(data).join(', ')}`);
            return [];
        }
        
        // 配列でない場合は配列化
        if (!Array.isArray(list)) {
            return [list];
        }
        
        // 詳細形式（オブジェクト配列）とシンプル形式（数値配列）の両方に対応
        // 詳細形式: [{ number: 1, description: "...", commonEventId: 5 }, ...]
        // シンプル形式: [1, 2, 3, ...]
        const numbers = list.map(item => {
            // オブジェクトの場合はnumberプロパティを取得
            if (typeof item === 'object' && item !== null && 'number' in item) {
                return item.number;
            }
            // 数値の場合はそのまま返す
            return item;
        });
        
        return numbers;
    }

    /**
     * 指定フォルダ・番号のCG詳細情報を取得
     * @param {string} folder フォルダ名
     * @param {number} number CG番号
     * @returns {object|null} { description: string, commonEventId: number } または null
     */
    static getCGData(folder, number) {
        const data = window.$dataCGList;
        if (!data) return null;
        
        const list = data[folder];
        if (!list || !Array.isArray(list)) return null;
        
        // リストから該当番号を探す
        for (const item of list) {
            // 詳細形式の場合
            if (typeof item === 'object' && item !== null && item.number === number) {
                return {
                    description: item.description || '',
                    commonEventId: item.commonEventId || 0
                };
            }
            // シンプル形式の場合
            if (item === number) {
                return {
                    description: '',
                    commonEventId: 0
                };
            }
        }
        
        return null;
    }

    /**
     * 番号リスト文字列をパース（例: "1-5,7,9" → [1,2,3,4,5,7,9]）
     * @param {string} str 番号リスト文字列
     * @returns {number[]} 番号配列
     */
    static parseNumberList(str) {
        const result = [];
        if (str == null) return result;
        
        for (const part of String(str).split(",")) {
            const trimmed = part.trim();
            if (!trimmed) continue;
            
            if (trimmed.includes("-")) {
                const [a, b] = trimmed.split("-").map(Number);
                if (Number.isFinite(a) && Number.isFinite(b)) {
                    const min = Math.min(a, b);
                    const max = Math.max(a, b);
                    for (let i = min; i <= max; i++) {
                        if (i > 0) result.push(i);
                    }
                }
            } else {
                const n = Number(trimmed);
                if (Number.isFinite(n) && n > 0) {
                    result.push(n);
                }
            }
        }
        
        return [...new Set(result)].sort((a, b) => a - b);
    }

    /**
     * 変数展開（\V[n]を変数値に置換）
     * @param {string} str 元の文字列
     * @returns {string} 展開後の文字列
     */
    static expandVariables(str) {
        let result = String(str ?? "");
        result = result.replace(/\\V\[(\d+)\]/gi, (_, n) => {
            const id = Number(n);
            if ($gameVariables && Number.isFinite(id)) {
                const value = $gameVariables.value(id);
                return value != null ? String(value) : "0";
            }
            return "0";
        });
        return result;
    }

    /**
     * 数値引数を展開＆パース
     * @param {string} str 引数文字列
     * @returns {number} 数値
     */
    static parseNumberArg(str) {
        const expanded = this.expandVariables(str);
        const num = Number(expanded);
        return Number.isFinite(num) ? num : 0;
    }
}

//=============================================================================
// UI構築ヘルパー
//=============================================================================

class UIBuilder {
    /**
     * ボタンスプライトを作成
     */
    static createButton(label, width, height, onClick, fontSize = 20) {
        // 2倍解像度で描画してscale 0.5で表示（文字ジャギー軽減）
        const dw = width * 2;
        const dh = height * 2;
        const bitmap = new Bitmap(dw, dh);
        bitmap.smooth = true;
        bitmap.fillRect(0, 0, dw, dh, "rgba(0,0,0,0.6)");
        bitmap.textColor = "white";
        bitmap.outlineColor = "black";
        bitmap.outlineWidth = 8;
        bitmap.fontSize = fontSize * 2;
        bitmap.drawText(label, 0, 0, dw, dh, "center");
        
        const sprite = new Sprite(bitmap);
        sprite.scale.set(0.5, 0.5);
        // hitRectは表示サイズ（元サイズ）で設定
        sprite._hitRect = { x: 0, y: 0, w: width, h: height };
        sprite._onClick = onClick;
        sprite._ownBitmap = true;  // new Bitmap()製のためdestroyしてよい
        
        return sprite;
    }

    /**
     * 画像ボタンを生成する（テキストなし・画像表示）
     * @param {string} imageName - img/pictures/ 配下のファイル名（拡張子なし）
     * @param {number} dispWidth  - 表示幅（0で画像の元サイズ）
     * @param {number} dispHeight - 表示高さ（0で画像の元サイズ）
     * @param {Function} onClick  - クリック時コールバック
     * @returns {Sprite}
     */
    static createImageButton(imageName, dispWidth, dispHeight, onClick) {
        const sprite = new Sprite();
        sprite._onClick = onClick;
        // ロード完了前の当たり判定（ロード後に正しい値で上書きされる）
        sprite._hitRect = { x: 0, y: 0, w: dispWidth || 48, h: dispHeight || 48 };

        // ImageManager.loadPicture() は自動で ".png" を付加するため、拡張子を除去してから渡す
        const bitmap = ImageManager.loadPicture(imageName.replace(/\.[^.]+$/, ''));
        bitmap.addLoadListener(() => {
            if (bitmap.width === 0 || bitmap.height === 0) {
                console.warn(`[${PLUGIN_NAME}] ボタン画像が読み込めません: ${imageName}`);
                return;
            }
            sprite.bitmap = bitmap;
            // ImageManagerキャッシュ経由のため _ownBitmap はセットしない（destroyしない）

            const w = dispWidth  > 0 ? dispWidth  : bitmap.width;
            const h = dispHeight > 0 ? dispHeight : bitmap.height;
            sprite.scale.x = w / bitmap.width;
            sprite.scale.y = h / bitmap.height;
            // 当たり判定をロード後の正しいサイズで上書き
            sprite._hitRect = { x: 0, y: 0, w: w, h: h };
        });

        return sprite;
    }

    /**
     * ロック中サムネイルを作成
     */
    static createLockedThumb(width, height) {
        const sprite = new Sprite();
        
        // カスタム画像が設定されている場合はそちらを優先
        if (CONFIG.lockedImage) {
            // ImageManager.loadPicture() は自動で ".png" を付加するため、拡張子を除去してから渡す
            const bitmap = ImageManager.loadPicture(CONFIG.lockedImage.replace(/\.[^.]+$/, ''));
            bitmap.addLoadListener(() => {
                if (bitmap.width === 0 || bitmap.height === 0) {
                    // 画像ロード失敗時はLOCKEDテキストにフォールバック
                    UIBuilder._applyLockedText(sprite, width, height);
                    return;
                }
                const scale = Math.min(
                    width / bitmap.width,
                    height / bitmap.height,
                    1
                );
                sprite.bitmap = bitmap;
                sprite.scale.set(scale, scale);
            });
            return sprite;
        }
        
        // 画像未設定：2倍解像度テキスト表示（文字ジャギー軽減）
        UIBuilder._applyLockedText(sprite, width, height);
        return sprite;
    }

    /**
     * スプライトにLOCKEDテキストを描画（2倍解像度方式）
     * @private
     */
    static _applyLockedText(sprite, width, height) {
        // スクリーンのアスペクト比からCG画像の表示幅を推定する。
        // CG画像は通常スクリーンと同じアスペクト比で制作されるため、
        // height × (screenW / screenH) が実際の画像表示幅に近くなる。
        // Math.min で最大 width を超えないよう制限する。
        const aspectW = Math.min(width, Math.round(height * Graphics.width / Graphics.height));
        const dw = aspectW * 2;
        const dh = height * 2;
        const bitmap = new Bitmap(dw, dh);
        bitmap.smooth = true;
        bitmap.fillRect(0, 0, dw, dh, "rgba(0,0,0,0.6)");
        bitmap.textColor = "white";
        bitmap.outlineColor = "black";
        bitmap.outlineWidth = 8;
        bitmap.fontSize = 44;
        bitmap.drawText(STRINGS.lockedLabel, 0, 0, dw, dh, "center");
        sprite.bitmap = bitmap;
        sprite.scale.set(0.5, 0.5);
        // new Bitmap()で生成した場合のみdestroyしてよいとマーク
        sprite._ownBitmap = true;
    }

    /**
     * 当たり判定チェック
     */
    static hitTest(x, y, sprite) {
        if (!sprite || !sprite.visible || !sprite._hitRect) {
            return false;
        }
        const r = sprite._hitRect;
        return x >= sprite.x + r.x && 
               x <= sprite.x + r.x + r.w && 
               y >= sprite.y + r.y && 
               y <= sprite.y + r.y + r.h;
    }
}

//=============================================================================
// Game_System - CG解放状態の管理
//=============================================================================

const _Game_System_initialize = Game_System.prototype.initialize;
Game_System.prototype.initialize = function() {
    _Game_System_initialize.call(this);
    if (!this._cgUnlocks) {
        this._cgUnlocks = {};
    }
};

/**
 * CGを開放
 * @param {string} folder フォルダ名
 * @param {number} number CG番号
 */
Game_System.prototype.unlockCG = function(folder, number) {
    const mode = CONFIG.unlockMode;
    if (mode === "global" || mode === "global-manual") {
        // global系: _cgGlobalCache に記録
        if (!_cgGlobalCache[folder]) _cgGlobalCache[folder] = [];
        if (!_cgGlobalCache[folder].includes(number)) {
            _cgGlobalCache[folder].push(number);
        }
    } else {
        // slot系: $gameSystem._cgUnlocks に記録（従来通り）
        if (!this._cgUnlocks[folder]) this._cgUnlocks[folder] = [];
        if (!this._cgUnlocks[folder].includes(number)) {
            this._cgUnlocks[folder].push(number);
        }
    }
    DataManager.saveCGUnlocksImmediately();
};

/**
 * CGが開放済みか判定
 * @param {string} folder フォルダ名
 * @param {number} number CG番号
 * @returns {boolean} 開放済みならtrue
 */
Game_System.prototype.isCGUnlocked = function(folder, number) {
    if (CONFIG.allUnlocked) return true;
    const mode = CONFIG.unlockMode;
    if (mode === "global" || mode === "global-manual") {
        // global系: _cgGlobalCache を参照
        return Array.isArray(_cgGlobalCache[folder]) &&
               _cgGlobalCache[folder].includes(number);
    }
    // slot系: $gameSystem._cgUnlocks を参照（従来通り）
    return Array.isArray(this._cgUnlocks[folder]) &&
           this._cgUnlocks[folder].includes(number);
};

/**
 * 指定フォルダの開放済みCG番号リストを取得
 * @param {string} folder フォルダ名
 * @returns {number[]} 開放済み番号の配列
 */
Game_System.prototype.getUnlockedCGs = function(folder) {
    const allNumbers = CGDataManager.listNumbers(folder);
    return allNumbers.filter(n => this.isCGUnlocked(folder, n));
};

//=============================================================================
// DataManager - CG開放情報の保存処理
//=============================================================================

/**
 * global系専用ファイルへ書き込む
 * ファイル未存在（初回書き込み）は正常系として処理する
 */
DataManager._writeCGUnlocksGlobal = function() {
    return StorageManager.loadObject(CG_GLOBAL_SAVE_KEY)
        .catch(() => null)  // ファイル未存在（初回書き込み）→ null として正常処理続行
        .then(existing => {
            const data = existing || {};
            // ★必ずディープコピー（非同期競合対策）
            data._cgUnlocks = JsonEx.makeDeepCopy(_cgGlobalCache);
            return StorageManager.saveObject(CG_GLOBAL_SAVE_KEY, data);
        }).catch(e => {
            // この .catch には saveObject の失敗のみ到達する
            console.warn(`[${PLUGIN_NAME}] CG開放情報(global)の保存失敗:`, e);
        });
};

/**
 * 現在のスロットのセーブファイルへ開放情報を書き戻す
 * スロット未確定（一度もセーブ/ロードしていない）の場合は中断する
 */
DataManager._writeCGUnlocksToSlot = function() {
    const savefileId = this._lastAccessedId;
    // 正の整数でなければスロット未確定 → 中断
    if (!savefileId || !Number.isInteger(savefileId) || savefileId <= 0) return;
    const key = this.makeSavename(savefileId);
    return StorageManager.loadObject(key).then(saveData => {
        // ファイルが存在しない・systemが壊れている場合は触れない
        if (!saveData || typeof saveData.system !== "object" || saveData.system === null) return;
        // ★必ずディープコピー（非同期競合対策）
        saveData.system._cgUnlocks = JsonEx.makeDeepCopy($gameSystem._cgUnlocks);
        return StorageManager.saveObject(key, saveData);
    }).catch(e => {
        console.warn(`[${PLUGIN_NAME}] CG開放情報(slot:${savefileId})の保存失敗:`, e);
    });
};

/**
 * 即時保存エントリーポイント（デバウンス付き）
 * 手動セーブのみモードでは何もしない
 */
DataManager._cgSaveTimer = null;

DataManager.saveCGUnlocksImmediately = function() {
    const mode = CONFIG.unlockMode;
    // 手動セーブのみモードは即時保存しない
    if (mode === "slot-manual" || mode === "global-manual") return;

    // デバウンス: 100ms以内の連続呼び出しを1回のI/Oにまとめる
    if (this._cgSaveTimer) clearTimeout(this._cgSaveTimer);
    this._cgSaveTimer = setTimeout(() => {
        this._cgSaveTimer = null;
        if (mode === "global") {
            this._writeCGUnlocksGlobal();
        } else {
            this._writeCGUnlocksToSlot();
        }
    }, 100);
};

/**
 * DataManager.saveGame フック
 * _lastAccessedId を記録し、global-manualモードでは専用ファイルにも書き込む
 */
const _DataManager_saveGame = DataManager.saveGame;
DataManager.saveGame = function(savefileId) {
    this._lastAccessedId = savefileId;
    const result = _DataManager_saveGame.call(this, savefileId);
    if (CONFIG.unlockMode === "global-manual") {
        // セーブ完了後にglobalファイルも更新する
        // ※エラーハンドリングは _writeCGUnlocksGlobal 内部で完結している
        result.then(() => this._writeCGUnlocksGlobal());
    }
    return result;
};

/**
 * DataManager.loadGame フック
 * _lastAccessedId を記録する（global系はキャッシュを参照するため追加処理不要）
 */
const _DataManager_loadGame = DataManager.loadGame;
DataManager.loadGame = function(savefileId) {
    this._lastAccessedId = savefileId;
    return _DataManager_loadGame.call(this, savefileId);
};

PluginManager.registerCommand(pluginName, "OpenViewer", args => {
    const folder = String(args.folder || "cg01");
    const startRaw = String(args.startNumber || "").trim();
    
    const tryOpenScene = () => {
        if (window.$dataCGList) {
            if (startRaw === "") {
                // 空欄 → ギャラリーを開く
                $gameTemp._cgGalleryFolder = folder;
                SceneManager.push(Scene_CGGallery);
            } else {
                // 数値または変数 → ビューアーを開く
                const start = CGDataManager.parseNumberArg(startRaw) || 1;
                $gameTemp._cgViewerArgs = { folder, start };
                SceneManager.push(Scene_CGViewer);
            }
        } else {
            console.warn(`[${PLUGIN_NAME}] CGList.json not yet loaded. Retrying...`);
            setTimeout(tryOpenScene, 100);
        }
    };
    tryOpenScene();
});

/**
 * CGを開放
 */
PluginManager.registerCommand(pluginName, "UnlockCG", args => {
    const folder = String(args.folder || "cg01");
    const numbers = CGDataManager.parseNumberList(args.numbers ?? "1");
    
    for (const num of numbers) {
        $gameSystem.unlockCG(folder, num);
    }
});

/**
 * CGをピクチャとして表示
 */
PluginManager.registerCommand(pluginName, "ShowCGPicture", args => {
    const folder = CGDataManager.expandVariables(args.folder || "cg01");
    const number = CGDataManager.parseNumberArg(args.number || "1");
    const pictureId = CGDataManager.parseNumberArg(args.pictureId || "1");
    
    if (!number || !pictureId) {
        console.warn(`[${PLUGIN_NAME}] ShowCGPicture: invalid number or pictureId.`, { folder, number, pictureId });
        return;
    }
    
    // パラメータ取得
    const origin = CGDataManager.parseNumberArg(args.origin || "1");
    
    // X/Y座標: 空欄なら画面中央
    const x = args.x && args.x !== "" 
        ? CGDataManager.parseNumberArg(args.x) 
        : Graphics.width / 2;
    const y = args.y && args.y !== "" 
        ? CGDataManager.parseNumberArg(args.y) 
        : Graphics.height / 2;
    
    const scaleX = CGDataManager.parseNumberArg(args.scaleX || "100");
    const scaleY = CGDataManager.parseNumberArg(args.scaleY || "100");
    const opacity = CGDataManager.parseNumberArg(args.opacity || "255");
    const blendMode = CGDataManager.parseNumberArg(args.blendMode || "0");
    
    // ピクチャ名を生成（フォルダ名/番号）
    const name = `${folder}/${number}`;
    
    $gameScreen.showPicture(pictureId, name, origin, x, y, scaleX, scaleY, opacity, blendMode);
});

/**
 * CG設定ツールを開く
 */
PluginManager.registerCommand(pluginName, "OpenSettingTool", args => {
    openCGSettingTool();
});

/**
 * CG設定ツール起動処理
 */

// 現在のツールウィンドウ参照（グローバル）
let currentToolWindow = null;

// メッセージリスナーを1回だけ設定（初期化時）
let messageListenerInitialized = false;

function initializeMessageListener() {
    if (messageListenerInitialized) {
        return; // 既に設定済み
    }
    
    window.addEventListener('message', (event) => {
        const request = event.data;
        
        // 現在のウィンドウからのメッセージのみ処理
        if (!currentToolWindow) {
            return;
        }
        
        console.log(`[${PLUGIN_NAME}] メッセージ受信:`, request.type);
        
        switch (request.type) {
            case 'loadCGList':
                handleLoadCGList(request);
                break;
            case 'saveCGList':
                handleSaveCGList(request);
                break;
            case 'loadCommonEvents':
                handleLoadCommonEvents(request);
                break;
            case 'selectFolder':
                handleSelectFolder(request);
                break;
            case 'rescanFolder':
                handleRescanFolder(request);
                break;
            case 'loadButtonConfig':
                handleLoadButtonConfig();
                break;
            case 'saveButtonConfig':
                handleSaveButtonConfig(request);
                break;
            case 'deleteButtonConfig':
                handleDeleteButtonConfig();
                break;
            case 'selectButtonImage':
                handleSelectButtonImage(request);
                break;
            case 'previewInGame':
                handlePreviewInGame(request);
                break;
            default:
                console.warn(`[${PLUGIN_NAME}] 未知のメッセージタイプ:`, request.type);
        }
    });
    
    messageListenerInitialized = true;
    console.log(`[${PLUGIN_NAME}] メッセージリスナーを初期化しました`);
}

function openCGSettingTool() {
    // NW.js環境チェック
    if (!Utils.isNwjs()) {
        console.error(`[${PLUGIN_NAME}] 設定ツールはNW.js環境でのみ動作します`);
        return;
    }
    
    // メッセージリスナーを初期化（1回だけ）
    initializeMessageListener();
    
    try {
        const nw = require('nw.gui');
        const path = require('path');
        const fs = require('fs');
        
        // コモンイベント情報を取得
        const commonEvents = [];
        if ($dataCommonEvents) {
            for (let i = 1; i < $dataCommonEvents.length; i++) {
                const event = $dataCommonEvents[i];
                if (event && event.name) {
                    commonEvents.push({
                        id: i,
                        name: event.name
                    });
                }
            }
        }
        
        console.log(`[${PLUGIN_NAME}] コモンイベント情報を取得: ${commonEvents.length}件`);
        
        // 一時ファイルに保存（pluginsフォルダに保存）
        const pluginDir = path.join(process.cwd(), 'js', 'plugins');
        const tempFile = path.join(pluginDir, 'temp_commonEvents.json');
        
        // コモンイベント情報を保存
        fs.writeFileSync(tempFile, JSON.stringify(commonEvents, null, 2));
        console.log(`[${PLUGIN_NAME}] 一時ファイルを作成: ${tempFile}`);
        
        // ツールを開く（pluginsフォルダから読み込み）
        const toolPath = 'file://' + path.join(process.cwd(), 'js', 'plugins', 'CGSettingTool.html');
        console.log(`[${PLUGIN_NAME}] 設定ツールを起動: ${toolPath}`);
        
        nw.Window.open(toolPath, {
            width: 1200,
            height: 800,
            title: 'CGViewerMZ 設定ツール',
            resizable: true,
            min_width: 1000,
            min_height: 600
        }, (toolWindow) => {
            console.log(`[${PLUGIN_NAME}] 設定ツールウィンドウを開きました`);
            
            // 現在のツールウィンドウを更新
            currentToolWindow = toolWindow;
            
            // ウィンドウが閉じられた時の処理
            toolWindow.on('closed', () => {
                console.log(`[${PLUGIN_NAME}] 設定ツールウィンドウが閉じられました`);
                if (currentToolWindow === toolWindow) {
                    currentToolWindow = null;
                }
            });
        });
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] 設定ツールの起動に失敗:`, error);
    }
}

// postMessage通信ハンドラー

function handleLoadCommonEvents(request) {
    try {
        const fs = require('fs');
        const path = require('path');
        
        const tempFile = path.join(process.cwd(), 'js', 'plugins', 'temp_commonEvents.json');
        
        if (!fs.existsSync(tempFile)) {
            sendResponseToTool({
                type: 'commonEventsLoaded',
                success: false,
                error: 'ファイルが見つかりません'
            });
            return;
        }
        
        const content = fs.readFileSync(tempFile, 'utf8');
        const data = JSON.parse(content);
        
        console.log(`[${PLUGIN_NAME}] コモンイベント送信: ${data.length}件`);
        
        sendResponseToTool({
            type: 'commonEventsLoaded',
            success: true,
            data: data
        });
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] コモンイベント読み込み失敗:`, error);
        sendResponseToTool({
            type: 'commonEventsLoaded',
            success: false,
            error: error.message
        });
    }
}

function handleLoadCGList(request) {
    try {
        const fs = require('fs');
        const path = require('path');
        
        const cgListPath = path.join(process.cwd(), 'data', 'CGList.json');
        
        if (!fs.existsSync(cgListPath)) {
            sendResponseToTool({
                type: 'cgListLoaded',
                success: false,
                error: 'ファイルが見つかりません'
            });
            return;
        }
        
        const content = fs.readFileSync(cgListPath, 'utf8');
        const data = JSON.parse(content);
        
        console.log(`[${PLUGIN_NAME}] CGList.json送信:`, data);
        
        sendResponseToTool({
            type: 'cgListLoaded',
            success: true,
            data: data
        });
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] CGList.json読み込み失敗:`, error);
        sendResponseToTool({
            type: 'cgListLoaded',
            success: false,
            error: error.message
        });
    }
}

function handleSaveCGList(request) {
    try {
        const fs = require('fs');
        const path = require('path');
        
        const cgListPath = path.join(process.cwd(), 'data', 'CGList.json');
        const json = JSON.stringify(request.data, null, 2);
        
        fs.writeFileSync(cgListPath, json, 'utf8');

        // メモリ上の $dataCGList も更新する（プレビューに即反映されるよう）
        window.$dataCGList = request.data;
        
        console.log(`[${PLUGIN_NAME}] CGList.json保存完了:`, cgListPath);
        
        sendResponseToTool({
            type: 'cgListSaved',
            success: true,
            path: cgListPath
        });
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] CGList.json保存失敗:`, error);
        sendResponseToTool({
            type: 'cgListSaved',
            success: false,
            error: error.message
        });
    }
}

function handleSelectFolder(request) {
    try {
        const fs = require('fs');
        const path = require('path');
        
        // デフォルトパス: img/picturesフォルダ
        const defaultPath = path.join(process.cwd(), 'img', 'pictures');
        
        // フォルダ選択ダイアログを開く
        const input = document.createElement('input');
        input.type = 'file';
        input.nwdirectory = true;
        input.nwworkingdir = defaultPath;
        
        input.addEventListener('change', function() {
            const selectedPath = this.value;
            
            // フォーカスを設定ツールに戻す
            if (currentToolWindow && currentToolWindow.focus) {
                try {
                    currentToolWindow.focus();
                } catch (e) {
                    // フォーカスエラーは無視
                }
            }
            
            if (!selectedPath) {
                sendResponseToTool({
                    type: 'folderSelected',
                    success: false,
                    error: 'フォルダが選択されませんでした'
                });
                return;
            }
            
            console.log(`[${PLUGIN_NAME}] フォルダ選択: ${selectedPath}`);
            
            // imgからの相対パスを計算
            const imgPath = path.join(process.cwd(), 'img');
            const relativePath = path.relative(imgPath, selectedPath);
            let folderName = relativePath.replace(/\\/g, '/'); // Windowsパス対応
            
            // pictures/ 配下かチェック
            if (!folderName.startsWith('pictures/')) {
                sendResponseToTool({
                    type: 'folderSelected',
                    success: false,
                    error: 'このプラグインはimg/pictures/配下のフォルダのみ対応しています。\n\n選択されたフォルダ: ' + folderName
                });
                return;
            }
            
            // pictures/ プレフィックスを削除
            folderName = folderName.substring('pictures/'.length);
            
            // 共通スキャン処理を呼出し
            scanFolderAndRespond(selectedPath, folderName, 'folderSelected');
        });
        
        input.click();
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] フォルダ選択失敗:`, error);
        sendResponseToTool({
            type: 'folderSelected',
            success: false,
            error: error.message
        });
    }
}

/**
 * 再スキャン: フォルダピッカーを開かず、既知のフォルダ名から直接スキャン
 */
function handleRescanFolder(request) {
    try {
        const path = require('path');
        
        const folderName = request.folderName;
        if (!folderName) {
            sendResponseToTool({
                type: 'folderRescanned',
                success: false,
                error: 'フォルダ名が指定されていません'
            });
            return;
        }
        
        // img/pictures/${folderName} の絶対パスを構築
        const absolutePath = path.join(process.cwd(), 'img', 'pictures', folderName);
        
        // 共通スキャン処理を呼出し
        scanFolderAndRespond(absolutePath, folderName, 'folderRescanned');
        
    } catch (error) {
        console.error(`[${PLUGIN_NAME}] 再スキャン失敗:`, error);
        sendResponseToTool({
            type: 'folderRescanned',
            success: false,
            error: error.message
        });
    }
}

/**
 * フォルダスキャン共通処理
 * @param {string} absolutePath - スキャン対象の絶対パス
 * @param {string} folderName - CGList.json上のフォルダ名
 * @param {string} responseType - レスポンスのtype（'folderSelected' or 'folderRescanned'）
 */
function scanFolderAndRespond(absolutePath, folderName, responseType) {
    const fs   = require('fs');
    const path = require('path');
    
    fs.readdir(absolutePath, (err, files) => {
        if (err) {
            sendResponseToTool({
                type: responseType,
                success: false,
                error: 'フォルダの読み込みに失敗: ' + err.message
            });
            return;
        }
        
        // 画像ファイルのみフィルタ
        const imageFiles = files.filter(f => 
            /\.(png|jpg|jpeg|webp|gif)$/i.test(f)
        );
        
        if (imageFiles.length === 0) {
            sendResponseToTool({
                type: responseType,
                success: false,
                error: '画像ファイルが見つかりませんでした'
            });
            return;
        }
        
        // 番号付きファイルのみ抽出
        const numberedFiles = [];
        for (const fileName of imageFiles) {
            const match = fileName.match(/^(\d+)\.(png|jpg|jpeg|webp|gif)$/i);
            if (match) {
                numberedFiles.push({
                    number: parseInt(match[1], 10),
                    fileName: fileName
                });
            }
        }
        
        if (numberedFiles.length === 0) {
            sendResponseToTool({
                type: responseType,
                success: false,
                error: '番号付き画像ファイル（例: 1.png, 2.png）が見つかりませんでした'
            });
            return;
        }
        
        // 番号順にソート
        numberedFiles.sort((a, b) => a.number - b.number);
        
        // thumb/ サブフォルダをスキャンして番号ごとに存在確認
        const thumbDir = path.join(absolutePath, 'thumb');
        let thumbCount = 0;
        
        try {
            if (fs.existsSync(thumbDir)) {
                const thumbFiles = fs.readdirSync(thumbDir);
                const thumbNumbers = new Set();
                for (const tf of thumbFiles) {
                    const m = tf.match(/^(\d+)\.(png|jpg|jpeg|webp|gif)$/i);
                    if (m) thumbNumbers.add(parseInt(m[1], 10));
                }
                // CG番号と突き合わせてカウント
                thumbCount = numberedFiles.filter(f => thumbNumbers.has(f.number)).length;
            }
        } catch (e) {
            // thumbスキャン失敗は無視（thumbなしとして扱う）
            thumbCount = 0;
        }
        
        // 結果を送信（thumbCountを追加）
        sendResponseToTool({
            type: responseType,
            success: true,
            folderName: folderName,
            files: numberedFiles,
            thumbCount: thumbCount
        });
    });
}

//=============================================================================
// ボタン設定ファイル（CGViewerMZ_Config.json）の通信ハンドラ
//=============================================================================

// JS-3: Config.json を読み込み、画面サイズも同じレスポンスに含めて返す
function handleLoadButtonConfig() {
    try {
        const fs   = require("fs");
        const path = require("path");
        const configPath = path.join(process.cwd(), "data", "CGViewerMZ_Config.json");

        if (!fs.existsSync(configPath)) {
            // ファイルなし = 正常系（初回）
            sendResponseToTool({
                type:         "buttonConfigLoaded",
                success:      false,
                reason:       "notfound",
                screenWidth:  Graphics.width,
                screenHeight: Graphics.height
            });
            return;
        }

        const data = JSON.parse(fs.readFileSync(configPath, "utf8"));
        sendResponseToTool({
            type:         "buttonConfigLoaded",
            success:      true,
            data:         data,
            screenWidth:  Graphics.width,
            screenHeight: Graphics.height
        });
    } catch (e) {
        console.error(`[${PLUGIN_NAME}] Config.json 読み込みエラー:`, e);
        sendResponseToTool({
            type:         "buttonConfigLoaded",
            success:      false,
            reason:       e.message,
            screenWidth:  Graphics.width,
            screenHeight: Graphics.height
        });
    }
}

// JS-4: Config.json に書き込む
function handleSaveButtonConfig(request) {
    try {
        const fs   = require("fs");
        const path = require("path");
        const configPath = path.join(process.cwd(), "data", "CGViewerMZ_Config.json");
        fs.writeFileSync(configPath, JSON.stringify(request.data, null, 2), "utf8");
        sendResponseToTool({ type: "buttonConfigSaved", success: true, path: configPath });
    } catch (e) {
        console.error(`[${PLUGIN_NAME}] Config.json 保存エラー:`, e);
        sendResponseToTool({ type: "buttonConfigSaved", success: false, reason: e.message });
    }
}

// JS-5: Config.json を削除する
function handleDeleteButtonConfig() {
    try {
        const fs   = require("fs");
        const path = require("path");
        const configPath = path.join(process.cwd(), "data", "CGViewerMZ_Config.json");
        if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
        sendResponseToTool({ type: "buttonConfigDeleted", success: true });
    } catch (e) {
        console.error(`[${PLUGIN_NAME}] Config.json 削除エラー:`, e);
        sendResponseToTool({ type: "buttonConfigDeleted", success: false, reason: e.message });
    }
}

// JS-6: ボタン画像ファイルを選択させ、img/pictures/ からの相対パスを返す
// handleSelectFolder と同じパターンで実装する
function handleSelectButtonImage(request) {
    try {
        const path        = require("path");
        const defaultPath = path.join(process.cwd(), "img", "pictures");

        const input          = document.createElement("input");
        input.type           = "file";
        input.accept         = "image/*";
        input.nwworkingdir   = defaultPath;

        input.addEventListener("change", function() {
            const selectedPath = this.value;

            // フォーカスをツールウィンドウに戻す（handleSelectFolder と同じパターン）
            if (currentToolWindow && currentToolWindow.focus) {
                try { currentToolWindow.focus(); } catch (e) { /* 無視 */ }
            }

            if (!selectedPath) {
                sendResponseToTool({
                    type:      "buttonImageSelected",
                    buttonKey: request.buttonKey,
                    success:   false,
                    reason:    "cancelled"
                });
                return;
            }

            // img/pictures/ からの相対パスに変換
            const imgPicturesPath = path.join(process.cwd(), "img", "pictures");
            const relativePath    = path.relative(imgPicturesPath, selectedPath).replace(/\\/g, "/");

            if (relativePath.startsWith("..")) {
                sendResponseToTool({
                    type:      "buttonImageSelected",
                    buttonKey: request.buttonKey,
                    success:   false,
                    reason:    "img/pictures/ 配下のファイルのみ使用できます"
                });
                return;
            }

            sendResponseToTool({
                type:         "buttonImageSelected",
                buttonKey:    request.buttonKey,
                success:      true,
                relativePath: relativePath
            });
        });

        input.click();

    } catch (e) {
        console.error(`[${PLUGIN_NAME}] 画像選択エラー:`, e);
        sendResponseToTool({
            type:      "buttonImageSelected",
            buttonKey: request.buttonKey,
            success:   false,
            reason:    e.message
        });
    }
}

// JS-7: 現在の Config.json を再読み込みしてからギャラリーをプレビュー起動する
// _loadButtonConfig() 再実行が必須：ツール上で設定変更→保存した内容をCONFIGに反映してからプレビューするため
function handlePreviewInGame(request) {
    try {
        if (!window.$dataCGList || Object.keys($dataCGList).length === 0) {
            sendResponseToTool({ type: "previewInGameReady", success: false, reason: "noCGData" });
            return;
        }

        // 保存済みのConfig.jsonをCONFIGに再反映（プレビューに最新の設定を反映するため）
        _loadButtonConfig();

        // 静的メソッドを使う（$gameTemp._cgGalleryFolder のセットも内部で実行される）
        Scene_CGGallery._openedByPreview = true;   // プレビューモードフラグ（terminate()で終了通知に使用）
        Scene_CGGallery.open(String(request.folder || "cg01"));

        sendResponseToTool({ type: "previewInGameReady", success: true });
    } catch (e) {
        console.error(`[${PLUGIN_NAME}] プレビュー起動エラー:`, e);
        sendResponseToTool({ type: "previewInGameReady", success: false, reason: e.message });
    }
}

// 安全なpostMessage送信（グローバルなcurrentToolWindowを使用）
function sendResponseToTool(message) {
    try {
        // currentToolWindow が null なら終了
        if (!currentToolWindow) {
            return;
        }
        
        // postMessage実行
        // currentToolWindow.window へのアクセス自体がエラーを投げる可能性があるが
        // try-catchで捕捉して静かに無視する
        currentToolWindow.window.postMessage(message, '*');
        
    } catch (error) {
        // エラーは静かに無視（ウィンドウが閉じられている場合など）
    }
}

//=============================================================================
// Scene_CGEventPlayer - コモンイベント再生専用シーン
//=============================================================================
// Scene_Mapに戻らず、ビューアーから直接pushして使用する。
// 黒背景＋ピクチャ＋メッセージウィンドウでコモンイベントを再生し、
// 終了後にpop()でビューアーに復帰する。
//
// ※クラス定義にfunction/prototype方式を使用している理由:
// Scene_MessageがES5のfunction方式で定義されているため、
// ES6 classのextendsでは正しく継承できない。
// （Scene_CGGalleryのScene_Base継承はES6 classで問題ない）

function Scene_CGEventPlayer() {
    this.initialize(...arguments);
}

Scene_CGEventPlayer.prototype = Object.create(Scene_Message.prototype);
Scene_CGEventPlayer.prototype.constructor = Scene_CGEventPlayer;

Scene_CGEventPlayer.prototype.initialize = function() {
    Scene_Message.prototype.initialize.call(this);
};

Scene_CGEventPlayer.prototype.create = function() {
    Scene_Message.prototype.create.call(this);
    this._commonEventId = $gameTemp._cgEventPlayerId || 0;
    $gameTemp._cgEventPlayerId = null;
    this._eventFinished = false;
    this._finishWaitFrames = 0;
    // マップの$gameScreen状態を退避（コモンイベントが$gameScreenを汚染しないようにする）
    this._savedPictures = [];
    for (let i = 1; i <= $gameScreen.maxPictures(); i++) {
        const pic = $gameScreen.picture(i);
        if (pic) {
            this._savedPictures[i] = JsonEx.makeDeepCopy(pic);
        }
    }
    // 画面効果の状態を退避（色調・フラッシュ・シェイク・天候・ズーム・フェード）
    this._savedScreenState = {
        brightness:         $gameScreen._brightness,
        tone:               JsonEx.makeDeepCopy($gameScreen._tone),
        toneTarget:         JsonEx.makeDeepCopy($gameScreen._toneTarget),
        toneDuration:       $gameScreen._toneDuration,
        flashColor:         JsonEx.makeDeepCopy($gameScreen._flashColor),
        flashDuration:      $gameScreen._flashDuration,
        shakePower:         $gameScreen._shakePower,
        shakeSpeed:         $gameScreen._shakeSpeed,
        shakeDuration:      $gameScreen._shakeDuration,
        shakeDirection:     $gameScreen._shakeDirection,
        zoomX:              $gameScreen._zoomX,
        zoomY:              $gameScreen._zoomY,
        zoomScale:          $gameScreen._zoomScale,
        zoomScaleTarget:    $gameScreen._zoomScaleTarget,
        zoomDuration:       $gameScreen._zoomDuration,
        weatherType:        $gameScreen._weatherType,
        weatherPower:       $gameScreen._weatherPower,
        weatherPowerTarget: $gameScreen._weatherPowerTarget,
        weatherDuration:    $gameScreen._weatherDuration,
        fadeOutDuration:    $gameScreen._fadeOutDuration,
        fadeInDuration:     $gameScreen._fadeInDuration
    };
    this.createBackground();
    this.createPictureContainer();
    this.createWindowLayer();
    this.createAllWindows();
    this.setupInterpreter();
};

Scene_CGEventPlayer.prototype.createBackground = function() {
    this._backgroundSprite = new Sprite(new Bitmap(Graphics.width, Graphics.height));
    this._backgroundSprite.bitmap.fillAll("black");
    this.addChild(this._backgroundSprite);
};

Scene_CGEventPlayer.prototype.createPictureContainer = function() {
    this._pictureContainer = new Sprite();
    this._pictureContainer.setFrame(0, 0, Graphics.width, Graphics.height);
    for (let i = 1; i <= $gameScreen.maxPictures(); i++) {
        this._pictureContainer.addChild(new Sprite_Picture(i));
    }
    this.addChild(this._pictureContainer);
};

Scene_CGEventPlayer.prototype.setupInterpreter = function() {
    this._interpreter = new Game_Interpreter();
    if (this._commonEventId > 0 && $dataCommonEvents[this._commonEventId]) {
        const commonEvent = $dataCommonEvents[this._commonEventId];
        this._interpreter.setup(commonEvent.list);
    } else {
        console.warn(`[${PLUGIN_NAME}] Scene_CGEventPlayer: Invalid commonEventId=${this._commonEventId}`);
        this._eventFinished = true;
    }
};

Scene_CGEventPlayer.prototype.update = function() {
    Scene_Message.prototype.update.call(this);

    // 早送り判定（Scene_Mapと同等の仕組み）
    if (this.isFastForward()) {
        this.cancelMessageWait();
        this.updateMain();
    }
    this.updateMain();

    // イベント終了判定（インタープリタ停止 かつ メッセージ非表示）
    if (!this._eventFinished && this._interpreter &&
        !this._interpreter.isRunning() && !$gameMessage.isBusy()) {
        this._eventFinished = true;
        this._finishWaitFrames = 2; // 安全のため数フレーム待機
    }

    // 待機後にビューアーへ復帰
    if (this._eventFinished) {
        if (this._finishWaitFrames > 0) {
            this._finishWaitFrames--;
        } else {
            this.cleanupAndReturn();
        }
    }
};

/**
 * メイン更新（画面効果＋インタープリタ）
 * Scene_Map.updateMain()と同等の役割。
 * $gameScreen.update()で画面フラッシュ・シェイク・色調変更等も処理する。
 */
Scene_CGEventPlayer.prototype.updateMain = function() {
    $gameScreen.update();
    if (this._interpreter) {
        this._interpreter.update();
    }
};

/**
 * 早送り判定（OK/Space長押し中に2倍速実行）
 */
Scene_CGEventPlayer.prototype.isFastForward = function() {
    return (
        this._interpreter && this._interpreter.isRunning() &&
        !SceneManager.isSceneChanging() &&
        (Input.isLongPressed("ok") || TouchInput.isLongPressed())
    );
};

Scene_CGEventPlayer.prototype.cleanupAndReturn = function() {
    // マップのピクチャ状態を復元
    for (let i = 1; i <= $gameScreen.maxPictures(); i++) {
        $gameScreen.erasePicture(i);
    }
    if (this._savedPictures) {
        for (let i = 1; i <= $gameScreen.maxPictures(); i++) {
            if (this._savedPictures[i]) {
                $gameScreen._pictures[i] = this._savedPictures[i];
            }
        }
        this._savedPictures = null;
    }
    // 画面効果の状態を復元
    if (this._savedScreenState) {
        const s = this._savedScreenState;
        $gameScreen._brightness         = s.brightness;
        $gameScreen._tone               = s.tone;
        $gameScreen._toneTarget         = s.toneTarget;
        $gameScreen._toneDuration       = s.toneDuration;
        $gameScreen._flashColor         = s.flashColor;
        $gameScreen._flashDuration      = s.flashDuration;
        $gameScreen._shakePower         = s.shakePower;
        $gameScreen._shakeSpeed         = s.shakeSpeed;
        $gameScreen._shakeDuration      = s.shakeDuration;
        $gameScreen._shakeDirection     = s.shakeDirection;
        $gameScreen._zoomX              = s.zoomX;
        $gameScreen._zoomY              = s.zoomY;
        $gameScreen._zoomScale          = s.zoomScale;
        $gameScreen._zoomScaleTarget    = s.zoomScaleTarget;
        $gameScreen._zoomDuration       = s.zoomDuration;
        $gameScreen._weatherType        = s.weatherType;
        $gameScreen._weatherPower       = s.weatherPower;
        $gameScreen._weatherPowerTarget = s.weatherPowerTarget;
        $gameScreen._weatherDuration    = s.weatherDuration;
        $gameScreen._fadeOutDuration    = s.fadeOutDuration;
        $gameScreen._fadeInDuration     = s.fadeInDuration;
        this._savedScreenState = null;
    }
    SceneManager.pop();
};

//=============================================================================
// Scene_CGViewer - CGビューアー
//=============================================================================

class Scene_CGViewer extends Scene_Base {
    create() {
        super.create();
        this.loadViewerData();
        this.createDisplayObjects();
        this.updateDisplay();
    }

    updateCommonEventKey() {
        // Rキーでコモンイベント再生（メニュー非表示時のみ）
        if (!Input.isTriggered('commonEvent') || this._menuVisible) return;
        
        const currentNumber = this._unlockedNumbers[this._currentIndex];
        const cgData = CGDataManager.getCGData(this._folder, currentNumber);
        
        if (cgData && cgData.commonEventId > 0) {
            this.playCommonEvent();
        }
    }

    /**
     * ビューアーデータの読み込み
     */
    loadViewerData() {
        // ギャラリーからの選択で来た場合
        if ($gameTemp._cgGallerySelected) {
            const sel = $gameTemp._cgGallerySelected;
            $gameTemp._cgViewerArgs = { folder: sel.folder, start: sel.number };
            $gameTemp._cgGallerySelected = null;
        }

        const args = $gameTemp._cgViewerArgs || { folder: "cg01", start: 1 };
        $gameTemp._cgViewerArgs = null;
        this._folder = args.folder;
        const startNumber = args.start;
        
        // 開放済みCGリストを取得
        this._unlockedNumbers = $gameSystem.getUnlockedCGs(this._folder);
        
        // 開始位置を決定
        this._currentIndex = 0;
        if (this._unlockedNumbers.length > 0) {
            const index = this._unlockedNumbers.indexOf(startNumber);
            this._currentIndex = index >= 0 ? index : 0;
        }
        
        this._menuVisible = this._unlockedNumbers.length === 0;
        
        // クリック判定用（ドラッグと区別するため）
        this._clickStartX = 0;
        this._clickStartY = 0;
        this._buttonClicked = false;  // ボタンがクリックされたかのフラグ
        
        // 画像ロード追跡ID（非同期コールバックのレース条件防止）
        this._loadId = 0;
    }

    /**
     * 表示オブジェクトの作成
     */
    createDisplayObjects() {
        this.createBackground();
        
        if (this._unlockedNumbers.length > 0) {
            this.createMainSprite();
            this.createNavigationButtons();
            
            // ピンチ操作のセットアップ
            if (CONFIG.zoomEnabled && CONFIG.enablePinch) {
                this.setupPinchHandlers();
            }
        } else {
            this.createNoContentMessage();
            this.createExitButton();
        }
    }
    
    /**
     * ピンチ操作のイベントハンドラーをセットアップ
     */
    setupPinchHandlers() {
        this._touches = [];
        this._lastPinchDistance = 0;
        
        // イベントハンドラーをバインド（terminateで確実に削除するため）
        this._touchStartHandler = this.onTouchStart.bind(this);
        this._touchMoveHandler = this.onTouchMove.bind(this);
        this._touchEndHandler = this.onTouchEnd.bind(this);
        
        try {
            window.addEventListener("touchstart", this._touchStartHandler, { passive: false });
            window.addEventListener("touchmove", this._touchMoveHandler, { passive: false });
            window.addEventListener("touchend", this._touchEndHandler, { passive: false });
            window.addEventListener("touchcancel", this._touchEndHandler, { passive: false });
        } catch (error) {
            console.error(`[${PLUGIN_NAME}] Failed to setup pinch handlers:`, error);
        }
    }
    
    /**
     * タッチ開始イベント
     */
    onTouchStart(event) {
        this._touches = Array.from(event.touches);
        
        if (this._touches.length === 2) {
            event.preventDefault();
            this._lastPinchDistance = this.getTouchDistance();
        }
    }
    
    /**
     * タッチ移動イベント
     */
    onTouchMove(event) {
        this._touches = Array.from(event.touches);
        
        if (this._touches.length === 2 && this._zoomController && this._lastPinchDistance > 0) {
            event.preventDefault();
            
            const currentDistance = this.getTouchDistance();
            const centerX = (this._touches[0].clientX + this._touches[1].clientX) / 2;
            const centerY = (this._touches[0].clientY + this._touches[1].clientY) / 2;
            
            // 拡大率の変化を計算
            const factor = currentDistance / this._lastPinchDistance;
            
            // 拡大/縮小実行
            this._zoomController.zoomAtPoint(centerX, centerY, factor);
            
            // 次回のために更新
            this._lastPinchDistance = currentDistance;
        }
    }
    
    /**
     * タッチ終了イベント
     */
    onTouchEnd(event) {
        this._touches = Array.from(event.touches);
        
        if (this._touches.length < 2) {
            this._lastPinchDistance = 0;
        }
    }
    
    /**
     * 2つのタッチ点間の距離を計算
     */
    getTouchDistance() {
        if (this._touches.length < 2) return 0;
        
        const dx = this._touches[0].clientX - this._touches[1].clientX;
        const dy = this._touches[0].clientY - this._touches[1].clientY;
        return Math.sqrt(dx * dx + dy * dy);
    }

    createBackground() {
        const sprite = new Sprite(new Bitmap(Graphics.width, Graphics.height));
        sprite.bitmap.fillAll("black");
        this.addChild(sprite);
        this._bgSprite = sprite;
    }

    createMainSprite() {
        const sprite = new Sprite();
        sprite.anchor.set(0.5, 0.5);
        sprite.x = Graphics.width / 2;
        sprite.y = Graphics.height / 2;
        this.addChild(sprite);
        this._mainSprite = sprite;
    }

    createNavigationButtons() {
        this._buttons = [];
        
        const specs = [
            { label: STRINGS.btnPrev, action: () => this.previousImage(true) },
            { label: STRINGS.btnNext, action: () => this.nextImage(true) },
            { label: STRINGS.btnList, action: () => this.openGallery() },
            { label: STRINGS.btnExit, action: () => this.exitViewer() }
        ];
        
        // コモンイベントがある場合はイベント再生ボタンを追加
        const currentNumber = this._unlockedNumbers[this._currentIndex];
        const cgData = CGDataManager.getCGData(this._folder, currentNumber);
        
        if (cgData && cgData.commonEventId && cgData.commonEventId > 0) {
            specs.push({ 
                label: CONFIG.eventButtonText, 
                action: () => this.playCommonEvent() 
            });
        }
        
        const V = UI.VIEWER;
        const count = specs.length;
        const availableWidth = Graphics.width - V.BUTTON_SCREEN_MARGIN * 2;

        // specsと同じ順番でpos/imageリストを構築
        const buttonPosList = [
            CONFIG.viewerPrevButtonPos,
            CONFIG.viewerNextButtonPos,
            CONFIG.viewerListButtonPos,
            CONFIG.viewerExitButtonPos,
        ];
        const buttonImageList = [
            CONFIG.viewerPrevButtonImage,
            CONFIG.viewerNextButtonImage,
            CONFIG.viewerListButtonImage,
            CONFIG.viewerExitButtonImage,
        ];
        if (specs.length === 5) {
            buttonPosList.push(CONFIG.viewerEventButtonPos);
            buttonImageList.push(CONFIG.viewerEventButtonImage);
        }

        // ボタン幅を決定
        // テキストボタン：フォントサイズを調整しながらテキスト幅を計測
        // 画像ボタン：dispWidth（0の場合はBUTTON_HEIGHTをフォールバック）で固定
        let fontSize = V.BUTTON_FONT_SIZE;
        let buttonWidths = [];
        let totalNeeded = 0;
        
        for (; fontSize >= V.BUTTON_MIN_FONT_SIZE; fontSize -= 2) {
            const tempBitmap = new Bitmap(1, 1);
            tempBitmap.fontSize = fontSize;
            
            buttonWidths = specs.map((spec, i) => {
                const img = buttonImageList[i];
                if (img.image) {
                    return img.width > 0 ? img.width : V.BUTTON_HEIGHT;
                }
                const textWidth = tempBitmap.measureTextWidth(spec.label);
                return Math.ceil(textWidth + V.BUTTON_PADDING * 2);
            });
            tempBitmap.destroy();
            
            totalNeeded = buttonWidths.reduce((sum, w) => sum + w, 0)
                        + V.BUTTON_MIN_GAP * (count - 1);
            
            if (totalNeeded <= availableWidth) break;
        }
        
        // 最小フォントでも収まらない場合：テキストボタンのみ縮小（画像ボタンは固定）
        if (totalNeeded > availableWidth) {
            const textCount = buttonImageList.filter(img => !img.image).length || 1;
            const imageTotal = buttonWidths
                .filter((_, i) => buttonImageList[i].image)
                .reduce((sum, w) => sum + w, 0);
            const uniformWidth = Math.floor(
                (availableWidth - V.BUTTON_MIN_GAP * (count - 1) - imageTotal) / textCount
            );
            buttonWidths = buttonWidths.map((w, i) =>
                buttonImageList[i].image ? w : Math.max(uniformWidth, 40)
            );
        }
        
        // 間隔を計算（余裕があれば広げるが上限あり）
        const totalButtonWidth = buttonWidths.reduce((sum, w) => sum + w, 0);
        const remainingSpace = availableWidth - totalButtonWidth;
        const gap = count > 1
            ? Math.min(V.BUTTON_MAX_GAP, Math.floor(remainingSpace / (count - 1)))
            : 0;
        
        // 中央配置で描画
        const totalLayoutWidth = totalButtonWidth + gap * (count - 1);
        let x = Math.floor((Graphics.width - totalLayoutWidth) / 2);
        const y = Graphics.height - V.BUTTON_Y_OFFSET;
        
        specs.forEach((spec, i) => {
            const w = buttonWidths[i];
            const img = buttonImageList[i];

            // 画像指定あり → 画像ボタン、なし → テキストボタン
            const button = img.image
                ? UIBuilder.createImageButton(img.image, img.width, img.height, spec.action)
                : UIBuilder.createButton(spec.label, w, V.BUTTON_HEIGHT, spec.action, fontSize);

            // 自動レイアウトのXYをセット（x進行は位置上書き前に計算）
            button.x = x;
            button.y = y;
            x += w + gap;

            // 明示指定があれば上書き（X/Yは独立して上書き可能）
            const pos = buttonPosList[i];
            if (pos && pos.x !== null) button.x = pos.x;
            if (pos && pos.y !== null) button.y = pos.y;

            this.addChild(button);
            this._buttons.push(button);
        });
    }

    createNoContentMessage() {
        const width = 480;
        const height = 60;
        // 2倍解像度で描画してscale 0.5で表示（文字ジャギー軽減）
        const bitmap = new Bitmap(width * 2, height * 2);
        bitmap.smooth = true;
        bitmap.textColor = "white";
        bitmap.outlineColor = "black";
        bitmap.outlineWidth = 8;
        bitmap.fontSize = 44;
        bitmap.drawText(STRINGS.noContent, 0, 0, width * 2, height * 2, "center");
        
        const sprite = new Sprite(bitmap);
        sprite.scale.set(0.5, 0.5);
        sprite.anchor.set(0.5, 0.5);
        sprite.x = Graphics.width / 2;
        sprite.y = Graphics.height / 2;
        this.addChild(sprite);
        this._noContentSprite = sprite;
    }

    createExitButton() {
        this._buttons = [];
        const W = 120;
        const button = UIBuilder.createButton(
            STRINGS.btnExit,
            W,
            UI.VIEWER.BUTTON_HEIGHT,
            () => this.exitViewer()
        );
        button.x = (Graphics.width - W) / 2;
        button.y = Graphics.height - UI.VIEWER.BUTTON_Y_OFFSET;
        this.addChild(button);
        this._buttons.push(button);
    }

    /**
     * 表示更新
     */
    updateDisplay() {
        this.updateButtonVisibility();
        if (this._unlockedNumbers.length > 0) {
            this.showCurrentImage();
            this.preloadAdjacentImages();
        }
    }

    updateButtonVisibility() {
        if (this._buttons) {
            this._buttons.forEach(button => {
                button.visible = this._menuVisible;
            });
        }
    }

    /**
     * 前後の画像をプリロード
     */
    preloadAdjacentImages() {
        if (this._unlockedNumbers.length <= 1) return;
        
        const path = `img/pictures/${this._folder}/`;
        
        // 前の画像
        const prevIndex = (this._currentIndex - 1 + this._unlockedNumbers.length) % this._unlockedNumbers.length;
        const prevNumber = this._unlockedNumbers[prevIndex];
        
        // 次の画像
        const nextIndex = (this._currentIndex + 1) % this._unlockedNumbers.length;
        const nextNumber = this._unlockedNumbers[nextIndex];
        
        // プリロード（エラーは無視）
        try {
            ImageManager.loadBitmap(path, String(prevNumber));
            ImageManager.loadBitmap(path, String(nextNumber));
        } catch (error) {
            // プリロード失敗は無視（実際の表示時にエラー処理される）
        }
    }

    showCurrentImage() {
        if (!this._mainSprite || this._unlockedNumbers.length === 0) return;
        
        const number = this._unlockedNumbers[this._currentIndex];
        const path = `img/pictures/${this._folder}/`;
        const filename = String(number);
        const url = path + Utils.encodeURI(filename) + ".png";
        
        // キャッシュ上のBitmapが破壊されていないかチェック
        // (destroy()済みのBitmapはisReady()=trueだがwidth=0になる)
        const cached = ImageManager._cache[url];
        if (cached && cached.isReady() && cached.width === 0) {
            // 壊れたキャッシュエントリを削除して強制再読み込み
            delete ImageManager._cache[url];
        }
        
        try {
            const bitmap = ImageManager.loadBitmap(path, filename);
            const loadId = ++this._loadId; // レース条件防止
            
            bitmap.addLoadListener(() => {
                // 画像切り替えが起きていたらこのコールバックは無効
                if (this._loadId !== loadId) return;
                
                if (bitmap.width === 0 || bitmap.height === 0) {
                    console.error(`[${PLUGIN_NAME}] Failed to load image: ${path}${filename}`);
                    return;
                }
                
                const scale = Math.min(
                    Graphics.width / bitmap.width,
                    Graphics.height / bitmap.height,
                    1
                );
                this._mainSprite.scale.set(scale, scale);
                this._mainSprite.bitmap = bitmap;
                this._mainSprite.x = Graphics.width / 2;
                this._mainSprite.y = Graphics.height / 2;
                
                // 拡大/縮小コントローラーを初期化
                if (CONFIG.zoomEnabled) {
                    this._zoomController = new CGZoomController(this._mainSprite, scale);
                }
            });
        } catch (error) {
            console.error(`[${PLUGIN_NAME}] Error loading image:`, error);
        }
    }

    /**
     * 画像切り替え共通処理
     * @param {number} delta - 移動方向（-1: 前, +1: 次）
     * @param {boolean} showMenu - ボタンクリック時にメニューを表示するか
     */
    navigateImage(delta, showMenu = false) {
        if (this._unlockedNumbers.length === 0) return;
        this._currentIndex = (this._currentIndex + delta + this._unlockedNumbers.length) % this._unlockedNumbers.length;
        
        // 拡大率をリセット（新しい画像はフィット状態で表示）
        this._zoomController = null;
        
        // ボタンクリック時のみメニューを表示状態に設定（recreateNavigationButtons()の前に設定）
        if (showMenu) {
            this._menuVisible = true;
        }
        
        this.showCurrentImage();
        this.preloadAdjacentImages();
        this.recreateNavigationButtons();
        
        SoundManager.playCursor();
    }

    previousImage(showMenu = false) {
        this.navigateImage(-1, showMenu);
    }

    nextImage(showMenu = false) {
        this.navigateImage(+1, showMenu);
    }

    recreateNavigationButtons() {
        // 既存のボタンのBitmapを破棄してから削除（メモリリーク防止）
        // ★画像ボタン（ImageManagerキャッシュ経由）はdestroyしない。_ownBitmapで判定
        if (this._buttons) {
            this._buttons.forEach(button => {
                if (button.bitmap && button._ownBitmap) {
                    button.bitmap.destroy();
                }
                this.removeChild(button);
            });
        }
        // ボタンを再作成
        this.createNavigationButtons();
        // 表示/非表示を更新
        this.updateButtonVisibility();
    }

    openGallery() {
        $gameTemp._cgGalleryFolder = this._folder;
        SoundManager.playOk();
        this.cleanInput();
        // ギャラリーから来た場合はpopで戻る（スタック肥大化防止）
        const stack = SceneManager._stack;
        if (stack.length > 0 && stack[stack.length - 1] === Scene_CGGallery) {
            SceneManager.pop();
        } else {
            SceneManager.push(Scene_CGGallery);
        }
    }

    playCommonEvent() {
        const currentNumber = this._unlockedNumbers[this._currentIndex];
        const cgData = CGDataManager.getCGData(this._folder, currentNumber);
        
        if (!cgData || !cgData.commonEventId || cgData.commonEventId <= 0) {
            return;
        }
        
        SoundManager.playOk();
        this.cleanInput();
        
        // ビューワーの現在状態を保存（pop後に新Scene_CGViewerが復元できるように）
        $gameTemp._cgViewerArgs = { folder: this._folder, start: currentNumber };
        
        // コモンイベント再生シーンをpush（終了後にビューアーに自動復帰）
        $gameTemp._cgEventPlayerId = cgData.commonEventId;
        SceneManager.push(Scene_CGEventPlayer);
    }

    exitViewer() {
        SoundManager.playCancel();
        this.cleanInput();
        // スタック内のCGシーンをすべてスキップしてマップに直接戻る
        // （ギャラリー経由でビューアーを開いた場合もギャラリーに戻らず終了する）
        while (SceneManager._stack.length > 0) {
            const top = SceneManager._stack[SceneManager._stack.length - 1];
            if (top === Scene_CGViewer || top === Scene_CGGallery || top === Scene_CGEventPlayer) {
                SceneManager._stack.pop();
            } else {
                break;
            }
        }
        SceneManager.pop();
    }

    toggleMenu() {
        this._menuVisible = !this._menuVisible;
        this.updateButtonVisibility();
    }
    
    /**
     * シーン終了時のクリーンアップ
     */
    terminate() {
        // 背景のBitmap破棄（new Bitmap）
        if (this._bgSprite?.bitmap) {
            this._bgSprite.bitmap.destroy();
        }
        // ボタンのBitmap破棄
        // ★画像ボタン（ImageManagerキャッシュ経由）はdestroyしない。_ownBitmapで判定
        if (this._buttons) {
            this._buttons.forEach(button => {
                if (button.bitmap && button._ownBitmap) {
                    button.bitmap.destroy();
                }
            });
        }
        // 「開放済みCGなし」メッセージのBitmap破棄（new Bitmap）
        if (this._noContentSprite?.bitmap) {
            this._noContentSprite.bitmap.destroy();
        }
        
        // ピンチ操作のイベントリスナーを削除
        if (this._touchStartHandler) {
            try {
                window.removeEventListener("touchstart", this._touchStartHandler);
                window.removeEventListener("touchmove", this._touchMoveHandler);
                window.removeEventListener("touchend", this._touchEndHandler);
                window.removeEventListener("touchcancel", this._touchEndHandler);
            } catch (error) {
                console.error(`[${PLUGIN_NAME}] Failed to remove pinch handlers:`, error);
            }
        }
        
        super.terminate();
    }

    /**
     * 更新処理
     */
    update() {
        super.update();
        
        // Rキーでコモンイベント再生
        this.updateCommonEventKey();
        
        // 拡大機能が有効な場合の処理
        if (CONFIG.zoomEnabled && this._zoomController && this._unlockedNumbers.length > 0) {
            this.updateZoomControls();
        }
        
        // ESCキーで終了
        if (Input.isTriggered("escape")) {
            this.exitViewer();
            return;
        }
        
        // Backspaceキーで一覧に戻る
        if (Input.isTriggered("galleryList")) {
            this.openGallery();
            return;
        }
        
        // 左右キーでCG切り替え（Shift押下時は画像移動のため除外）
        if (Input.isTriggered("left") && !Input.isPressed("shift")) {
            this.previousImage();
            return;
        }
        if (Input.isTriggered("right") && !Input.isPressed("shift")) {
            this.nextImage();
            return;
        }
        
        // タッチ入力
        if (TouchInput.isTriggered()) {
            const x = TouchInput.x;
            const y = TouchInput.y;
            
            // クリック判定用に座標を記録
            this._clickStartX = x;
            this._clickStartY = y;
            this._buttonClicked = false;  // フラグ初期化
            
            // ダブルクリック/タップ判定（拡大機能有効時のみ）
            if (CONFIG.zoomEnabled && this._zoomController) {
                const isDouble = this._zoomController.handleDoubleClick(x, y);
                if (isDouble) {
                    return; // ダブルクリック処理完了
                }
            }
            
            // ボタンチェック（メニュー表示中のみ）
            if (this._menuVisible) {
                for (let i = this._buttons.length - 1; i >= 0; i--) {
                    const button = this._buttons[i];
                    if (UIBuilder.hitTest(x, y, button)) {
                        this._buttonClicked = true;  // ボタンがクリックされた
                        button._onClick?.();
                        return;
                    }
                }
            }
        }
        
        // ドラッグ処理（拡大機能有効時のみ）
        if (CONFIG.zoomEnabled && this._zoomController) {
            this.updateDragControls();
        }
        
        // クリック判定（ドラッグと区別）
        if (TouchInput.isReleased()) {
            const dx = TouchInput.x - this._clickStartX;
            const dy = TouchInput.y - this._clickStartY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            // 10px未満の移動かつボタンがクリックされていない場合のみメニュー切り替え
            if (distance < 10 && !this._buttonClicked) {
                this.toggleMenu();
            }
        }
    }
    
    /**
     * 拡大/縮小コントロールの更新
     */
    updateZoomControls() {
        if (!this._zoomController) return;
        
        const zoomFactor = 1 + CONFIG.zoomSpeed;
        const zoomInverse = 1 / zoomFactor;
        
        // ★重要: Shift + 矢印キーを先にチェック（拡大/縮小と競合するため）
        if (Input.isPressed("shift")) {
            const speed = CONFIG.panSpeed;
            let moved = false;
            
            if (Input.isRepeated("left")) {
                this._zoomController.pan(speed, 0);
                moved = true;
            }
            if (Input.isRepeated("right")) {
                this._zoomController.pan(-speed, 0);
                moved = true;
            }
            if (Input.isRepeated("up")) {
                this._zoomController.pan(0, speed);
                moved = true;
            }
            if (Input.isRepeated("down")) {
                this._zoomController.pan(0, -speed);
                moved = true;
            }
            
            // Shift押下中は拡大/縮小をスキップ
            if (moved) return;
        }
        
        // キーボード: +/- または ↑↓ で拡大/縮小
        if (Input.isRepeated("pageup") || Input.isRepeated("up")) {
            this._zoomController.zoomAtCenter(zoomFactor);
        }
        if (Input.isRepeated("pagedown") || Input.isRepeated("down")) {
            this._zoomController.zoomAtCenter(zoomInverse);
        }
        
        // 0キーまたはHomeキーで100%リセット
        if (Input.isTriggered("tab")) { // tabキーを0代わりに使用
            this._zoomController.resetZoom();
        }
        
        // Enterキーでフィット⇔100%トグル
        if (Input.isTriggered("ok")) {
            this._zoomController.toggleFitAnd100();
        }
        
        // マウスホイールで拡大/縮小（Googleマップと同じ方向）
        if (TouchInput.wheelY !== 0) {
            const delta = TouchInput.wheelY;
            // ホイール上（正）= 縮小、ホイール下（負）= 拡大
            const factor = delta > 0 ? zoomInverse : zoomFactor;
            this._zoomController.zoomAtPoint(TouchInput.x, TouchInput.y, factor);
        }
        
        // ゴム跳ね返り効果の更新
        this._zoomController.updateBounce();
    }
    
    /**
     * ドラッグコントロールの更新
     */
    updateDragControls() {
        if (!this._zoomController) return;
        
        // 拡大中のみドラッグを有効化
        if (this._zoomController.getScale() <= this._zoomController.getFitScale() + 0.01) {
            this._zoomController.endDrag();
            return;
        }
        
        if (TouchInput.isPressed()) {
            if (!this._zoomController.isDragging()) {
                // ドラッグ開始（メニュー非表示時のみ）
                if (!this._menuVisible) {
                    this._zoomController.startDrag(TouchInput.x, TouchInput.y);
                }
            } else {
                // ドラッグ更新
                this._zoomController.updateDrag(TouchInput.x, TouchInput.y);
            }
        } else {
            // ドラッグ終了
            this._zoomController.endDrag();
        }
    }
}

//=============================================================================
// Scene_CGGallery - CGギャラリー
//=============================================================================

class Scene_CGGallery extends Scene_Base {
    create() {
        super.create();
        this.initializeGalleryData();
        this.createDisplayObjects();
        this.refreshThumbnails();
        this.updatePageButtons();
    }

    /**
     * ギャラリーデータの初期化
     */
    initializeGalleryData() {
        this._folder = $gameTemp._cgGalleryFolder || "cg01";
        this._allNumbers = CGDataManager.listNumbers(this._folder);
        this._pageSize = CONFIG.thumbCols * CONFIG.thumbRows;
        
        // 保存済み状態があれば復元、なければ先頭から
        const saved = Scene_CGGallery._savedState[this._folder];
        if (saved) {
            const maxPage = Math.max(0, Math.floor((this._allNumbers.length - 1) / this._pageSize));
            this._currentPage = Math.min(saved.page, maxPage);
            this._focusedIndex = saved.focusedIndex;
        } else {
            this._currentPage = 0;
            this._focusedIndex = -1;
        }
        
        // フェード制御
        this._fadeState = FADE_STATE.NONE;
        this._fadeTimer = 0;
        this._fadeCallback = null;
        
        // キーボードフォーカス（ボタン側は常にリセット）
        this._focusedButton = -1;  // -1 = フォーカスなし、0以上 = ボタンインデックス
    }

    /**
     * 表示オブジェクトの作成
     */
    createDisplayObjects() {
        this.createBackground();
        this.createBackgroundImage();  // 背景画像（黒背景の上、サムネイルの下）
        this.createThumbnailContainer();
        this.createFocusFrame();
        this.createFooterButtons();
        this.createUnlockCounter();    // 開放カウンター（ボタンの上、フェードの下）
        this.createFadeOverlay(); // 最前面に配置
    }

    createBackground() {
        const sprite = new Sprite(new Bitmap(Graphics.width, Graphics.height));
        sprite.bitmap.fillAll("black");
        this.addChild(sprite);
        this._bgSprite = sprite;
    }
    
    createBackgroundImage() {
        // 背景画像が設定されていない場合は何もしない
        if (!CONFIG.galleryBackground) return;
        
        const filename = CONFIG.galleryBackground;
        // ImageManager.loadPicture() は自動で ".png" を付加するため、拡張子を除去してから渡す
        const bitmap = ImageManager.loadPicture(filename.replace(/\.[^.]+$/, ''));
        
        bitmap.addLoadListener(() => {
            if (bitmap.width === 0 || bitmap.height === 0) {
                console.warn(`[${PLUGIN_NAME}] Failed to load gallery background: ${filename}`);
                return;
            }
            
            const mode = CONFIG.galleryBackgroundMode;
            let sprite;
            
            // 表示モードに応じて処理
            if (mode === "tile") {
                // タイル状に繰り返し
                sprite = new TilingSprite(bitmap);
                sprite.move(0, 0, Graphics.width, Graphics.height);
                
            } else {
                // original または stretch
                sprite = new Sprite(bitmap);
                
                if (mode === "stretch") {
                    // 画面全体に引き伸ばし
                    sprite.scale.x = Graphics.width / bitmap.width;
                    sprite.scale.y = Graphics.height / bitmap.height;
                    sprite.x = 0;
                    sprite.y = 0;
                    
                } else {
                    // original: 元サイズで中央配置
                    sprite.scale.x = 1.0;
                    sprite.scale.y = 1.0;
                    sprite.x = (Graphics.width - bitmap.width) / 2;
                    sprite.y = (Graphics.height - bitmap.height) / 2;
                }
            }
            
            // 黒背景の直後に挿入（サムネイルより下）
            this.addChildAt(sprite, 1);
        });
    }

    createFadeOverlay() {
        const sprite = new Sprite(new Bitmap(Graphics.width, Graphics.height));
        sprite.bitmap.fillAll("black");
        sprite.opacity = 0;
        this.addChild(sprite);
        this._fadeOverlay = sprite;
    }

    createThumbnailContainer() {
        this._thumbnailContainer = new Sprite();
        this.addChild(this._thumbnailContainer);
    }
    
    createFocusFrame() {
        // フォーカス枠（矩形の枠線）
        this._focusFrame = new Sprite();
        this._focusFrame.visible = false;
        this.addChild(this._focusFrame);
    }
    
    updateFocusFrame() {
        if (!this._focusFrame) return;
        
        // 旧Bitmapを破棄（メモリリーク防止）
        if (this._focusFrame.bitmap) {
            this._focusFrame.bitmap.destroy();
            this._focusFrame.bitmap = null;
        }
        
        // ボタンフォーカスの場合
        if (this._focusedButton >= 0) {
            const button = this._footerButtons[this._focusedButton];
            if (!button) {
                this._focusFrame.visible = false;
                return;
            }
            
            const frameWidth = button._hitRect.w;
            const frameHeight = button._hitRect.h;
            const bitmap = new Bitmap(frameWidth, frameHeight);
            const color = CONFIG.focusColor;
            const thickness = 3;
            
            // 矩形の枠線を描画
            bitmap.fillRect(0, 0, frameWidth, thickness, color);  // 上
            bitmap.fillRect(0, frameHeight - thickness, frameWidth, thickness, color);  // 下
            bitmap.fillRect(0, 0, thickness, frameHeight, color);  // 左
            bitmap.fillRect(frameWidth - thickness, 0, thickness, frameHeight, color);  // 右
            
            this._focusFrame.bitmap = bitmap;
            this._focusFrame.x = button.x + button._hitRect.x;
            this._focusFrame.y = button.y + button._hitRect.y;
            this._focusFrame.visible = true;
            return;
        }
        
        // サムネイルフォーカスの場合
        if (this._focusedIndex < 0) {
            this._focusFrame.visible = false;
            return;
        }
        
        const start = this._currentPage * this._pageSize;
        const pageNumbers = this._allNumbers.slice(start, start + this._pageSize);
        const indexInPage = this._focusedIndex - start;
        
        // 現在のページ範囲外ならフォーカス枠を非表示
        if (indexInPage < 0 || indexInPage >= pageNumbers.length) {
            this._focusFrame.visible = false;
            return;
        }
        
        const margin = CONFIG.gridMargin;
        const footerHeight = UI.GALLERY.BUTTON_HEIGHT + UI.GALLERY.BUTTON_Y_OFFSET;
        const gridWidth = Graphics.width - margin * 2;
        const gridHeight = Graphics.height - footerHeight - margin * 2;
        const cellWidth = Math.floor(gridWidth / CONFIG.thumbCols);
        const cellHeight = Math.floor(gridHeight / CONFIG.thumbRows);
        
        const row = Math.floor(indexInPage / CONFIG.thumbCols);
        const col = indexInPage % CONFIG.thumbCols;
        const cellX = margin + col * cellWidth;
        const cellY = margin + row * cellHeight;
        
        // 枠線を描画
        const frameWidth = cellWidth;
        const frameHeight = cellHeight;
        const bitmap = new Bitmap(frameWidth, frameHeight);
        const color = CONFIG.focusColor;
        const thickness = 3;
        
        // 矩形の枠線を描画
        bitmap.fillRect(0, 0, frameWidth, thickness, color);  // 上
        bitmap.fillRect(0, frameHeight - thickness, frameWidth, thickness, color);  // 下
        bitmap.fillRect(0, 0, thickness, frameHeight, color);  // 左
        bitmap.fillRect(frameWidth - thickness, 0, thickness, frameHeight, color);  // 右
        
        this._focusFrame.bitmap = bitmap;
        this._focusFrame.x = cellX;
        this._focusFrame.y = cellY;
        this._focusFrame.visible = true;
    }

    createFooterButtons() {
        const specs = [
            { label: STRINGS.btnPrevPage,    action: () => this.previousPage() },
            { label: STRINGS.btnNextPage,    action: () => this.nextPage() },
            { label: STRINGS.btnGalleryExit, action: () => this.exitGallery() }
        ];
        
        this._footerButtons = [];
        
        const { BUTTON_WIDTH: W, BUTTON_HEIGHT: H, BUTTON_SPACING: S, BUTTON_Y_OFFSET: Y } = UI.GALLERY;
        const totalWidth = W * specs.length + S * (specs.length - 1);
        const startX = Math.floor((Graphics.width - totalWidth) / 2);
        const y = Graphics.height - Y;

        const buttonPosList = [
            CONFIG.galleryPrevButtonPos,
            CONFIG.galleryNextButtonPos,
            CONFIG.galleryExitButtonPos,
        ];
        const buttonImageList = [
            CONFIG.galleryPrevButtonImage,
            CONFIG.galleryNextButtonImage,
            CONFIG.galleryExitButtonImage,
        ];
        
        specs.forEach((spec, i) => {
            const img = buttonImageList[i];

            // 画像指定あり → 画像ボタン、なし → テキストボタン
            const button = img.image
                ? UIBuilder.createImageButton(img.image, img.width, img.height, spec.action)
                : UIBuilder.createButton(spec.label, W, H, spec.action);

            // 自動レイアウトXYをセット
            button.x = startX + i * (W + S);
            button.y = y;

            // 明示指定があれば上書き
            const pos = buttonPosList[i];
            if (pos && pos.x !== null) button.x = pos.x;
            if (pos && pos.y !== null) button.y = pos.y;

            this.addChild(button);
            this._footerButtons.push(button);
        });
    }

    /**
     * 開放カウンターを作成（例: 12 / 30）
     * ギャラリー下部ボタンの右側に表示する。
     * 表示位置はプラグインパラメーターで上書き可能。
     */
    createUnlockCounter() {
        if (!CONFIG.showUnlockCounter) return;

        const total    = CGDataManager.listNumbers(this._folder).length;
        const unlocked = $gameSystem.getUnlockedCGs(this._folder).length;
        const text     = `${unlocked} / ${total}`;

        // 表示位置の計算
        // デフォルト: フッターボタン3個の右端＋マージン、ボタンと同じY
        const { BUTTON_HEIGHT: H, BUTTON_Y_OFFSET: Y_OFFSET } = UI.GALLERY;

        const defaultX = Math.floor(Graphics.width * 750 / 816);
        const defaultY = Graphics.height - Y_OFFSET;

        const x = CONFIG.unlockCounterPos.x !== null ? CONFIG.unlockCounterPos.x : defaultX;
        const y = CONFIG.unlockCounterPos.y !== null ? CONFIG.unlockCounterPos.y : defaultY;

        // 描画領域: 画面右端までの余白を使用
        // 2倍解像度で描画してscale 0.5で表示（文字ジャギー軽減、既存パターンと同じ）
        const drawW = Math.max(40, (Graphics.width - x - CONFIG.gridMargin)) * 2;
        const drawH = H * 2;
        const bitmap = new Bitmap(drawW, drawH);
        bitmap.smooth = true;
        bitmap.textColor    = CONFIG.counterTextColor;
        bitmap.outlineColor = CONFIG.counterOutlineColor;
        bitmap.outlineWidth = CONFIG.counterOutlineWidth * 2;  // 2倍解像度のため倍にする
        bitmap.fontSize     = CONFIG.counterFontSize * 2;      // 2倍解像度のため倍にする
        bitmap.drawText(text, 0, 0, drawW, drawH, "left");

        const sprite    = new Sprite(bitmap);
        sprite.scale.set(0.5, 0.5);
        sprite.x        = x;
        sprite.y        = y;
        sprite._ownBitmap = true;  // new Bitmap()製のためterminateで破棄してよい

        this._unlockCounterSprite = sprite;
        this.addChild(sprite);
    }

    /**
     * 最大ページ番号を取得
     * @returns {number}
     */
    getMaxPage() {
        return Math.max(0, Math.floor((this._allNumbers.length - 1) / this._pageSize));
    }

    /**
     * ページ数に応じてページボタンの表示を更新
     */
    updatePageButtons() {
        const visible = this.getMaxPage() > 0;
        // 前ページ・次ページボタン（0番目と1番目）のみ制御
        if (this._footerButtons[0]) this._footerButtons[0].visible = visible;
        if (this._footerButtons[1]) this._footerButtons[1].visible = visible;
    }

    /**
     * サムネイル表示更新
     */
    refreshThumbnails() {
        // 旧サムネイルの手動生成Bitmapを破棄（メモリリーク防止）
        if (this._thumbnailContainer) {
            for (const child of this._thumbnailContainer.children) {
                // 説明文スプライト等（_ownBitmapフラグで手動生成Bitmapのみdestroyする）
                if (child.bitmap && child._ownBitmap) {
                    child.bitmap.destroy();
                }
                // LOCKEDサムネイル等の子スプライト（同様に_ownBitmapのみdestroy）
                if (child.children) {
                    for (const grandChild of child.children) {
                        if (grandChild.bitmap && grandChild._ownBitmap) {
                            grandChild.bitmap.destroy();
                        }
                    }
                }
            }
        }
        this._thumbnailContainer.removeChildren();
        
        const start = this._currentPage * this._pageSize;
        const pageNumbers = this._allNumbers.slice(start, start + this._pageSize);
        
        if (pageNumbers.length === 0) return;
        
        const margin = CONFIG.gridMargin;
        const footerHeight = UI.GALLERY.BUTTON_HEIGHT + UI.GALLERY.BUTTON_Y_OFFSET;
        const gridWidth = Graphics.width - margin * 2;
        const gridHeight = Graphics.height - footerHeight - margin * 2;
        const cellWidth = Math.floor(gridWidth / CONFIG.thumbCols);
        const cellHeight = Math.floor(gridHeight / CONFIG.thumbRows);
        
        pageNumbers.forEach((number, index) => {
            const row = Math.floor(index / CONFIG.thumbCols);
            const col = index % CONFIG.thumbCols;
            
            const cellX = margin + col * cellWidth;
            const cellY = margin + row * cellHeight;
            
            const sprite = this.createThumbnailSprite(number, cellWidth, cellHeight);
            sprite.x = cellX + Math.floor(cellWidth / 2);
            sprite.y = cellY + Math.floor(cellHeight / 2);
            
            this._thumbnailContainer.addChild(sprite);
            
            // 説明文を別途追加（imageスプライトのscaleに影響されないように直接コンテナに追加）
            const isUnlocked = $gameSystem.isCGUnlocked(this._folder, number);
            const showDesc = isUnlocked || CONFIG.showLockedDescription;
            
            if (showDesc) {
                const cgData = CGDataManager.getCGData(this._folder, number);
                if (cgData && cgData.description && CONFIG.descHeight > 0) {
                    const descH = CONFIG.descHeight;
                    // 画像表示幅に合わせた説明文幅を計算（_applyLockedTextと同じ式）
                    // これにより「左揃え/中央揃え/右揃え」が画像を軸として機能する
                    const thumbnailH = cellHeight - descH;
                    const imageW = Math.min(
                        cellWidth - CONFIG.thumbPadding,
                        Math.round((thumbnailH - CONFIG.thumbPadding) * Graphics.width / Graphics.height)
                    );
                    const descW = imageW;
                    const descX = cellX + Math.floor((cellWidth - descW) / 2);
                    // MZのWindow描画と同じ1:1方式で描画（PixiJSスケーリングなし）
                    const descBitmap = new Bitmap(descW, descH);
                    descBitmap.fontSize = Math.max(14, descH - 8);
                    descBitmap.textColor = CONFIG.descTextColor;
                    descBitmap.outlineColor = CONFIG.descOutlineColor;
                    descBitmap.outlineWidth = CONFIG.descOutlineWidth;
                    // Canvas APIはlineWidth=0のセットを仕様上無視するため、
                    // outlineWidth=0のときは_drawTextOutlineを無効化してstrokeTextをスキップする。
                    if (CONFIG.descOutlineWidth <= 0) {
                        descBitmap._drawTextOutline = function() {};
                    }
                    descBitmap.drawText(cgData.description, 0, 0, descW, descH, CONFIG.descTextAlign);
                    const descText = new Sprite(descBitmap);
                    descText.x = descX;
                    descText.y = cellY + cellHeight - descH - 2;
                    // new Bitmap()で生成したためdestroyしてよいとマーク
                    descText._ownBitmap = true;
                    this._thumbnailContainer.addChild(descText);
                }
            }
        });
        
        // プリロード（次ページと前ページ）
        this.preloadAdjacentPages();
        
        // フォーカス枠を更新
        this.updateFocusFrame();
    }

    /**
     * 前後ページの画像をプリロード
     */
    preloadAdjacentPages() {
        const maxPage = this.getMaxPage();
        if (maxPage === 0) return; // 1ページのみならプリロード不要
        
        const path = `img/pictures/${this._folder}/`;
        const pagesToPreload = [];
        
        // 前ページ
        const prevPage = this._currentPage > 0 ? this._currentPage - 1 : maxPage;
        pagesToPreload.push(prevPage);
        
        // 次ページ
        const nextPage = this._currentPage < maxPage ? this._currentPage + 1 : 0;
        pagesToPreload.push(nextPage);
        
        // プリロード実行
        pagesToPreload.forEach(page => {
            const start = page * this._pageSize;
            const pageNumbers = this._allNumbers.slice(start, start + this._pageSize);
            
            pageNumbers.forEach(number => {
                const isUnlocked = $gameSystem.isCGUnlocked(this._folder, number);
                if (isUnlocked) {
                    try {
                        ImageManager.loadBitmap(path, String(number));
                    } catch (error) {
                        // プリロード失敗は無視
                    }
                }
            });
        });
    }

    createThumbnailSprite(number, cellWidth, cellHeight) {
        const sprite = new Sprite();
        sprite.anchor.set(0.5, 0.5);
        sprite._hitRect = {
            x: -Math.floor(cellWidth / 2),
            y: -Math.floor(cellHeight / 2),
            w: cellWidth,
            h: cellHeight
        };
        sprite._cgNumber = number;
        
        const isUnlocked = $gameSystem.isCGUnlocked(this._folder, number);
        
        // サムネイル画像用の高さ（説明文エリアを除いた高さ）
        const thumbnailHeight = cellHeight - CONFIG.descHeight;
        
        if (isUnlocked) {
            this.loadThumbnailImage(sprite, number, cellWidth, thumbnailHeight);
        } else {
            const lockedSprite = UIBuilder.createLockedThumb(
                cellWidth - CONFIG.thumbPadding,
                thumbnailHeight - CONFIG.thumbPadding
            );
            lockedSprite.anchor.set(0.5, 0.5);
            sprite.addChild(lockedSprite);
        }
        
        return sprite;
    }

    loadThumbnailImage(sprite, number, cellWidth, cellHeight) {
        // ★設計上の重要な注意点:
        //
        // [問題1] Bitmap._onError は _callLoadListeners() を呼ばないため、
        //   addLoadListener はファイルが存在しない場合に発火しない。
        //   「thumbが失敗したら元画像にフォールバック」という連鎖方式は機能しない。
        //
        // [問題2] ImageManager.loadBitmap() はキャッシュにBitmapを保持する。
        //   Scene_Base.isReady() → ImageManager.isReady() は全キャッシュをスキャンし、
        //   isError()なBitmapがあればthrowLoadError()を投げる。
        //   存在しないthumb画像をImageManager経由でロードすると
        //   ギャラリーシーンのisReady()でエラー画面が出る。
        //
        // [解決策]
        //   元画像: ImageManager.loadBitmap()（キャッシュ経由・正常動作）
        //   thumb:  Bitmap.load()直接（キャッシュを迂回・isReady()に影響しない）
        //
        //   元画像のaddLoadListenerは必ず発火（ファイルが存在するため）→ 保証
        //   thumbのaddLoadListenerはthumbが存在する場合のみ発火 → 成功時のみ上書き
        
        const basePath  = `img/pictures/${this._folder}/`;
        const thumbPath = `img/pictures/${this._folder}/thumb/`;
        const filename  = String(number);
        
        // スプライトにBitmapを適用するヘルパー
        // thumbBitmap（事前リサイズ済み）および bilinear モード時の元画像に使用
        const applyToSprite = (bitmap) => {
            const scale = Math.min(
                (cellWidth  - CONFIG.thumbPadding) / bitmap.width,
                (cellHeight - CONFIG.thumbPadding) / bitmap.height,
                1
            );
            sprite.scale.set(scale, scale);
            sprite.bitmap = bitmap;
        };

        // origBitmap（フルサイズCG）用：モードに応じて縮小方式を切り替え
        // bilinear: PixiJSのGPUスケール（MZデフォルトと同等。シャープ）
        // area:     Canvas高品質リサンプリング（imageSmoothingQuality='high'）。なめらか。
        // ※ origBitmapはImageManagerの共有キャッシュのためsmooth変更は不可。
        //   areaモードでは独立したBitmapを生成して表示し、_ownBitmapでdestroyを管理する。
        const applyOrigToSprite = (bitmap) => {
            if (CONFIG.thumbScaleMode === "area") {
                const dw = Math.max(1, Math.round(
                    Math.min((cellWidth  - CONFIG.thumbPadding),
                             (cellHeight - CONFIG.thumbPadding) * bitmap.width / bitmap.height)
                ));
                const dh = Math.max(1, Math.round(dw * bitmap.height / bitmap.width));
                const downBmp = new Bitmap(dw, dh);
                downBmp.context.imageSmoothingEnabled = true;
                downBmp.context.imageSmoothingQuality = 'high';
                downBmp.blt(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, dw, dh);
                downBmp._ownBitmap = true;
                sprite.scale.set(1, 1);
                sprite.bitmap = downBmp;
                sprite._ownBitmap = true;
            } else {
                // bilinear: 従来通りPixiJSスプライトスケール
                applyToSprite(bitmap);
            }
        };
        
        try {
            // ① 元画像をImageManager経由でロード（フォールバック保証）
            const origUrl = basePath + Utils.encodeURI(filename) + ".png";
            const origCached = ImageManager._cache[origUrl];
            if (origCached && origCached.isReady() && origCached.width === 0) {
                delete ImageManager._cache[origUrl];
            }
            const origBitmap = ImageManager.loadBitmap(basePath, filename);
            origBitmap.addLoadListener(() => {
                if (origBitmap.width === 0 || origBitmap.height === 0) {
                    console.error(`[${PLUGIN_NAME}] Failed to load thumbnail: ${basePath}${filename}`);
                    return;
                }
                // thumbがまだ適用されていない場合のみ元画像を適用
                if (!sprite.bitmap) {
                    applyOrigToSprite(origBitmap);
                }
            });
            
            // ② thumb画像をBitmap.load()直接でロード（ImageManagerキャッシュを迂回）
            //   存在しない場合は_onErrorが呼ばれてコールバックは発火しないが、
            //   ①で元画像が適用されているので問題ない。
            //   キャッシュを迂回するためImageManager.isReady()にも影響しない。
            const thumbUrl = thumbPath + Utils.encodeURI(filename) + ".png";
            const thumbBitmap = Bitmap.load(thumbUrl);
            thumbBitmap.addLoadListener(() => {
                if (thumbBitmap.width > 0 && thumbBitmap.height > 0) {
                    // thumbが存在した → 元画像を上書き（事前リサイズ済みのため常にapplyToSprite）
                    applyToSprite(thumbBitmap);
                }
            });
            
        } catch (error) {
            console.error(`[${PLUGIN_NAME}] Error loading thumbnail:`, error);
        }
    }

    /**
     * フェード制御
     */
    startFade(state, callback) {
        this._fadeState = state;
        this._fadeTimer = CONFIG.fadeDuration;
        this._fadeCallback = callback;
    }

    updateFade() {
        if (this._fadeState === FADE_STATE.NONE) return;
        
        const step = 255 / CONFIG.fadeDuration;
        
        if (this._fadeState === FADE_STATE.OUT) {
            this._fadeOverlay.opacity = Math.min(255, this._fadeOverlay.opacity + step);
            if (--this._fadeTimer <= 0) {
                this._fadeState = FADE_STATE.NONE;
                if (this._fadeCallback) {
                    this._fadeCallback();
                    this._fadeCallback = null;
                }
            }
        } else if (this._fadeState === FADE_STATE.IN) {
            this._fadeOverlay.opacity = Math.max(0, this._fadeOverlay.opacity - step);
            if (--this._fadeTimer <= 0) {
                this._fadeState = FADE_STATE.NONE;
                if (this._fadeCallback) {
                    this._fadeCallback();
                    this._fadeCallback = null;
                }
            }
        }
    }

    /**
     * ページ切り替え
     */
    previousPage() {
        if (this._fadeState !== FADE_STATE.NONE) return;
        
        const currentButtonFocus = this._focusedButton;  // 現在のボタンフォーカスを保存
        
        this.startFade(FADE_STATE.OUT, () => {
            const maxPage = this.getMaxPage();
            this._currentPage = this._currentPage > 0 ? this._currentPage - 1 : maxPage;
            this._focusedIndex = -1;    // サムネイルフォーカスリセット
            this._focusedButton = currentButtonFocus;  // ボタンフォーカスは維持
            this.refreshThumbnails();
            this.startFade(FADE_STATE.IN, null);
        });
        
        SoundManager.playCursor();
    }

    nextPage() {
        if (this._fadeState !== FADE_STATE.NONE) return;
        
        const currentButtonFocus = this._focusedButton;  // 現在のボタンフォーカスを保存
        
        this.startFade(FADE_STATE.OUT, () => {
            const maxPage = this.getMaxPage();
            this._currentPage = this._currentPage < maxPage ? this._currentPage + 1 : 0;
            this._focusedIndex = -1;    // サムネイルフォーカスリセット
            this._focusedButton = currentButtonFocus;  // ボタンフォーカスは維持
            this.refreshThumbnails();
            this.startFade(FADE_STATE.IN, null);
        });
        
        SoundManager.playCursor();
    }

    /**
     * 現在のページ・フォーカス状態をstatic変数に保存
     */
    _saveGalleryState() {
        Scene_CGGallery._savedState[this._folder] = {
            page: this._currentPage,
            focusedIndex: this._focusedIndex
        };
    }

    exitGallery() {
        // 終了前に状態を保存
        this._saveGalleryState();
        SoundManager.playCancel();
        this.cleanInput();
        // スタック内のCGシーンをスキップしてマップに直接戻る
        while (SceneManager._stack.length > 0) {
            const top = SceneManager._stack[SceneManager._stack.length - 1];
            if (top === Scene_CGViewer || top === Scene_CGGallery || top === Scene_CGEventPlayer) {
                SceneManager._stack.pop();
            } else {
                break;
            }
        }
        SceneManager.pop();
    }

    /**
     * シーン終了時のクリーンアップ
     */
    terminate() {
        // 背景のBitmap破棄（new Bitmap）
        if (this._bgSprite?.bitmap) {
            this._bgSprite.bitmap.destroy();
        }
        // ※サムネイルのBitmapはImageManagerキャッシュ経由のため破棄しない。
        // destroy()するとキャッシュ内のBitmapが破壊され、
        // 次にビューワーが同じ画像を読み込む際に壊れたBitmapが返される。
        
        // フッターボタンのBitmap破棄
        // ★画像ボタン（ImageManagerキャッシュ経由）はdestroyしない。_ownBitmapで判定
        if (this._footerButtons) {
            this._footerButtons.forEach(button => {
                if (button.bitmap && button._ownBitmap) {
                    button.bitmap.destroy();
                }
            });
        }
        // フォーカス枠のBitmap破棄（new Bitmap）
        if (this._focusFrame && this._focusFrame.bitmap) {
            this._focusFrame.bitmap.destroy();
        }
        // フェードオーバーレイのBitmap破棄（new Bitmap）
        if (this._fadeOverlay && this._fadeOverlay.bitmap) {
            this._fadeOverlay.bitmap.destroy();
        }
        // 開放カウンターのBitmap破棄（new Bitmap、_ownBitmapフラグ確認）
        if (this._unlockCounterSprite?.bitmap && this._unlockCounterSprite._ownBitmap) {
            this._unlockCounterSprite.bitmap.destroy();
        }
        
        super.terminate();

        // プレビューモードで開かれた場合、ツールに終了を通知する
        if (Scene_CGGallery._openedByPreview) {
            Scene_CGGallery._openedByPreview = false;
            sendResponseToTool({ type: 'previewInGameEnded' });
        }
    }
    
    /**
     * キーボードフォーカス操作
     */
    updateKeyboardControls() {
        // PageUp/PageDown でページ切り替え（フォーカス状態によらず常に有効）
        if (Input.isTriggered("pageup")) {
            this.previousPage();
            return;
        }
        if (Input.isTriggered("pagedown")) {
            this.nextPage();
            return;
        }
        
        const start = this._currentPage * this._pageSize;
        const pageNumbers = this._allNumbers.slice(start, start + this._pageSize);
        
        if (pageNumbers.length === 0) return;
        
        // ボタンフォーカス中の処理
        if (this._focusedButton >= 0) {
            let newButton = this._focusedButton;
            let moved = false;
            
            // 左右でボタン間移動
            if (Input.isRepeated("left")) {
                newButton = Math.max(0, this._focusedButton - 1);
                moved = (newButton !== this._focusedButton);
            } else if (Input.isRepeated("right")) {
                newButton = Math.min(this._footerButtons.length - 1, this._focusedButton + 1);
                moved = (newButton !== this._focusedButton);
            }
            
            if (moved) {
                this._focusedButton = newButton;
                this.updateFocusFrame();
                SoundManager.playCursor();
                return;
            }
            
            // 上キーでサムネイルに戻る
            if (Input.isRepeated("up")) {
                this._focusedButton = -1;
                // 最下段の中央列にフォーカス
                const lastRowStart = Math.floor((pageNumbers.length - 1) / CONFIG.thumbCols) * CONFIG.thumbCols;
                this._focusedIndex = start + Math.min(lastRowStart + Math.floor(CONFIG.thumbCols / 2), pageNumbers.length - 1);
                this.updateFocusFrame();
                SoundManager.playCursor();
                return;
            }
            
            // Enter/Zでボタン実行
            if (Input.isTriggered("ok")) {
                const button = this._footerButtons[this._focusedButton];
                if (button && button._onClick) {
                    button._onClick();
                }
                return;
            }
            
            return;  // ボタンフォーカス中は他の処理をスキップ
        }
        
        // サムネイルフォーカス中の処理
        // フォーカスがない場合は最初の要素にフォーカス
        if (this._focusedIndex < 0) {
            if (Input.isTriggered("up") || Input.isTriggered("down") || 
                Input.isTriggered("left") || Input.isTriggered("right")) {
                this._focusedIndex = start;
                this._focusedButton = -1;
                this.updateFocusFrame();
                SoundManager.playCursor();
                return;
            }
            return;  // フォーカスなしで他のキーは無視
        }
        
        const indexInPage = this._focusedIndex - start;
        const row = Math.floor(indexInPage / CONFIG.thumbCols);
        const col = indexInPage % CONFIG.thumbCols;
        
        let newRow = row;
        let newCol = col;
        let moved = false;
        
        // 矢印キーでフォーカス移動
        if (Input.isRepeated("up")) {
            newRow = Math.max(0, row - 1);
            moved = (newRow !== row);
        } else if (Input.isRepeated("down")) {
            // 最下段でさらに下 → ボタンフォーカスへ
            if (row === CONFIG.thumbRows - 1 || indexInPage + CONFIG.thumbCols >= pageNumbers.length) {
                this._focusedIndex = -1;
                this._focusedButton = 1;  // 中央ボタン（次ページ）にフォーカス
                this.updateFocusFrame();
                SoundManager.playCursor();
                return;
            }
            newRow = Math.min(CONFIG.thumbRows - 1, row + 1);
            moved = (newRow !== row);
        } else if (Input.isRepeated("left")) {
            newCol = Math.max(0, col - 1);
            moved = (newCol !== col);
        } else if (Input.isRepeated("right")) {
            newCol = Math.min(CONFIG.thumbCols - 1, col + 1);
            moved = (newCol !== col);
        }
        
        if (moved) {
            const newIndexInPage = newRow * CONFIG.thumbCols + newCol;
            if (newIndexInPage < pageNumbers.length) {
                this._focusedIndex = start + newIndexInPage;
                this.updateFocusFrame();
                SoundManager.playCursor();
            }
            return;
        }
        
        // Enter/Zキーで選択
        if (Input.isTriggered("ok")) {
            const number = pageNumbers[indexInPage];
            if (number !== undefined) {
                this.onThumbnailClick(number);
            }
            return;
        }
    }

    /**
     * 更新処理
     */
    update() {
        super.update();
        this.updateFade();
        
        // ESCキーで終了
        if (Input.isTriggered("escape")) {
            this.exitGallery();
            return;
        }
        
        // キーボード操作
        this.updateKeyboardControls();
        
        // タッチ入力
        if (TouchInput.isTriggered()) {
            const x = TouchInput.x;
            const y = TouchInput.y;
            
            // サムネイルクリック
            for (let i = this._thumbnailContainer.children.length - 1; i >= 0; i--) {
                const sprite = this._thumbnailContainer.children[i];
                if (UIBuilder.hitTest(x, y, sprite)) {
                    this.onThumbnailClick(sprite._cgNumber);
                    return;
                }
            }
            
            // フッターボタンクリック
            for (let i = this._footerButtons.length - 1; i >= 0; i--) {
                const button = this._footerButtons[i];
                if (UIBuilder.hitTest(x, y, button)) {
                    button._onClick?.();
                    return;
                }
            }
        }
    }

    onThumbnailClick(number) {
        const isUnlocked = $gameSystem.isCGUnlocked(this._folder, number);
        
        if (isUnlocked) {
            // ギャラリーを離れる前に現在状態を保存
            this._saveGalleryState();
            $gameTemp._cgGallerySelected = { folder: this._folder, number: number };
            SoundManager.playOk();
            SceneManager.push(Scene_CGViewer);
        } else {
            SoundManager.playBuzzer();
        }
    }
}

// ギャラリーのページ・カーソル位置をセッション中保持するstatic変数
// 構造: { "フォルダ名": { page: N, focusedIndex: N } }
Scene_CGGallery._savedState = {};

/**
 * 外部連携API - ギャラリーを開く
 * 独自タイトル画面など外部から直接ギャラリーを起動する際に使用する。
 * 例: Scene_CGGallery.open("cg01")
 * @param {string} folder フォルダ名
 */
Scene_CGGallery.open = function(folder) {
    $gameTemp._cgGalleryFolder = String(folder || "cg01");
    SceneManager.push(Scene_CGGallery);
};

// IIFEスコープ外からも呼び出せるようにグローバルに公開
// スクリプトコマンドや独自タイトル画面からの外部連携API用
window.Scene_CGGallery = Scene_CGGallery;

//=============================================================================
// Scene_CGSlotNotice - スロットモード案内シーン
//=============================================================================
// slot系モードでタイトル画面からギャラリーを起動しようとした際に表示する案内画面。
// 黒背景＋テキスト＋戻るボタンのシンプル構成。
// 「明確にギャラリーが起動できないことを伝える」ための意図的なデザイン。

class Scene_CGSlotNotice extends Scene_Base {
    create() {
        super.create();
        this._createBackground();
        this._createNoticeText();
        this._createBackButton();
    }

    _createBackground() {
        // 黒背景（意図的にギャラリー背景を使わない）
        const bg = new Sprite(new Bitmap(Graphics.width, Graphics.height));
        bg.bitmap.fillRect(0, 0, Graphics.width, Graphics.height, "#000000");
        this.addChild(bg);
        this._bgSprite = bg;
    }

    _createNoticeText() {
        const lines = STRINGS.slotNotice;

        const lineH = 36;
        const totalH = lines.length * lineH;
        const startY = Math.floor((Graphics.height - totalH) / 2) - 40;

        const bitmap = new Bitmap(Graphics.width, Graphics.height);
        bitmap.textColor = "#ffffff";
        bitmap.outlineColor = "#000000";
        bitmap.outlineWidth = 4;
        bitmap.fontSize = 22;

        lines.forEach((line, i) => {
            if (line === "") return;
            bitmap.drawText(line, 0, startY + i * lineH, Graphics.width, lineH, "center");
        });

        const sprite = new Sprite(bitmap);
        this.addChild(sprite);
        this._textSprite = sprite;
    }

    _createBackButton() {
        const W = 160;
        const H = 48;
        const x = Math.floor((Graphics.width - W) / 2);
        const y = Graphics.height - 100;

        const button = UIBuilder.createButton(STRINGS.btnBack, W, H, () => this._onBack());
        button.x = x;
        button.y = y;
        this.addChild(button);
        this._backButton = button;
    }

    _onBack() {
        SoundManager.playCancel();
        this.cleanInput();
        SceneManager.pop();
    }

    update() {
        super.update();
        // キーボード・ゲームパッドのキャンセル操作でも戻れる
        if (Input.isTriggered("cancel") || Input.isTriggered("escape")) {
            this._onBack();
            return;
        }
        // タッチ・クリック判定
        if (TouchInput.isTriggered()) {
            const x = TouchInput.x;
            const y = TouchInput.y;
            if (UIBuilder.hitTest(x, y, this._backButton)) {
                this._backButton._onClick?.();
            }
        }
    }

    terminate() {
        // new Bitmap()で生成したものを破棄
        if (this._bgSprite?.bitmap) {
            this._bgSprite.bitmap.destroy();
        }
        if (this._textSprite?.bitmap) {
            this._textSprite.bitmap.destroy();
        }
        // UIBuilder.createButtonのbitmapはnew Bitmap()なので破棄してよい
        if (this._backButton?.bitmap) {
            this._backButton.bitmap.destroy();
        }
        super.terminate();
    }
}

//=============================================================================
// Scene_Title - タイトル画面拡張
//=============================================================================

// タイトル画面のコマンドウィンドウ拡張
const _Window_TitleCommand_makeCommandList = Window_TitleCommand.prototype.makeCommandList;
Window_TitleCommand.prototype.makeCommandList = function() {
    _Window_TitleCommand_makeCommandList.call(this);
    
    // NW.js環境かつ設定が有効な場合のみ追加
    if (CONFIG.showToolInTitle && Utils.isNwjs()) {
        this.addCommand(CONFIG.toolCommandName, "cgSettingTool");
    }
    
    // タイトルギャラリーコマンド追加
    // slot/global問わず表示する。slot系の場合は選択時にScene_CGSlotNoticeで案内する。
    TITLE_GALLERY_COMMANDS.forEach((cmd, i) => {
        this.addCommand(cmd.label, `cgGallery_${i}`);
    });
};

// タイトル画面のシーン拡張
const _Scene_Title_createCommandWindow = Scene_Title.prototype.createCommandWindow;
Scene_Title.prototype.createCommandWindow = function() {
    _Scene_Title_createCommandWindow.call(this);
    this._commandWindow.setHandler("cgSettingTool", this.commandCGSettingTool.bind(this));
    
    // タイトルギャラリーハンドラー登録
    TITLE_GALLERY_COMMANDS.forEach((cmd, i) => {
        this._commandWindow.setHandler(`cgGallery_${i}`, () => {
            this.commandCGGallery(cmd.folder);
        });
    });
};

Scene_Title.prototype.commandCGGallery = function(folder) {
    const isSlot = CONFIG.unlockMode === "slot" || CONFIG.unlockMode === "slot-manual";
    if (isSlot) {
        // slot系: 案内シーンへ遷移
        this._commandWindow.close();
        SceneManager.push(Scene_CGSlotNotice);
    } else {
        // global系: ギャラリーを開く
        this._commandWindow.close();
        Scene_CGGallery.open(folder);
    }
};

Scene_Title.prototype.commandCGSettingTool = function() {
    // ウィンドウは閉じずにツールを開く
    openCGSettingTool();
    // コマンドウィンドウを再度アクティブにする
    this._commandWindow.activate();
};

})();
