695 lines
28 KiB
JavaScript
695 lines
28 KiB
JavaScript
//=============================================================================
|
||
// MM_OptionExpansion.js
|
||
//=============================================================================
|
||
// Copyright (c) 2025- 牛乳もなか
|
||
//=============================================================================
|
||
// Version
|
||
// 2025/10/3 1.0.0 初期バージョン公開
|
||
//=============================================================================
|
||
/*:
|
||
* @target MZ
|
||
* @plugindesc v1.0.0 オプション項目スライダー化
|
||
* @url https://x.com/milkmonaka_1
|
||
* @author milkmonaka
|
||
* @help オプション画面を拡張し、BGM/BGS/ME/SEなどをスライダー化します。
|
||
*
|
||
* ■操作
|
||
* ・スライダー部分クリック:クリック位置に応じて音量を直接設定
|
||
* ・ノブをドラッグ:ドラッグ操作で音量を変更
|
||
* ・左右キー:ステップ幅で増減
|
||
* ・ミュートアイコンをクリック:ミュートON/OFF
|
||
*
|
||
* ■仕様・注意等
|
||
* ・画像未指定でも動作します(簡易描画)。
|
||
* ・画像比率は自由ですが、横長のベースと小さめのノブを想定しています。
|
||
* ・他のOptions系プラグインよりは下に配置したほうが良いでしょう。
|
||
* ・SimpleVoice.jsを導入している場合これもスライダー化されます。
|
||
* ・CustomizeConfigItem.jsを導入している場合、数値項目をスライダー化できます。
|
||
*
|
||
* ■オプション
|
||
* ・通常の「常時ダッシュ:ON」等の項目をミュートと同様のチェックボックスに変更できます。
|
||
*
|
||
* This software is released under the MIT License.
|
||
* http://opensource.org/licenses/mit-license.php
|
||
*
|
||
* @param statusWidth
|
||
* @text ステータス幅(px)
|
||
* @type number
|
||
* @min 100
|
||
* @desc スライダー・数値・ミュート枠までの総広さです。
|
||
* @default 200
|
||
*
|
||
* @param statusPadding
|
||
* @text 余白(px)
|
||
* @type number
|
||
* @min 0
|
||
* @desc スライダー・数値・ミュート枠の表示間隔です。
|
||
* @default 12
|
||
*
|
||
* @param valueWidth
|
||
* @text 数値の横幅
|
||
* @type number
|
||
* @min 10
|
||
* @desc 音量などの数値の横幅です。あまり変える必要はないと思いますが文字サイズ大きい時などに調整。
|
||
* @default 60
|
||
*
|
||
* @param checkBoxSize
|
||
* @text チェックボックスサイズ
|
||
* @type number
|
||
* @min 10
|
||
* @desc チェックボックスサイズです。縦横比は1:1です。あまり大きくしても項目の高さ以上は意味がないです。
|
||
* @default 32
|
||
*
|
||
* @param step
|
||
* @text 変更ステップ(%)
|
||
* @type number
|
||
* @min 1
|
||
* @max 50
|
||
* @desc 一操作毎に変わる量です。カスタム項目の場合はCustomizeConfigItem.jsでの変化値を使います。
|
||
* @default 5
|
||
*
|
||
* @param sliderImage
|
||
* @text スライダー画像
|
||
* @type file
|
||
* @dir img/system
|
||
* @desc スライダーのベース画像(横棒)を指定、未指定なら矩形描画。
|
||
* @default
|
||
*
|
||
* @param sliderHitPaddingY
|
||
* @text スライダー判定の上下余白(px)
|
||
* @type number
|
||
* @min 0
|
||
* @default 6
|
||
* @desc 横棒の画像の高さ次第ですが少し広めにした方がクリックしやすいと思います。
|
||
*
|
||
* @param knobImage
|
||
* @text ノブ画像
|
||
* @type file
|
||
* @dir img/system
|
||
* @desc スライダーのつまみ画像を指定、未指定なら菱形で描画。
|
||
* @default
|
||
*
|
||
* @param checkOnImage
|
||
* @text チェック画像
|
||
* @type file
|
||
* @dir img/system
|
||
* @default
|
||
*
|
||
* @param checkBaseImage
|
||
* @text チェック枠画像
|
||
* @type file
|
||
* @dir img/system
|
||
* @default
|
||
*
|
||
* @param unifyAllRows
|
||
* @text 全行でステータス幅を確保
|
||
* @type boolean
|
||
* @on true
|
||
* @off false
|
||
* @default false
|
||
* @desc trueにするとスライダーが無い項目も同じ幅を確保します。
|
||
*
|
||
* @param customSliderNames
|
||
* @text スライダー化する項目名
|
||
* @type string[]
|
||
* @default []
|
||
* @desc CustomizeConfigItem.js の「数値項目」のうち、ここに列挙した項目名称をスライダー表示にします。
|
||
*
|
||
* @param checkboxSymbols
|
||
* @text チェックボックス化するシンボル
|
||
* @type string[]
|
||
* @default ["alwaysDash"]
|
||
* @desc シンボルを入力します(例: alwaysDash, commandRemember, touchUI)。自分で追加した項目の場合そのシンボルを書く必要があります。
|
||
*/
|
||
(() => {
|
||
"use strict";
|
||
|
||
const PLUGIN_NAME = decodeURIComponent(document.currentScript.src).match(/^.*\/js\/plugins\/(.+)\.js$/)[1];
|
||
const params = PluginManager.parameters(PLUGIN_NAME);
|
||
const STATUS_WIDTH = Number(params.statusWidth || 200);
|
||
const AREA_PAD = Number(params.statusPadding || 12);
|
||
const VAL_WIDTH = Number(params.valueWidth || 60);
|
||
const BOX_SIZE = Number(params.checkBoxSize || 32);
|
||
const STEP = Number(params.step || 5);
|
||
const SLIDER_IMG = String(params.sliderImage || "");
|
||
const HIT_PAD_Y = Number(params.sliderHitPaddingY || 6);
|
||
const KNOB_IMG = String(params.knobImage || "");
|
||
const CHECK_BASE_IMG = String(params.checkBaseImage || "");
|
||
const CHECK_ON_IMG = String(params.checkOnImage || "");
|
||
const UNIFY_ALL = params.unifyAllRows === "true";
|
||
const CUSTOM_SLIDER_NAMES = (() => {
|
||
const raw = params.customSliderNames;
|
||
if (raw == null || raw === "") return [];
|
||
try {
|
||
// プラグインマネージャ経由なら JSON 文字列
|
||
const parsed = JSON.parse(raw);
|
||
if (Array.isArray(parsed)) {
|
||
return parsed.map(e => String(e).trim()).filter(s => s.length > 0);
|
||
}
|
||
} catch (_) {
|
||
// JSONでない場合はカンマ区切りとして解釈
|
||
if (typeof raw === "string") {
|
||
return raw.split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||
}
|
||
}
|
||
return [];
|
||
})();
|
||
|
||
|
||
// 画像ロード
|
||
const sliderBitmap = SLIDER_IMG ? ImageManager.loadSystem(SLIDER_IMG) : null;
|
||
const knobBitmap = KNOB_IMG ? ImageManager.loadSystem(KNOB_IMG) : null;
|
||
const checkBaseBitmap = CHECK_BASE_IMG ? ImageManager.loadSystem(CHECK_BASE_IMG) : null;
|
||
const checkOnBitmap = CHECK_ON_IMG ? ImageManager.loadSystem(CHECK_ON_IMG) : null;
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// ConfigManager 拡張:ミュート & VOICE
|
||
//
|
||
const _ConfigManager_makeData = ConfigManager.makeData;
|
||
ConfigManager.makeData = function() {
|
||
const config = _ConfigManager_makeData.call(this);
|
||
config.bgmMute = this.bgmMute ?? false;
|
||
config.bgsMute = this.bgsMute ?? false;
|
||
config.meMute = this.meMute ?? false;
|
||
config.seMute = this.seMute ?? false;
|
||
config.voiceMute = this.voiceMute ?? false;
|
||
return config;
|
||
};
|
||
|
||
const _ConfigManager_applyData = ConfigManager.applyData;
|
||
ConfigManager.applyData = function(config) {
|
||
_ConfigManager_applyData.call(this, config);
|
||
this.bgmMute = this.readFlag(config, "bgmMute");
|
||
this.bgsMute = this.readFlag(config, "bgsMute");
|
||
this.meMute = this.readFlag(config, "meMute");
|
||
this.seMute = this.readFlag(config, "seMute");
|
||
this.voiceMute = this.readFlag(config, "voiceMute");
|
||
};
|
||
|
||
ConfigManager.readVolume = function(config, name) {
|
||
const value = Number(config[name]);
|
||
return isNaN(value) ? 100 : value.clamp(0, 100);
|
||
};
|
||
|
||
// AudioManagerへ反映(既存のルート音量計算にミュートを掛ける)
|
||
const _AudioManager_updateBgmParameters = AudioManager.updateBgmParameters;
|
||
AudioManager.updateBgmParameters = function(currentBgm) {
|
||
_AudioManager_updateBgmParameters.call(this, currentBgm);
|
||
if (this._bgmBuffer) this._bgmBuffer.volume = (ConfigManager.bgmMute ? 0 : this._bgmBuffer.volume);
|
||
};
|
||
const _AudioManager_updateBgsParameters = AudioManager.updateBgsParameters;
|
||
AudioManager.updateBgsParameters = function(currentBgs) {
|
||
_AudioManager_updateBgsParameters.call(this, currentBgs);
|
||
if (this._bgsBuffer) this._bgsBuffer.volume = (ConfigManager.bgsMute ? 0 : this._bgsBuffer.volume);
|
||
};
|
||
const _AudioManager_updateMeParameters = AudioManager.updateMeParameters;
|
||
AudioManager.updateMeParameters = function(currentMe) {
|
||
_AudioManager_updateMeParameters.call(this, currentMe);
|
||
if (this._meBuffer) this._meBuffer.volume = (ConfigManager.meMute ? 0 : this._meBuffer.volume);
|
||
};
|
||
const _AudioManager_updateSeParameters = AudioManager.updateSeParameters;
|
||
AudioManager.updateSeParameters = function(buffer, se) {
|
||
_AudioManager_updateSeParameters.call(this, buffer, se);
|
||
if (buffer) buffer.volume = (ConfigManager.seMute ? 0 : buffer.volume);
|
||
};
|
||
|
||
const baseUpdateVoice = AudioManager.updateVoiceParameters
|
||
? AudioManager.updateVoiceParameters.bind(AudioManager)
|
||
// SimpleVoice より先に読み込まれた場合の保険
|
||
: function(buffer, voice) { this.updateBufferParameters(buffer, this._voiceVolume, voice); };
|
||
|
||
AudioManager.updateVoiceParameters = function(buffer, voice) {
|
||
baseUpdateVoice(buffer, voice);
|
||
if (buffer) buffer.volume = (ConfigManager.voiceMute ? 0 : buffer.volume);
|
||
};
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// Window_Options 拡張:描画と入力
|
||
//
|
||
const _Window_Options_statusWidth = Window_Options.prototype.statusWidth;
|
||
Window_Options.prototype.statusWidth = function(isCustom) {
|
||
if (UNIFY_ALL || isCustom) return STATUS_WIDTH;
|
||
return this._statusWidthOverride ?? _Window_Options_statusWidth.call(this);
|
||
};
|
||
|
||
// ミュートを管理するキーの対応表
|
||
const MUTE_KEY_MAP = {
|
||
bgmVolume: "bgmMute",
|
||
bgsVolume: "bgsMute",
|
||
meVolume: "meMute",
|
||
seVolume: "seMute",
|
||
voiceVolume: "voiceMute"
|
||
};
|
||
|
||
// 音量行のカスタム描画
|
||
const _Window_Options_drawItem = Window_Options.prototype.drawItem;
|
||
Window_Options.prototype.drawItem = function(index) {
|
||
const symbol = this.commandSymbol(index);
|
||
const prevOverride = this._statusWidthOverride;
|
||
|
||
if (!UNIFY_ALL) {
|
||
// 音量行のみ右側エリアぶんを確保
|
||
this._statusWidthOverride = this.isVolumeSymbol(symbol) ? STATUS_WIDTH : null;
|
||
}
|
||
|
||
if (this.isVolumeSymbol(symbol) || this._isCustomSliderSymbol(symbol)) {
|
||
this.drawVolumeItem(index);
|
||
} else {
|
||
_Window_Options_drawItem.apply(this, arguments);
|
||
}
|
||
|
||
this._statusWidthOverride = prevOverride ?? null;
|
||
};
|
||
|
||
Window_Options.prototype.drawVolumeItem = function(index) {
|
||
const rectDraw = this.itemRectWithPadding(index);
|
||
const rectLine = this.itemLineRect(index);
|
||
|
||
const symbol = this.commandSymbol(index);
|
||
const title = this.commandName(index);
|
||
|
||
const isVolume = this.isVolumeSymbol(symbol);
|
||
const isCustom = !isVolume && this._isCustomSliderSymbol && this._isCustomSliderSymbol(symbol);
|
||
|
||
// カスタム用の仕様(min/max/step)を取得
|
||
const spec = isCustom ? this._customSliderSpec(symbol) : null;
|
||
|
||
// 値の取得(volume は 0-100 前提、custom は任意の範囲)
|
||
const valueRaw = this.getConfigValue(symbol);
|
||
|
||
// drawSlider に渡すのは 0-100(%)で統一
|
||
const valuePct = isCustom
|
||
? Math.round((valueRaw - spec.min) / Math.max(1, spec.max - spec.min) * 100)
|
||
: valueRaw;
|
||
|
||
// ミュート関連(カスタムは不要)
|
||
const muteKey = isVolume ? MUTE_KEY_MAP[symbol] : null;
|
||
const isEnabled = isVolume ? !ConfigManager[muteKey] : false;
|
||
|
||
const statusW = this.statusWidth(isCustom);
|
||
const titleW = rectDraw.width - statusW;
|
||
|
||
// 左:タイトル
|
||
this.resetTextColor();
|
||
this.changePaintOpacity(this.isCommandEnabled(index));
|
||
this.drawText(title, rectDraw.x, rectDraw.y, titleW, "left");
|
||
|
||
// 右エリアのレイアウト
|
||
const areaX = rectDraw.x + titleW;
|
||
const areaY = rectDraw.y + Math.floor((rectDraw.height - 24) / 2);
|
||
|
||
// カスタムのときはミュートUIを置かない
|
||
const checkSize = BOX_SIZE;
|
||
const valueW = VAL_WIDTH;
|
||
const padding = AREA_PAD;
|
||
const sliderW = statusW - (checkSize + padding) - (valueW + padding) - padding;
|
||
|
||
// スライダー見た目とヒット矩形(左右は同じにしてズレ防止)
|
||
const visualRect = new Rectangle(areaX + padding, areaY + 6, sliderW, 12);
|
||
const desiredHitHeight = visualRect.height + HIT_PAD_Y * 2;
|
||
const hitHeight = Math.min(rectDraw.height, desiredHitHeight);
|
||
const hitY = Math.round(visualRect.y + visualRect.height / 2 - hitHeight / 2);
|
||
const hitRect = new Rectangle(visualRect.x, hitY, visualRect.width, hitHeight);
|
||
|
||
// スライダー描画(0-100%)
|
||
this.drawSlider(visualRect, valuePct);
|
||
|
||
// 右端の数値
|
||
const text = isCustom ? String(valueRaw) : (String(valueRaw).padStart(2, " ") + "%");
|
||
if (isCustom) {
|
||
this.drawText(text, areaX + sliderW + (padding * 2), rectDraw.y, valueW + checkSize + padding, "center");
|
||
} else {
|
||
this.drawText(text, areaX + sliderW + (padding * 2), rectDraw.y, valueW, "center");
|
||
}
|
||
|
||
// ミュート(音量のみ)
|
||
if (isVolume) {
|
||
const muteX = areaX + sliderW + valueW + (padding * 3);
|
||
this.drawMuteIcon(
|
||
new Rectangle(muteX, rectDraw.y + Math.floor((rectDraw.height - checkSize) / 2), checkSize, checkSize),
|
||
isEnabled
|
||
);
|
||
// クリック判定用領域を保存(音量)
|
||
this._volumeHitAreas = this._volumeHitAreas || {};
|
||
this._volumeHitAreas[index] = {
|
||
slider: hitRect,
|
||
mute: new Rectangle(muteX, rectDraw.y, checkSize, rectDraw.height)
|
||
};
|
||
} else if (isCustom) {
|
||
// クリック判定用領域を保存(カスタム)
|
||
this._customSliderAreas = this._customSliderAreas || {};
|
||
this._customSliderAreas[index] = { slider: hitRect, spec, symbol };
|
||
}
|
||
};
|
||
|
||
Window_Options.prototype.drawSlider = function(rect, value) {
|
||
const rate = value / 100;
|
||
// ベース
|
||
if (sliderBitmap && sliderBitmap.isReady()) {
|
||
this.contents.blt(sliderBitmap, 0, 0, sliderBitmap.width, sliderBitmap.height,
|
||
rect.x, rect.y, rect.width, rect.height);
|
||
} else {
|
||
this.contents.paintOpacity = 128;
|
||
this.contents.fillRect(rect.x, rect.y + rect.height/2 - 2, rect.width, 4, ColorManager.gaugeBackColor());
|
||
this.contents.paintOpacity = 255;
|
||
}
|
||
// ノブ位置
|
||
const knobX = Math.round(rect.x + rect.width * rate);
|
||
if (knobBitmap && knobBitmap.isReady()) {
|
||
const kw = knobBitmap.width, kh = knobBitmap.height;
|
||
this.contents.blt(knobBitmap, 0, 0, kw, kh, knobX - kw/2, rect.y + rect.height/2 - kh/2);
|
||
} else {
|
||
const kSize = 16;
|
||
this.drawDiamond(knobX, rect.y + rect.height/2, kSize, ColorManager.normalColor());
|
||
}
|
||
};
|
||
|
||
Window_Options.prototype.drawDiamond = function(cx, cy, size, color) {
|
||
const ctx = this.contents.context;
|
||
ctx.save();
|
||
ctx.fillStyle = color;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy - size/2);
|
||
ctx.lineTo(cx + size/2, cy);
|
||
ctx.lineTo(cx, cy + size/2);
|
||
ctx.lineTo(cx - size/2, cy);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.restore();
|
||
this.contents._baseTexture.update();
|
||
};
|
||
|
||
Window_Options.prototype.drawMuteIcon = function(rect, isMute) {
|
||
if (checkBaseBitmap && checkBaseBitmap.isReady()) {
|
||
this.contents.blt(checkBaseBitmap, 0, 0, checkBaseBitmap.width, checkBaseBitmap.height, rect.x, rect.y, rect.width, rect.height);
|
||
} else {
|
||
// □にチェック
|
||
this.contents.paintOpacity = 255;
|
||
this.contents.strokeRect(rect.x+2, rect.y+4, rect.width-4, rect.height-8, ColorManager.textColor(0));
|
||
}
|
||
if (isMute) {
|
||
if (checkOnBitmap && checkOnBitmap.isReady()) {
|
||
this.contents.blt(checkOnBitmap, 0, 0, checkOnBitmap.width, checkOnBitmap.height, rect.x, rect.y, rect.width, rect.height);
|
||
} else {
|
||
this.contents.fillRect(rect.x+6, rect.y+8, rect.width-12, rect.height-16, ColorManager.textColor(0));
|
||
}
|
||
}
|
||
};
|
||
|
||
// ステータス文字列は使わないので空文字返し
|
||
const _Window_Options_statusText = Window_Options.prototype.statusText;
|
||
Window_Options.prototype.statusText = function(index) {
|
||
const symbol = this.commandSymbol(index);
|
||
if (this.isVolumeSymbol(symbol) || (this._isCustomSliderSymbol && this._isCustomSliderSymbol(symbol))) {
|
||
return "";
|
||
}
|
||
return _Window_Options_statusText.apply(this, arguments);
|
||
};
|
||
|
||
// クリック処理(スライダー直指定&ミュート切替)
|
||
const _Window_Options_processTouch = Window_Options.prototype.processTouch;
|
||
Window_Options.prototype.processTouch = function() {
|
||
if (this.isOpenAndActive()) {
|
||
// ドラッグ継続
|
||
if (this._draggingSlider && TouchInput.isPressed()) {
|
||
const area = this._draggingSlider.area;
|
||
const x = TouchInput.x - this.x - this.padding;
|
||
const ptX = Math.min(Math.max(x, area.x), area.x + area.width);
|
||
const ratio = (ptX - area.x) / area.width;
|
||
const symbol = this._draggingSlider.symbol;
|
||
if (this._draggingSlider.spec) {
|
||
const s = this._draggingSlider.spec;
|
||
const raw = s.min + (Math.min(Math.max(ratio, 0), 1)) * (s.max - s.min);
|
||
const snapped = Math.round((raw - s.min) / s.step) * s.step + s.min;
|
||
this.changeValue(symbol, Number(snapped).clamp(s.min, s.max));
|
||
} else {
|
||
const raw = Math.round(Math.min(Math.max(ratio, 0), 1) * 100);
|
||
const step = (typeof STEP !== "undefined") ? STEP : 5;
|
||
const snapped = Math.round(raw / step) * step;
|
||
this.changeValue(symbol, Math.min(Math.max(snapped, 0), 100));
|
||
}
|
||
return;
|
||
} else if (!TouchInput.isPressed()) {
|
||
this._draggingSlider = null;
|
||
}
|
||
|
||
const hitIndex = this.hitTestTouchInside();
|
||
if (hitIndex >= 0) {
|
||
const symbol = this.commandSymbol(hitIndex);
|
||
|
||
// チェックボックス化した通常項目
|
||
if (this._isCheckboxSymbol && this._isCheckboxSymbol(symbol)) {
|
||
const area = this._checkboxAreas?.[hitIndex];
|
||
if (area) {
|
||
const x = TouchInput.x - this.x - this.padding;
|
||
const y = TouchInput.y - this.y - this.padding;
|
||
const pt = new Point(x, y);
|
||
if (this.pointInRect(pt, area) && TouchInput.isTriggered()) {
|
||
this.changeValue(symbol, !this.getConfigValue(symbol));
|
||
SoundManager.playCursor();
|
||
this.redrawItem(hitIndex);
|
||
this._symbol = symbol;
|
||
return; // 既定処理へ渡さない
|
||
}
|
||
}
|
||
}
|
||
|
||
// 音量行
|
||
if (this.isVolumeSymbol(symbol)) {
|
||
const area = this._volumeHitAreas?.[hitIndex];
|
||
if (area) {
|
||
const x = TouchInput.x - this.x - this.padding;
|
||
const y = TouchInput.y - this.y - this.padding;
|
||
const pt = new Point(x, y);
|
||
|
||
// ミュートクリック
|
||
if (this.pointInRect(pt, area.mute) && TouchInput.isTriggered()) {
|
||
const muteKey = (typeof MUTE_KEY_MAP !== "undefined") ? MUTE_KEY_MAP[symbol] : null;
|
||
if (muteKey) {
|
||
ConfigManager[muteKey] = !ConfigManager[muteKey];
|
||
SoundManager.playCursor();
|
||
try {
|
||
if (AudioManager._bgmBuffer) AudioManager.updateBgmParameters(AudioManager._currentBgm);
|
||
if (AudioManager._bgsBuffer) AudioManager.updateBgsParameters(AudioManager._currentBgs);
|
||
if (AudioManager._meBuffer) AudioManager.updateMeParameters(AudioManager._currentMe);
|
||
if (AudioManager._voiceBuffers?.length) {
|
||
AudioManager._voiceBuffers.forEach(b => AudioManager.updateVoiceParameters(b));
|
||
}
|
||
} catch (e) {}
|
||
this.redrawItem(hitIndex);
|
||
}
|
||
this._symbol = symbol;
|
||
return; // 既定処理へ渡さない
|
||
}
|
||
|
||
// スライダークリック/ドラッグ開始
|
||
if (this.pointInRect(pt, area.slider) && (TouchInput.isTriggered() || TouchInput.isPressed())) {
|
||
const ratio = (pt.x - area.slider.x) / area.slider.width;
|
||
const raw = Math.round(Math.min(Math.max(ratio, 0), 1) * 100);
|
||
const step = (typeof STEP !== "undefined") ? STEP : 5;
|
||
const snapped = Math.round(raw / step) * step;
|
||
this.changeValue(symbol, Math.min(Math.max(snapped, 0), 100));
|
||
this._draggingSlider = { symbol, area: area.slider };
|
||
this._symbol = symbol;
|
||
return; // 既定処理へ渡さない
|
||
}
|
||
}
|
||
}
|
||
|
||
// カスタムスライダー(CustomizeConfigItem)
|
||
if (this._isCustomSliderSymbol && this._isCustomSliderSymbol(symbol)) {
|
||
const area = this._customSliderAreas?.[hitIndex];
|
||
if (area) {
|
||
const x = TouchInput.x - this.x - this.padding;
|
||
const y = TouchInput.y - this.y - this.padding;
|
||
const pt = new Point(x, y);
|
||
if (this.pointInRect(pt, area.slider) && (TouchInput.isTriggered() || TouchInput.isPressed())) {
|
||
const ratio = (pt.x - area.slider.x) / area.slider.width;
|
||
const raw = area.spec.min + (Math.min(Math.max(ratio, 0), 1)) * (area.spec.max - area.spec.min);
|
||
const snapped = Math.round((raw - area.spec.min) / area.spec.step) * area.spec.step + area.spec.min;
|
||
this.changeValue(symbol, Number(snapped).clamp(area.spec.min, area.spec.max));
|
||
this._draggingSlider = { symbol, area: area.slider, spec: area.spec };
|
||
this._symbol = symbol;
|
||
return; // 既定処理へ渡さない
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_Window_Options_processTouch.apply(this, arguments);
|
||
};
|
||
|
||
const _Window_Options_onTouchOk = Window_Selectable.prototype.onTouchOk;
|
||
Window_Selectable.prototype.onTouchOk = function() {
|
||
if (this._symbol) {
|
||
this._symbol = null;
|
||
return;
|
||
} else {
|
||
_Window_Options_onTouchOk.apply(this, arguments)
|
||
}
|
||
};
|
||
|
||
Window_Options.prototype.hitTestTouchInside = function() {
|
||
// canvasToLocalX/Y が無い環境でも動くフォールバック
|
||
const localX = this.canvasToLocalX
|
||
? this.canvasToLocalX(TouchInput.x)
|
||
: TouchInput.x - this.x - this.padding;
|
||
const localY = this.canvasToLocalY
|
||
? this.canvasToLocalY(TouchInput.y)
|
||
: TouchInput.y - this.y - this.padding;
|
||
return this.hitTest(localX, localY);
|
||
};
|
||
|
||
Window_Options.prototype.pointInRect = function(p, r) {
|
||
return p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height;
|
||
};
|
||
|
||
// ステップ幅を差し替え
|
||
Window_Options.prototype.volumeOffset = function() {
|
||
return STEP;
|
||
};
|
||
|
||
// 音量シンボル判定(VOICE含む)
|
||
const _Window_Options_isVolumeSymbol = Window_Options.prototype.isVolumeSymbol;
|
||
Window_Options.prototype.isVolumeSymbol = function(symbol) {
|
||
if (symbol === "voiceVolume") return true;
|
||
return _Window_Options_isVolumeSymbol.apply(this, arguments);
|
||
};
|
||
|
||
const _Window_Options_isOkEnabled = Window_Options.prototype.isOkEnabled;
|
||
Window_Options.prototype.isOkEnabled = function() {
|
||
const symbol = this.commandSymbol(this.index());
|
||
if (this._isCustomSliderSymbol && this._isCustomSliderSymbol(symbol)) return false; // カスタムだけ無効
|
||
return _Window_Options_isOkEnabled.apply(this, arguments);
|
||
};
|
||
|
||
// OK
|
||
const _Window_Options_processOk = Window_Options.prototype.processOk;
|
||
Window_Options.prototype.processOk = function() {
|
||
const symbol = this.commandSymbol(this.index());
|
||
if (this.isVolumeSymbol(symbol)) {
|
||
const muteKey = (typeof MUTE_KEY_MAP !== "undefined") ? MUTE_KEY_MAP[symbol] : null;
|
||
if (muteKey) {
|
||
ConfigManager[muteKey] = !ConfigManager[muteKey];
|
||
SoundManager.playCursor();
|
||
try {
|
||
if (AudioManager._bgmBuffer) AudioManager.updateBgmParameters(AudioManager._currentBgm);
|
||
if (AudioManager._bgsBuffer) AudioManager.updateBgsParameters(AudioManager._currentBgs);
|
||
if (AudioManager._meBuffer) AudioManager.updateMeParameters(AudioManager._currentMe);
|
||
if (AudioManager._voiceBuffers?.length) {
|
||
AudioManager._voiceBuffers.forEach(b => AudioManager.updateVoiceParameters(b));
|
||
}
|
||
} catch (e) { /* no-op */ }
|
||
this.redrawItem(this.index());
|
||
}
|
||
return; // 既定のOKは流さない
|
||
}
|
||
_Window_Options_processOk.apply(this, arguments);
|
||
};
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// ON/OFF項目をチェックボックス化バッチ
|
||
//
|
||
// チェックボックス化するシンボルを列挙
|
||
const CHECKBOX_SYMBOLS = (() => {
|
||
const raw = params.checkboxSymbols;
|
||
if (raw == null || raw === "") return new Set();
|
||
try {
|
||
const arr = JSON.parse(raw);
|
||
return new Set(Array.isArray(arr) ? arr.map(s => String(s).trim()) : []);
|
||
} catch (_e) {
|
||
if (typeof raw === "string") {
|
||
return new Set(raw.split(",").map(s => s.trim()).filter(Boolean));
|
||
}
|
||
return new Set();
|
||
}
|
||
})();
|
||
|
||
// 内部ヘルパ
|
||
Window_Options.prototype._isCheckboxSymbol = function(symbol) {
|
||
if (!symbol) return false;
|
||
if (!CHECKBOX_SYMBOLS.has(symbol)) return false;
|
||
const v = this.getConfigValue(symbol);
|
||
return typeof v === "boolean";
|
||
};
|
||
|
||
// 右側のステータス文字列を消す(重なり防止)
|
||
const _statusText = Window_Options.prototype.statusText;
|
||
Window_Options.prototype.statusText = function(index) {
|
||
const symbol = this.commandSymbol(index);
|
||
if (this._isCheckboxSymbol(symbol)) return "";
|
||
return _statusText.apply(this, arguments);
|
||
};
|
||
|
||
// チェックボックス項目なら自前描画へ
|
||
const _drawItem = Window_Options.prototype.drawItem;
|
||
Window_Options.prototype.drawItem = function(index) {
|
||
const symbol = this.commandSymbol(index);
|
||
|
||
const prev = this._statusWidthOverride;
|
||
if (this._isCheckboxSymbol(symbol)) {
|
||
const statusW = this.statusWidth(false); // 既定幅を使う
|
||
this._statusWidthOverride = statusW;
|
||
this.drawCheckboxItem(index);
|
||
this._statusWidthOverride = prev ?? null;
|
||
return;
|
||
}
|
||
|
||
_drawItem.apply(this, arguments);
|
||
this._statusWidthOverride = prev ?? null;
|
||
};
|
||
|
||
// チェックボックス描画(ミュート流用)
|
||
Window_Options.prototype.drawCheckboxItem = function(index) {
|
||
const rect = this.itemRectWithPadding(index);
|
||
const title = this.commandName(index);
|
||
const symbol = this.commandSymbol(index);
|
||
const checked = !!this.getConfigValue(symbol);
|
||
|
||
// 左:タイトル
|
||
const statusW = this.statusWidth(false);
|
||
const titleW = rect.width - statusW;
|
||
this.resetTextColor();
|
||
this.changePaintOpacity(this.isCommandEnabled(index));
|
||
this.drawText(title, rect.x, rect.y, titleW, "left");
|
||
|
||
// 右:チェックボックス
|
||
const areaX = rect.x + titleW;
|
||
const checkSize = BOX_SIZE;
|
||
const chkX = areaX + statusW - checkSize;
|
||
const chkY = rect.y + Math.floor((rect.height - checkSize) / 2);
|
||
|
||
this.drawMuteIcon(new Rectangle(chkX, chkY, checkSize, checkSize), checked);
|
||
|
||
// クリック判定用領域を保存
|
||
this._checkboxAreas = this._checkboxAreas || {};
|
||
this._checkboxAreas[index] = new Rectangle(chkX, rect.y, checkSize, rect.height);
|
||
};
|
||
|
||
//-----------------------------------------------------------------------------
|
||
// CustomizeConfigItem対応追加バッチ
|
||
//
|
||
Window_Options.prototype._isNumberCustomSymbol = function(symbol) {
|
||
const cp = ConfigManager.getCustomParams?.();
|
||
if (!cp || !cp[symbol]) return false;
|
||
// CustomizeConfigItem 側の命名規則
|
||
return symbol.includes(ConfigManager._symbolNumber);
|
||
};
|
||
|
||
// 名称でスライダー化する対象かどうか
|
||
Window_Options.prototype._isCustomSliderSymbol = function(symbol) {
|
||
if (!this._isNumberCustomSymbol(symbol)) return false;
|
||
const cp = ConfigManager.getCustomParams();
|
||
const name = cp[symbol]?.name;
|
||
return !!name && CUSTOM_SLIDER_NAMES.includes(name);
|
||
};
|
||
|
||
// そのシンボルのスライダー仕様(最小/最大/ステップ)を取得
|
||
Window_Options.prototype._customSliderSpec = function(symbol) {
|
||
const cp = ConfigManager.getCustomParams();
|
||
const item = cp[symbol];
|
||
// NumberMin/Max/Step は CustomizeConfigItem が data.min/max/offset に入れる
|
||
return item ? { min: item.min, max: item.max, step: item.offset } : null;
|
||
};
|
||
|
||
})();
|