danoniplus 50.3.1 → 50.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3426 @@
1
+ /**
2
+ * Dancing☆Onigiri (CW Edition)
3
+ * Settings, Display, Ex-Settings画面
4
+ * - ページ: option, difSelector, scoreDetail, settingsDisplay, displayPreview, exSetting
5
+ *
6
+ * Source by tickle
7
+ * Created : 2026/09/13
8
+ * Revised :
9
+ *
10
+ * https://github.com/cwtickle/danoniplus
11
+ */
12
+
13
+ /*-----------------------------------------------------------*/
14
+ /* Scene : SETTINGS [lime] */
15
+ /*-----------------------------------------------------------*/
16
+
17
+ const commonSettingBtn = _labelName => {
18
+
19
+ const switchSave = evt => {
20
+ const from = boolToSwitch(g_stateObj.dataSaveFlg);
21
+ g_stateObj.dataSaveFlg = !g_stateObj.dataSaveFlg;
22
+ updateSettingSummary();
23
+
24
+ const to = boolToSwitch(g_stateObj.dataSaveFlg);
25
+ evt.target.classList.replace(g_cssObj[`button_${from}`], g_cssObj[`button_${to}`]);
26
+ };
27
+
28
+ multiAppend(divRoot,
29
+
30
+ // タイトル画面へ戻る
31
+ createCss2Button(`btnBack`, g_lblNameObj.b_back, () => true, {
32
+ ...g_lblPosObj.btnBack,
33
+ animationName: (g_initialFlg ? `` : `smallToNormalY`), resetFunc: () => titleInit(),
34
+ }, g_cssObj.button_Back),
35
+
36
+ // キーコンフィグ画面へ移動
37
+ createCss2Button(`btnKeyConfig`, g_lblNameObj.b_keyConfig, () => true, {
38
+ ...g_lblPosObj.btnKeyConfig,
39
+ animationName: (g_initialFlg ? `` : `smallToNormalY`), resetFunc: () => keyConfigInit(`Main`, true),
40
+ }, g_cssObj.button_Setting),
41
+
42
+ // プレイ開始
43
+ makePlayButton(() => loadMusic()),
44
+
45
+ // Display設定へ移動
46
+ createCss2Button(`btn${_labelName}`, `>`, () => true, {
47
+ ...g_lblPosObj.btnSwitchSetting,
48
+ title: g_msgObj[`to${_labelName}`], resetFunc: () => g_moveSettingWindow(),
49
+ cxtFunc: () => g_moveSettingWindow(true, -1),
50
+ }, g_cssObj.button_Mini),
51
+
52
+ // データセーブフラグの切替
53
+ createCss2Button(`btnSave`, g_lblNameObj.dataSave, evt => switchSave(evt), {
54
+ ...g_lblPosObj.btnSave,
55
+ cxtFunc: evt => switchSave(evt),
56
+ visibility: g_langStorage.safeMode === C_FLG_OFF ? C_DIS_INHERIT : `hidden`,
57
+ }, g_cssObj.button_Default, (g_stateObj.dataSaveFlg ? g_cssObj.button_ON : g_cssObj.button_OFF)),
58
+
59
+ // データ管理画面へ移動
60
+ createCss2Button(`btnReset`, g_lblNameObj.dataReset, () => {
61
+ dataMgtInit();
62
+ }, g_lblPosObj.btnReset, g_cssObj.button_Reset),
63
+
64
+ // 前提条件表示用画面へ移動(debug=trueの場合のみ)
65
+ createCss2Button(`btnPrecond`, g_lblNameObj.b_precond, () => true, {
66
+ ...g_lblPosObj.btnPrecond,
67
+ resetFunc: () => preconditionInit(),
68
+ }, g_cssObj.button_Setting),
69
+
70
+ // 設定内容サマリを表示
71
+ createCss2Button(`btnSettingSummary`, `>`, () => true, {
72
+ ...g_lblPosObj.btnSettingSummary,
73
+ resetFunc: () => {
74
+ g_stateObj.settingSummaryVisible = !g_stateObj.settingSummaryVisible;
75
+ visibleSettingSummary(g_stateObj.settingSummaryVisible);
76
+ },
77
+ }, g_cssObj.button_Mini),
78
+ );
79
+ makeSettingSummary();
80
+ };
81
+
82
+ const makeSettingSummary = () => {
83
+ const tmpDiv = createEmptySprite(divRoot, `settingSumSprite`, g_windowObj.settingSumSprite);
84
+ tmpDiv.style.background = g_headerObj.baseBrightFlg ? `#ffffffee` : `#000000cc`;
85
+
86
+ multiAppend(tmpDiv,
87
+ createDivCss2Label(`lblSummaryHeader`, g_lblNameObj.settingSummary, g_lblPosObj.lblSummaryHeader),
88
+ createDivCss2Label(`lblSummaryEnvironment`, ``, g_lblPosObj.lblSummaryEnvironment),
89
+ createDivCss2Label(`lblSummaryDifHeader`, g_lblNameObj.rt_Difficulty, g_lblPosObj.lblSummaryDifHeader),
90
+ createDivCss2Label(`lblSummaryDifInfo`, ``, g_lblPosObj.lblSummaryDifInfo),
91
+ createDivCss2Label(`lblSummaryPlaystyleHeader`, g_lblNameObj.rt_Style, g_lblPosObj.lblSummaryPlaystyleHeader),
92
+ createDivCss2Label(`lblSummaryPlaystyleInfo`, ``, g_lblPosObj.lblSummaryPlaystyleInfo),
93
+ createDivCss2Label(`lblSummaryDisplayHeader`, g_lblNameObj.rt_Display, g_lblPosObj.lblSummaryDisplayHeader),
94
+ createDivCss2Label(`lblSummaryDisplayInfo`, ``, g_lblPosObj.lblSummaryDisplayInfo),
95
+ createDivCss2Label(`lblSummaryDisplay2Info`, ``, g_lblPosObj.lblSummaryDisplay2Info),
96
+ );
97
+ tmpDiv.style.visibility = g_stateObj.settingSummaryVisible ? `visible` : `hidden`;
98
+ dragDiv(`settingSumSprite`, {
99
+ minX: g_btnX() + 25, maxX: g_btnX() + 25, minY: 10, maxY: g_sHeight - 10,
100
+ });
101
+ if (g_posObj.settingSumSprite?.y !== undefined) {
102
+ document.getElementById(`settingSumSprite`).style.top = wUnit(g_posObj.settingSumSprite.y);
103
+ }
104
+ };
105
+
106
+ const visibleSettingSummary = _visible => {
107
+ const summaryDiv = document.getElementById(`settingSumSprite`);
108
+ if (summaryDiv) {
109
+ summaryDiv.style.visibility = _visible ? `visible` : `hidden`;
110
+ updateSettingSummary();
111
+ }
112
+ };
113
+
114
+ const updateSettingSummary = () => {
115
+ if (document.getElementById(`settingSumSprite`) === null) return;
116
+ const orgShuffleFlg = getOrgShuffleFlg();
117
+ const settingData = getSelectedSettingList(orgShuffleFlg);
118
+ const estimatedHighscoreCondition = g_stateObj.dataSaveFlg && (g_stateObj.autoPlay !== C_FLG_ALL && g_headerObj.playbackRate === 1 && g_stateObj.fadein < 10 &&
119
+ (g_stateObj.shuffle === C_FLG_OFF || (g_stateObj.shuffle.endsWith(`Mirror`) && orgShuffleFlg)));
120
+
121
+ document.getElementById(`lblSummaryDifInfo`).innerHTML = settingData.difData + `${estimatedHighscoreCondition ? '' : ` | <span class="common_auto common_bold">No Records</span>`}`;
122
+ document.getElementById(`lblSummaryPlaystyleInfo`).textContent = settingData.playStyleData + `${g_stateObj.excessive === C_FLG_ON ? ' | Excessive' : ''}`;
123
+ document.getElementById(`lblSummaryDisplayInfo`).textContent = settingData.displayData;
124
+ document.getElementById(`lblSummaryDisplay2Info`).textContent = settingData.display2Data;
125
+ document.getElementById(`lblSummaryEnvironment`).textContent =
126
+ `(Adj: ${g_stateObj.adjustment} f, Volume: ${g_stateObj.volume}%, ` +
127
+ `ColorType: ${g_colorType}, KeyPattern: ${g_keyObj.currentPtn === -1 ? 'Self' : g_keyObj.currentPtn + 1})`;
128
+
129
+ safeExecuteCustomHooks(`g_customJsObj.settingSummary`, g_customJsObj.settingSummary);
130
+ };
131
+
132
+ /**
133
+ * PLAYボタンの作成
134
+ * @param {Function} _func
135
+ * @returns {HTMLDivElement}
136
+ */
137
+ const makePlayButton = _func => createCss2Button(`btnPlay`, g_lblNameObj.b_play, () => true, {
138
+ ...g_lblPosObj.btnPlay, animationName: (g_initialFlg ? `` : `smallToNormalY`), resetFunc: _func,
139
+ }, g_cssObj.button_Next);
140
+
141
+ /**
142
+ * 設定・オプション画面初期化
143
+ */
144
+ const optionInit = () => {
145
+
146
+ clearWindow();
147
+ pauseBGM();
148
+ const divRoot = document.getElementById(`divRoot`);
149
+ g_currentPage = `option`;
150
+ g_stateObj.filterKeys = ``;
151
+
152
+ // 楽曲データの表示
153
+ const text = getMusicInfoView();
154
+ divRoot.appendChild(createDivCss2Label(`lblMusicInfo`, text,
155
+ { ...g_lblPosObj.lblMusicInfo, siz: getFontSize2(text, g_btnWidth(3 / 4), { maxSiz: 12 }) }));
156
+
157
+ // タイトル文字描画
158
+ divRoot.appendChild(getTitleDivLabel(`lblTitle`, g_lblNameObj.settings, 0, 15, `settings_Title`));
159
+
160
+ // オプションボタン用の設置
161
+ createOptionWindow(divRoot);
162
+
163
+ // ユーザカスタムイベント(初期)
164
+ safeExecuteCustomHooks(`g_customJsObj.option`, g_customJsObj.option);
165
+
166
+ // ボタン描画
167
+ commonSettingBtn(`Display`);
168
+
169
+ // キー操作イベント(デフォルト)
170
+ setShortcutEvent(g_currentPage, () => true, { dfEvtFlg: true });
171
+ document.oncontextmenu = () => true;
172
+ g_initialFlg = true;
173
+
174
+ safeExecuteCustomHooks(`g_skinJsObj.option`, g_skinJsObj.option);
175
+ };
176
+
177
+ /**
178
+ * 設定画面に表示する楽曲・BPM情報の取得
179
+ * @returns {string}
180
+ */
181
+ const getMusicInfoView = () => {
182
+ const idx = g_headerObj.musicNos[g_stateObj.scoreId] || 0;
183
+ let text = `♪` + (g_headerObj.musicSelectUse ? `${unEscapeHtml(g_headerObj.musicTitles[idx])} / ` : ``) +
184
+ `BPM: ${g_headerObj.bpms[idx]}`;
185
+ if (!g_headerObj.musicSelectUse && g_headerObj.bpms[idx] === `----`) {
186
+ text = ``;
187
+ }
188
+ return text;
189
+ };
190
+
191
+ /**
192
+ * 設定画面用スプライトリストの作成
193
+ * @param {object} _settingList (設定名、縦位置、縦位置差分、幅差分、高さ差分)を設定別にリスト化
194
+ * @returns
195
+ */
196
+ const setSpriteList = _settingList => {
197
+ const optionWidth = (g_sWidth - 450) / 2;
198
+ const spriteList = [];
199
+ Object.keys(_settingList).forEach(setting =>
200
+ spriteList[setting] = createEmptySprite(optionsprite, `${setting}Sprite`, {
201
+ x: 25, y: _settingList[setting].heightPos * g_limitObj.setLblHeight + (_settingList[setting].y || 0) + 20,
202
+ w: optionWidth + (_settingList[setting].dw || 0), h: g_limitObj.setLblHeight + (_settingList[setting].dh || 0),
203
+ }));
204
+ return spriteList;
205
+ };
206
+
207
+ /**
208
+ * スライダー共通処理 (Fadein, Appearance)
209
+ * @param {HTMLInputElement} _slider
210
+ * @param {HTMLDivElement} _link
211
+ * @param {string} _type
212
+ * @returns {string}
213
+ */
214
+ const inputSlider = (_slider, _link, _type) => {
215
+ const value = parseInt(_slider.value);
216
+ _link.textContent = g_sliderView.get(_type)(value);
217
+ return value;
218
+ };
219
+
220
+ /**
221
+ * 譜面変更セレクターの削除
222
+ */
223
+ const resetDifWindow = () => {
224
+ if (document.getElementById(`difList`) !== null) {
225
+ deleteChildspriteAll(`difList`);
226
+ [`difList`, `difCover`, `btnDifU`, `btnDifD`].forEach(obj => document.getElementById(obj).remove());
227
+ g_currentPage = `option`;
228
+ setShortcutEvent(g_currentPage, () => true, { displayFlg: false, dfEvtFlg: true });
229
+ }
230
+ };
231
+
232
+ /**
233
+ * 次の譜面番号を取得
234
+ * @param {number} _scoreId
235
+ * @param {number} _scrollNum
236
+ * @returns {number}
237
+ */
238
+ const getNextDifficulty = (_scoreId, _scrollNum) => {
239
+ const currentPosIdx = g_headerObj.viewLists.findIndex(val => val === _scoreId);
240
+ const nextPosIdx = (currentPosIdx === -1 ? 0 : nextPos(currentPosIdx, _scrollNum, g_headerObj.viewLists.length));
241
+ return g_headerObj.viewLists[nextPosIdx];
242
+ };
243
+
244
+ /**
245
+ * 譜面選択処理
246
+ * @param {number} _scrollNum
247
+ */
248
+ const nextDifficulty = (_scrollNum = 1) => {
249
+ g_keyObj.prevKey = g_headerObj.keyLabels[g_stateObj.scoreId];
250
+ g_stateObj.scoreId = getNextDifficulty(g_stateObj.scoreId, _scrollNum);
251
+ setDifficulty(true);
252
+ resetDifWindow();
253
+ };
254
+
255
+ /**
256
+ * 譜面リストの作成
257
+ * @param {HTMLDivElement} _difList
258
+ * @param {string} _targetKey
259
+ */
260
+ const makeDifList = (_difList, _targetKey = ``) => {
261
+ let k = 0, pos = 0, curk = -1;
262
+ g_headerObj.viewLists.forEach(j => {
263
+ const keyLabel = g_headerObj.keyLabels[j];
264
+ if (_targetKey === `` || keyLabel === _targetKey) {
265
+
266
+ // 譜面名の表示
267
+ const prefix = `${getKeyName(keyLabel)} / `;
268
+ let text = `${g_headerObj.difLabels[j]}`;
269
+ if (g_headerObj.makerView) {
270
+ text += ` (${g_headerObj.creatorNames[j]})`;
271
+ }
272
+ // キー種と譜面名に分割し、譜面名が長すぎる場合は二段に分割して表示
273
+ const [difText, difSiz] = getFontSizeMulti(text, g_limitObj.difSelectorWidth, {
274
+ maxSiz: g_limitObj.difSelectorSiz, maxSizMulti: 9, prefix, len: 30,
275
+ })
276
+ _difList.appendChild(makeDifLblCssButton(`dif${k}`, difText, k, () => nextDifficulty(j - g_stateObj.scoreId), {
277
+ btnStyle: (j === g_stateObj.scoreId ? `Setting` : `Default`), siz: difSiz,
278
+ }));
279
+ document.getElementById(`dif${k}`).style.lineHeight = `9px`;
280
+ if (j === g_stateObj.scoreId) {
281
+ pos = k + 6.5 * (g_sHeight - 239) / 261;
282
+ curk = k;
283
+ }
284
+ k++;
285
+ }
286
+ });
287
+ if (document.getElementById(`lblDifCnt`) === null) {
288
+ difCover.appendChild(createDivCss2Label(`lblDifCnt`, ``, {
289
+ x: 0, y: 27, w: g_limitObj.difCoverWidth, h: 16, siz: 12, fontWeight: `bold`,
290
+ }));
291
+ }
292
+ // フィルタなし> ALL: 1/ 5, フィルタあり> 7k: 1/ 1 のように現在位置と(フィルタされた)譜面数を表示
293
+ // 現在位置が不確定の場合は、7k: -/ 1 のように現在位置を「-」で表示
294
+ const keyUnitAbbName = getStgDetailName(getKeyUnitName(_targetKey)).slice(0, 1) || ``;
295
+ lblDifCnt.innerHTML = `${_targetKey === '' ? 'ALL' : getKeyName(_targetKey) + keyUnitAbbName}: ${curk === -1 ? '-' : curk + 1} / ${k}`;
296
+ _difList.scrollTop = Math.max(pos * g_limitObj.setLblHeight - parseInt(_difList.style.height), 0);
297
+ };
298
+
299
+ /**
300
+ * 譜面セレクター位置の変更ボタン
301
+ * @param {number} _scrollNum
302
+ */
303
+ const makeDifBtn = (_scrollNum = 1) => {
304
+ const dir = _scrollNum === 1 ? `D` : `U`;
305
+ return createCss2Button(`btnDif${dir}`, g_settingBtnObj.chara[dir], () => {
306
+ do {
307
+ g_stateObj.scoreId = getNextDifficulty(g_stateObj.scoreId, _scrollNum);
308
+ } while (g_stateObj.filterKeys !== `` && g_stateObj.filterKeys !== g_headerObj.keyLabels[g_stateObj.scoreId]);
309
+ setDifficulty(true);
310
+ deleteChildspriteAll(`difList`);
311
+ makeDifList(difList, g_stateObj.filterKeys);
312
+ g_keyObj.prevKey = g_keyObj.currentKey;
313
+ }, {
314
+ x: 430 + _scrollNum * 10, y: 40, w: 20, h: 20, siz: g_limitObj.jdgCntsSiz,
315
+ }, g_cssObj.button_Mini);
316
+ };
317
+
318
+ /**
319
+ * 譜面変更セレクターの作成・再作成
320
+ * @param {string} [_key=''] 絞り込みするキー名(無指定で絞り込みしない)
321
+ */
322
+ const createDifWindow = (_key = ``) => {
323
+ g_currentPage = `difSelector`;
324
+ setShortcutEvent(g_currentPage);
325
+ const difList = createEmptySprite(optionsprite, `difList`, g_windowObj.difList, g_cssObj.settings_DifSelector);
326
+ const difCover = createEmptySprite(optionsprite, `difCover`, g_windowObj.difCover, g_cssObj.settings_DifSelector);
327
+ const difFilter = createEmptySprite(difCover, `difFilter`, g_windowObj.difFilter, g_cssObj.settings_DifSelector)
328
+
329
+ // リスト再作成
330
+ makeDifList(difList, _key);
331
+
332
+ // ランダム選択
333
+ difCover.appendChild(
334
+ makeDifLblCssButton(`difRandom`, `RANDOM`, 0, () => {
335
+ nextDifficulty(Math.floor(Math.random() * g_headerObj.keyLabels.length));
336
+ }, { w: g_limitObj.difCoverWidth })
337
+ );
338
+
339
+ // 全リスト
340
+ difCover.appendChild(
341
+ makeDifLblCssButton(`keyFilter`, `ALL`, 1.9, () => {
342
+ resetDifWindow();
343
+ g_stateObj.filterKeys = ``;
344
+ createDifWindow();
345
+ }, { w: g_limitObj.difCoverWidth, btnStyle: (g_stateObj.filterKeys === `` ? `Setting` : `Default`) })
346
+ );
347
+
348
+ // キー別フィルタボタン作成
349
+ let pos = 0;
350
+ g_headerObj.viewKeyLists.forEach((targetKey, m) => {
351
+ difFilter.appendChild(
352
+ makeDifLblCssButton(`keyFilter${m}`, `${getKeyName(targetKey)} ${getStgDetailName(getKeyUnitName(targetKey))}`, m, () => {
353
+ resetDifWindow();
354
+ g_stateObj.filterKeys = targetKey;
355
+ createDifWindow(targetKey);
356
+ }, { w: g_limitObj.difCoverWidth, btnStyle: (g_stateObj.filterKeys === targetKey ? `Setting` : `Default`) })
357
+ );
358
+ if (g_stateObj.filterKeys === targetKey) {
359
+ pos = m + 5 * (g_sHeight - 300) / 200;
360
+ }
361
+ });
362
+ difFilter.scrollTop = Math.max(pos * g_limitObj.setLblHeight - parseInt(difFilter.style.height), 0);
363
+
364
+ multiAppend(optionsprite, makeDifBtn(-1), makeDifBtn());
365
+ };
366
+
367
+ /**
368
+ * 譜面変更ボタンを押したときの処理
369
+ * @param {number} [_num=1]
370
+ */
371
+ const changeDifficulty = (_num = 1) => {
372
+ if (g_headerObj.difSelectorUse) {
373
+ g_stateObj.filterKeys = ``;
374
+ if (document.getElementById(`difList`) === null) {
375
+ g_keyObj.prevKey = g_keyObj.currentKey;
376
+ createDifWindow();
377
+ } else {
378
+ resetDifWindow();
379
+ }
380
+ } else {
381
+ nextDifficulty(_num);
382
+ }
383
+ };
384
+
385
+ /**
386
+ * 譜面基礎データの取得
387
+ * @param {number} _scoreId
388
+ * @returns {{ arrowCnts: number, frzCnts: number, apm: number, playingTime: string }}
389
+ */
390
+ const getScoreBaseData = _scoreId => {
391
+ const arrowCnts = sumData(g_detailObj.arrowCnt[_scoreId]);
392
+ const frzCnts = sumData(g_detailObj.frzCnt[_scoreId]);
393
+ return {
394
+ arrowCnts: arrowCnts,
395
+ frzCnts: frzCnts,
396
+ apm: Math.round((arrowCnts + frzCnts) / (g_detailObj.playingFrame[_scoreId] / g_fps / 60)),
397
+ playingTime: transFrameToTimer(g_detailObj.playingFrame[_scoreId]),
398
+ };
399
+ };
400
+
401
+ /**
402
+ * 速度変化グラフの描画
403
+ * @param {number} _scoreId
404
+ */
405
+ const drawSpeedGraph = _scoreId => {
406
+ const startFrame = g_detailObj.startFrame[_scoreId];
407
+ const playingFrame = g_detailObj.playingFrameWithBlank[_scoreId];
408
+ const speedObj = {
409
+ speed: { frame: [0], speed: [1], cnt: 0, strokeColor: g_graphColorObj.speed },
410
+ boost: { frame: [0], speed: [1], cnt: 0, strokeColor: g_graphColorObj.boost }
411
+ };
412
+
413
+ const tmpSpeedPoint = [0];
414
+ Object.keys(speedObj).forEach(speedType => {
415
+ const frame = speedObj[speedType].frame;
416
+ const speed = speedObj[speedType].speed;
417
+ const speedData = g_detailObj[`${speedType}Data`][_scoreId];
418
+
419
+ for (let i = 0; i < speedData?.length; i += 2) {
420
+ if (speedData[i] >= startFrame) {
421
+ frame.push(speedData[i] - startFrame);
422
+ speed.push(getSpeedFactor(speedData[i + 1]));
423
+ tmpSpeedPoint.push(speedData[i] - startFrame);
424
+ }
425
+ speedObj[speedType].cnt++;
426
+ }
427
+ frame.push(playingFrame);
428
+ speed.push(speed.at(-1));
429
+ tmpSpeedPoint.push(playingFrame);
430
+ });
431
+ const speedPoints = makeDedupliArray(tmpSpeedPoint).sort((a, b) => a - b);
432
+ let speedPointIdx = 0;
433
+
434
+ const canvas = document.getElementById(`graphSpeed`);
435
+ const context = canvas.getContext(`2d`);
436
+ const [_a, _b] = [-75, 100];
437
+ const [_min, _max] = [-0.2, 2.2];
438
+ const lineX = [0, 150], lineY = 208;
439
+
440
+ const avgX = [0, 0];
441
+ const avgSubX = [0, 0];
442
+ context.clearRect(0, 0, canvas.width, canvas.height);
443
+ drawBaseLine(context, { _fixed: 1, _mark: `x`, _a, _b, _min, _max });
444
+
445
+ Object.keys(speedObj).forEach((speedType, j) => {
446
+ const frame = speedObj[speedType].frame;
447
+ const speed = speedObj[speedType].speed;
448
+
449
+ context.beginPath();
450
+ let preY;
451
+ let avgSubFrame = 0;
452
+
453
+ for (let i = 0; i < frame.length; i++) {
454
+ const x = frame[i] * (g_limitObj.graphWidth - 30) / playingFrame + 30;
455
+ const y = (Math.min(Math.max(speed[i], _min - 0.05), _max + 0.05) - 1) * _a + _b;
456
+
457
+ context.lineTo(x, preY);
458
+ context.lineTo(x, y);
459
+ preY = y;
460
+
461
+ const prevFrame = (i > 0 ? frame[i - 1] : 0); // frame[] は startFrame 相対。初回は 0 起点
462
+ const deltaFrame = frame[i] - prevFrame;
463
+ const prevSpeed = (i > 0 ? speed[i - 1] : 1);
464
+ avgX[j] += deltaFrame * prevSpeed;
465
+ if (prevSpeed !== 1) {
466
+ avgSubFrame += deltaFrame;
467
+ avgSubX[j] += deltaFrame * prevSpeed;
468
+ }
469
+ }
470
+ avgX[j] /= playingFrame;
471
+ avgSubX[j] /= Math.max(avgSubFrame, 1);
472
+
473
+ context.lineWidth = 2;
474
+ context.strokeStyle = speedObj[speedType].strokeColor;
475
+ context.stroke();
476
+
477
+ context.beginPath();
478
+ context.moveTo(lineX[j], lineY);
479
+ context.lineTo(lineX[j] + 25, lineY);
480
+ context.stroke();
481
+ context.font = `${wUnit(g_limitObj.mainSiz)} ${getBasicFont()}`;
482
+ context.fillText(g_lblNameObj[`s_${speedType}`], lineX[j] + 30, lineY + 3);
483
+
484
+ const maxSpeed = Math.max(...speed);
485
+ const minSpeed = Math.min(...speed);
486
+ context.font = `${wUnit(g_limitObj.graphMiniSiz)} ${getBasicFont()}`;
487
+ context.fillText(`(${minSpeed.toFixed(2)}x` + (minSpeed === maxSpeed ? `` : ` -- ${Math.max(...speed).toFixed(2)}x`) + `)`, lineX[j] + 30, lineY + 16);
488
+ context.fillText(`Avg. ` + (avgX[j] === 1 ? `----` : `${(avgSubX[j]).toFixed(2)}x`), lineX[j] + 30, lineY + 29);
489
+ updateScoreDetailLabel(`Speed`, `${speedType}S`, speedObj[speedType].cnt, j, g_lblNameObj[`s_${speedType}`]);
490
+ });
491
+ updateScoreDetailLabel(`Speed`, `avgS`, `${(avgX[0] * avgX[1]).toFixed(2)}x`, 2, g_lblNameObj.s_avg);
492
+
493
+ /**
494
+ * 速度ポインタ位置の変更
495
+ * @param {number} _num
496
+ */
497
+ const changeSpdCursor = (_num = 1) => {
498
+ speedPointIdx = nextPos(speedPointIdx, _num, speedPoints.length);
499
+ movePointer(speedPoints[speedPointIdx]);
500
+ }
501
+
502
+ /**
503
+ * 速度ポインタの移動
504
+ * @param {number} _frame
505
+ */
506
+ const movePointer = _frame => {
507
+ const canvasP = document.getElementById(`graphSpeed2`);
508
+ const contextP = canvasP.getContext(`2d`);
509
+ contextP.clearRect(0, 0, canvas.width, canvas.height);
510
+
511
+ const offsetX = _frame * (g_limitObj.graphWidth - 30) / playingFrame + 30;
512
+ const speed = { speed: 0, boost: 0 };
513
+
514
+ Object.keys(speedObj).forEach(speedType => {
515
+ const speedFrames = speedObj[speedType].frame.concat(Infinity);
516
+ const speedIndex = speedObj[speedType].frame.findIndex((frame, i) => frame <= _frame && _frame < speedFrames[i + 1]);
517
+ speed[speedType] = speedObj[speedType].speed[speedIndex];
518
+ const y = (Math.min(Math.max(speed[speedType], _min - 0.05), _max + 0.05) - 1) * _a + _b;
519
+
520
+ contextP.beginPath();
521
+ contextP.fillStyle = g_graphColorObj[speedType];
522
+ contextP.arc(offsetX, y, 5, 0, 360);
523
+ contextP.closePath();
524
+ contextP.fill();
525
+ });
526
+ calculateTotalSpeed(speed.speed, speed.boost, _frame);
527
+ };
528
+
529
+ // 速度計算用ラベルの再作成
530
+ deleteDiv(detailSpeed, `btnSpdCursorL`);
531
+ deleteDiv(detailSpeed, `btnSpdCursorR`);
532
+ if (document.getElementById(`lblSpdHeader`) === null) {
533
+ multiAppend(detailSpeed,
534
+ createDivCss2Label(`lblSpdHeader`, `TotalSpeed`, g_lblPosObj.lblSpdHeader),
535
+ createDivCss2Label(`lblSpdBase`, ``, g_lblPosObj.lblSpdBase),
536
+ createDivCss2Label(`lblSpdOverall`, ``, g_lblPosObj.lblSpdOverall),
537
+ createDivCss2Label(`lblSpdBoost`, ``, g_lblPosObj.lblSpdBoost),
538
+ createDivCss2Label(`lblSpdTotal`, ``, g_lblPosObj.lblSpdTotal),
539
+ createDivCss2Label(`lblSpdFrame`, ``, g_lblPosObj.lblSpdFrame),
540
+ );
541
+ }
542
+ multiAppend(detailSpeed,
543
+ createCss2Button(`btnSpdCursorL`, `<`, () => changeSpdCursor(-1),
544
+ g_lblPosObj.btnSpdCursorL, g_cssObj.button_Mini),
545
+ createCss2Button(`btnSpdCursorR`, `>`, () => changeSpdCursor(),
546
+ g_lblPosObj.btnSpdCursorR, g_cssObj.button_Mini),
547
+ );
548
+ movePointer(0);
549
+ };
550
+
551
+ /**
552
+ * 合計速度の表示更新
553
+ * @param {number} _speed
554
+ * @param {number} _boost
555
+ * @param {number} _frame
556
+ */
557
+ const calculateTotalSpeed = (_speed = null, _boost = null, _frame = 0) => {
558
+ if (document.getElementById(`lblSpdHeader`) === null) {
559
+ return;
560
+ }
561
+ let speed, boost;
562
+ if (_speed !== null && _boost !== null) {
563
+ speed = _speed;
564
+ boost = _boost;
565
+ lblSpdOverall.textContent = `x${_speed.toFixed(2)}`;
566
+ lblSpdBoost.textContent = `x${_boost.toFixed(2)}`;
567
+ } else {
568
+ speed = Number(lblSpdOverall.textContent.slice(1));
569
+ boost = Number(lblSpdBoost.textContent.slice(1));
570
+ }
571
+ lblSpdBase.textContent = `${g_stateObj.speed.toFixed(2)}`;
572
+ lblSpdTotal.textContent = `=${(g_stateObj.speed * speed * boost).toFixed(2)}`;
573
+ lblSpdFrame.textContent = `[${transFrameToTimer(_frame + g_detailObj.startFrame[g_stateObj.scoreId])}]`;
574
+ };
575
+
576
+ /**
577
+ * 譜面密度グラフの描画
578
+ * @param {number} _scoreId
579
+ */
580
+ const drawDensityGraph = _scoreId => {
581
+
582
+ const canvas = document.getElementById(`graphDensity`);
583
+ const context = canvas.getContext(`2d`);
584
+ drawBaseLine(context);
585
+ for (let j = 0; j < g_limitObj.densityDivision; j++) {
586
+ context.beginPath();
587
+ [``, `2Push`, `3Push`].forEach(val => {
588
+ context.fillStyle = (g_detailObj.maxDensity[_scoreId].includes(j) ? g_graphColorObj[`max${val}`] : g_graphColorObj[`default${val}`]);
589
+ context.fillRect(16 * j * 16 / g_limitObj.densityDivision + 30, 195 - 9 * g_detailObj[`density${val}Data`][_scoreId][j] / 10,
590
+ 15.5 * 16 / g_limitObj.densityDivision, 9 * g_detailObj[`density${val}Diff`][_scoreId][j] / 10
591
+ );
592
+ });
593
+ context.stroke();
594
+ }
595
+
596
+ const lineNames = [`Single`, `Chord`, `Triad+`];
597
+ Object.keys(g_graphColorObj).filter(val => val.indexOf(`max`) !== -1).forEach((val, j) => {
598
+ const lineX = 70 + j * 70;
599
+
600
+ context.beginPath();
601
+ context.lineWidth = 3;
602
+ context.fillStyle = g_rankObj.rankColorAllPerfect;
603
+ context.strokeStyle = g_graphColorObj[val];
604
+ context.moveTo(lineX, 215);
605
+ context.lineTo(lineX + 20, 215);
606
+ context.stroke();
607
+ context.font = `${wUnit(g_limitObj.difSelectorSiz)} ${getBasicFont()}`;
608
+ context.fillText(lineNames[j], lineX + 20, 218);
609
+ });
610
+
611
+ const obj = getScoreBaseData(_scoreId);
612
+ updateScoreDetailLabel(`Density`, `APM`, obj.apm, 0, g_lblNameObj.s_apm);
613
+ updateScoreDetailLabel(`Density`, `Time`, obj.playingTime, 1, g_lblNameObj.s_time);
614
+ updateScoreDetailLabel(`Density`, `Arrow`, obj.arrowCnts, 3, g_lblNameObj.s_arrow);
615
+ updateScoreDetailLabel(`Density`, `Frz`, obj.frzCnts, 4, `${g_lblNameObj.s_frz}${g_headerObj.frzStartjdgUse ? ' <span class="common_bold">(2x)</span>' : ''}`);
616
+ };
617
+
618
+ /**
619
+ * 譜面明細内の補足情報の登録・更新
620
+ * @param {string} _name 表示する譜面明細のラベル
621
+ * @param {string} _label
622
+ * @param {string} _value
623
+ * @param {number} [_pos=0] 表示位置
624
+ * @param {string} [_labelname=_label]
625
+ */
626
+ const updateScoreDetailLabel = (_name, _label, _value, _pos = 0, _labelname = _label) => {
627
+ const baseLabel = (_bLabel, _bLabelname, _bAlign) =>
628
+ document.getElementById(`detail${_name}`).appendChild(
629
+ createDivCss2Label(_bLabel, _bLabelname, {
630
+ x: 10, y: 130 + _pos * 16, w: 100, h: 16, siz: g_limitObj.difSelectorSiz, align: _bAlign,
631
+ })
632
+ );
633
+ if (document.getElementById(`data${_label}`) === null) {
634
+ baseLabel(`lbl${_label}`, `${_labelname}`, C_ALIGN_LEFT);
635
+ baseLabel(`data${_label}`, `${_value}`, C_ALIGN_RIGHT);
636
+ } else {
637
+ document.getElementById(`data${_label}`).textContent = `${_value}`;
638
+ }
639
+ };
640
+
641
+ /**
642
+ * グラフの縦軸を描画
643
+ * @param {CanvasRenderingContext2D} _context
644
+ * @param {number} [object._fixed=2] y座標)目盛表記する小数桁数
645
+ * @param {string} [object._mark=''] y座標)目盛の単位
646
+ * @param {number} [object._resolution=10] y座標)明細分割数
647
+ * @param {number} [object._a=-90]
648
+ * @param {number} [object._b=105]
649
+ * @param {number} [object._min=0] y座標)目盛の下限値
650
+ * @param {number} [object._max=2] y座標)目盛の上限値
651
+ */
652
+ const drawBaseLine = (_context, { _fixed = 2, _mark = ``, _resolution = 10, _a = -90, _b = 105, _min = 0, _max = 2 } = {}) => {
653
+ _context.clearRect(0, 0, g_limitObj.graphWidth, g_limitObj.graphHeight);
654
+
655
+ for (let j = _min * _resolution; j <= _max * _resolution; j += 5) {
656
+ for (let k = 0; k < 5; k++) {
657
+ if ((j + k) % 5 === 0) {
658
+ drawLine(_context, (j + k) / _resolution, `main`, { _fixed, _mark, _a, _b });
659
+ } else {
660
+ drawLine(_context, (j + k) / _resolution, `sub`, { _fixed, _mark, _a, _b });
661
+ }
662
+ }
663
+ }
664
+ };
665
+
666
+ /**
667
+ * グラフ上に目盛を表示
668
+ * @param {CanvasRenderingContext2D} _context
669
+ * @param {number} _y
670
+ * @param {string} _lineType
671
+ * @param {number} [object._fixed] y座標)目盛表記する小数桁数
672
+ * @param {string} [object._mark] y座標)目盛の単位
673
+ * @param {number} [object._a]
674
+ * @param {number} [object._b]
675
+ */
676
+ const drawLine = (_context, _y, _lineType, { _fixed, _mark, _a, _b } = {}) => {
677
+ const lineY = (_y - 1) * _a + _b;
678
+ _context.beginPath();
679
+ _context.moveTo(30, lineY);
680
+ _context.lineTo(g_limitObj.graphWidth, lineY);
681
+ _context.lineWidth = 1;
682
+
683
+ if (_lineType === `main`) {
684
+ const textBaseObj = document.getElementById(`lnkDifficulty`);
685
+ const textColor = window.getComputedStyle(textBaseObj, ``).color;
686
+ _context.strokeStyle = textColor;
687
+ _context.font = `${wUnit(12)} ${getBasicFont()}`;
688
+ _context.fillStyle = textColor;
689
+ _context.fillText(_y.toFixed(_fixed) + _mark, 2, lineY + 4);
690
+ } else {
691
+ _context.strokeStyle = `#646464`;
692
+ }
693
+ _context.stroke();
694
+ };
695
+
696
+ /**
697
+ * 譜面の難易度情報用ラベル作成
698
+ * @param {number} _scoreId
699
+ */
700
+ const makeDifInfoLabels = _scoreId => {
701
+
702
+ // ツール難易度
703
+ const detailToolDif = document.getElementById(`detailToolDif`);
704
+ /**
705
+ * 譜面の難易度情報ラベルの作成
706
+ * @param {string} _lbl
707
+ * @param {string} _data
708
+ * @param {object} _obj
709
+ * @returns {HTMLDivElement}
710
+ */
711
+ const makeDifInfoLabel = (_lbl, _data, { x = 130, y = 25, w = 125, h = 35, siz = g_limitObj.difSelectorSiz, ...rest } = {}) =>
712
+ createDivCss2Label(_lbl, _data, { x, y, w, h, siz, align: C_ALIGN_LEFT, ...rest });
713
+
714
+ let printData = ``;
715
+ for (let j = 0; j < g_detailObj.arrowCnt.length; j++) {
716
+ const obj = getScoreBaseData(j);
717
+ printData +=
718
+ // 譜面番号
719
+ `[${j + 1}]\t` +
720
+ // ツール値
721
+ `${g_detailObj.toolDif[j].tool}\t` +
722
+ // 同時
723
+ `${g_detailObj.toolDif[j].douji}\t` +
724
+ // 縦連
725
+ `${g_detailObj.toolDif[j].tate}\t` +
726
+ // 総矢印数
727
+ `${(obj.arrowCnts + obj.frzCnts)}\t` +
728
+ // 矢印
729
+ `${obj.arrowCnts}\t` +
730
+ // フリーズアロー
731
+ `${obj.frzCnts}\t` +
732
+ // APM
733
+ `${obj.apm}\t` +
734
+ // 時間(分秒)
735
+ `${obj.playingTime}\r\n`;
736
+ }
737
+ multiAppend(detailToolDif,
738
+ makeDifInfoLabel(`lblTooldif`, g_lblNameObj.s_level, g_lblPosObj.lblTooldif),
739
+ makeDifInfoLabel(`dataTooldif`, ``, g_lblPosObj.dataTooldif),
740
+ makeDifInfoLabel(`lblDouji`, g_lblNameObj.s_douji, g_lblPosObj.lblDouji),
741
+ makeDifInfoLabel(`lblTate`, g_lblNameObj.s_tate, g_lblPosObj.lblTate),
742
+ makeDifInfoLabel(`dataDouji`, ``, g_lblPosObj.dataDouji),
743
+ makeDifInfoLabel(`dataTate`, ``, g_lblPosObj.dataTate),
744
+ makeDifInfoLabel(`lblArrowInfo`, g_lblNameObj.s_cnts, g_lblPosObj.lblArrowInfo),
745
+ makeDifInfoLabel(`dataArrowInfo`, ``, g_lblPosObj.dataArrowInfo),
746
+ makeDifInfoLabel(`lblArrowInfo2`, ``, g_lblPosObj.lblArrowInfo2),
747
+ makeDifInfoLabel(`dataArrowInfo2`, ``, g_lblPosObj.dataArrowInfo2),
748
+ makeDifLblCssButton(`lnkDifInfo`, g_lblNameObj.s_print, 8, () => {
749
+ copyTextToClipboard(
750
+ `****** ${g_lblNameObj.s_printTitle} [${g_version}] ******\r\n\r\n`
751
+ + `\t${g_lblNameObj.s_printHeader}\r\n\r\n${printData}`, g_msgInfoObj.I_0003
752
+ );
753
+ }, g_lblPosObj.lnkDifInfo),
754
+ );
755
+ createScText(lnkDifInfo, `DifInfo`, { targetLabel: `lnkDifInfo`, x: -10 });
756
+ };
757
+
758
+ /**
759
+ * 譜面の難易度情報更新
760
+ * @param {number} _scoreId
761
+ */
762
+ const makeDifInfo = _scoreId => {
763
+
764
+ const arrowCnts = sumData(g_detailObj.arrowCnt[_scoreId]);
765
+ const frzCnts = sumData(g_detailObj.frzCnt[_scoreId].map(val => Math.floor(val)));
766
+ const push3CntStr = (g_detailObj.toolDif[_scoreId].push3.length === 0 ? `None` : `(${g_detailObj.toolDif[_scoreId].push3.join(', ')})`);
767
+
768
+ if (document.getElementById(`lblTooldif`) === null) {
769
+ makeDifInfoLabels(_scoreId);
770
+ }
771
+ dataTooldif.textContent = g_detailObj.toolDif[_scoreId].tool;
772
+ dataDouji.textContent = g_detailObj.toolDif[_scoreId].douji;
773
+ dataTate.textContent = g_detailObj.toolDif[_scoreId].tate;
774
+ lblArrowInfo2.innerHTML = g_lblNameObj.s_linecnts.split(`{0}`)
775
+ .join(`${makeDedupliArray(g_detailObj.toolDif[_scoreId].push3).length} /cnt:${g_detailObj.toolDif[_scoreId].push3cnt}`);
776
+ dataArrowInfo.innerHTML = `${arrowCnts + frzCnts * (g_headerObj.frzStartjdgUse ? 2 : 1)}
777
+ <span style="font-size:${wUnit(g_limitObj.difSelectorSiz)};">(${arrowCnts} + ${frzCnts}${g_headerObj.frzStartjdgUse ? ' <span class="common_bold">x 2</span>' : ''})</span>`;
778
+
779
+ const makeArrowCntsView = (_cntlist) => {
780
+ const targetKey = g_headerObj.keyLabels[_scoreId];
781
+ const cntlist = [
782
+ _cntlist.filter((val, j) =>
783
+ g_keyObj[`pos${targetKey}_0`][j] < g_keyObj[`div${targetKey}_0`]),
784
+ _cntlist.filter((val, j) =>
785
+ g_keyObj[`pos${targetKey}_0`][j] >= g_keyObj[`div${targetKey}_0`])
786
+ ];
787
+
788
+ let cntlistStr = ``;
789
+ cntlist.filter(array => array.length > 0).forEach(array => {
790
+ const maxVal = array.reduce((a, b) => Math.max(a, b));
791
+ const minVal = array.reduce((a, b) => Math.min(a, b));
792
+
793
+ cntlistStr += `[ `;
794
+ array.forEach((val, j) => {
795
+ if (maxVal !== minVal) {
796
+ array[j] = (val === minVal ? `<span class="settings_minArrowCnts">${val}</span>` :
797
+ (val === maxVal ? `<span class="settings_maxArrowCnts common_bold">${val}</span>` : val));
798
+ }
799
+ if (val - Math.floor(val) > 0) {
800
+ array[j] = `<span class="keyconfig_warning">${val}</span>`;
801
+ }
802
+ });
803
+ cntlistStr += array.join(`, `) + ` ]`;
804
+ });
805
+
806
+ return cntlistStr;
807
+ }
808
+
809
+ dataArrowInfo2.innerHTML = `<br>${makeArrowCntsView(g_detailObj.arrowCnt[_scoreId])}<br><br>
810
+ ${makeArrowCntsView(g_detailObj.frzCnt[_scoreId])}<br><br>${push3CntStr}`;
811
+ };
812
+
813
+ /**
814
+ * ハイスコア表示
815
+ * @param {number} _scoreId
816
+ */
817
+ const makeHighScore = _scoreId => {
818
+ const detailHighScore = document.getElementById(`detailHighScore`);
819
+
820
+ // 再描画のため一度クリア
821
+ deleteChildspriteAll(`detailHighScore`);
822
+
823
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
824
+ const assistFlg = (g_autoPlaysBase.includes(g_stateObj.autoPlay) ? `` : `-${getStgDetailName(g_stateObj.autoPlay)}${getStgDetailName('less')}`);
825
+ const mirrorName = (g_stateObj.shuffle === C_FLG_OFF ? `` : `-${g_stateObj.shuffle}`);
826
+ const transKeyName = getTransKeyName();
827
+ let scoreName = getStorageKeyName(g_headerObj.keyLabels[_scoreId], transKeyName, assistFlg, mirrorName, _scoreId);
828
+
829
+ if (!hasVal(g_localStorage.highscores?.[scoreName])) {
830
+
831
+ // 古いキー定義の情報を検索
832
+ const relatedKeys = Object.entries(g_keyObj.keyTransPattern)
833
+ .filter(([key, value]) => value === g_headerObj.keyLabels[_scoreId])
834
+ .map(([key]) => key);
835
+
836
+ // 古いキー定義のハイスコアがいる場合は、現行キー定義として表示
837
+ for (const legacyKey of relatedKeys) {
838
+ let tmpScoreName = getStorageKeyName(legacyKey, transKeyName, assistFlg, mirrorName, _scoreId);
839
+ const src = g_localStorage.highscores?.[tmpScoreName];
840
+ if (hasVal(src)) {
841
+ g_localStorage.highscores[scoreName] = structuredClone(src);
842
+ break;
843
+ }
844
+ }
845
+ }
846
+
847
+ const createScoreLabel = (_id, _text, { xPos = 0, yPos = 0, dx = 0, w = 150, h = 17, colorName = _id, align = C_ALIGN_LEFT, overflow = `visible` } = {}) =>
848
+ createDivCss2Label(`lblH${toCapitalize(_id)}`, _text, {
849
+ x: xPos * 150 + 130 + dx, y: yPos * 16 + 5, w, h, siz: 14, align, overflow,
850
+ }, g_cssObj[`common_${colorName}`]);
851
+
852
+ const charas = [
853
+ `ii`, `shakin`, `matari`, `shobon`, `uwan`, `kita`, `iknai`, `maxCombo`, `fmaxCombo`, ``, `score`,
854
+ ];
855
+ const extData = {
856
+ fast: `diffFast`, slow: `diffSlow`, adj: `estAdj`, excessive: `excessive`,
857
+ };
858
+ // 各判定 (FreezeComboとScoreの間に1行の空白を入れる)
859
+ charas.forEach((chara, j) => {
860
+ if (chara === ``) {
861
+ return;
862
+ }
863
+ multiAppend(detailHighScore,
864
+ createScoreLabel(chara, g_lblNameObj[`j_${chara}`], { yPos: j }),
865
+ createScoreLabel(`${chara}S`, g_localStorage.highscores?.[scoreName]?.[chara] ?? C_FLG_HYPHEN,
866
+ { xPos: 0, yPos: j, align: C_ALIGN_RIGHT }),
867
+ );
868
+ });
869
+ // Fast, Slow, 推定Adj, Excessive (値が無ければスキップ)
870
+ Object.keys(extData).forEach((chara, j) => {
871
+ if (!hasVal(g_localStorage.highscores?.[scoreName]?.[chara], C_FLG_HYPHEN)) {
872
+ return;
873
+ }
874
+ multiAppend(detailHighScore,
875
+ createScoreLabel(chara, g_lblNameObj[`j_${chara}`], { xPos: 1, yPos: j, dx: 20, colorName: extData[chara] }),
876
+ createScoreLabel(`${chara}S`, g_localStorage.highscores?.[scoreName]?.[chara],
877
+ { xPos: 1, yPos: j, dx: -25, align: C_ALIGN_RIGHT }),
878
+ );
879
+ });
880
+ if (hasVal(g_localStorage.highscores?.[scoreName]?.adj)) {
881
+ multiAppend(detailHighScore, createScoreLabel(`adjF`, `f`, { xPos: 2, yPos: 2, dx: -23 }));
882
+ }
883
+
884
+ // カスタム表示 (resultValsViewに指定した表示のみ)
885
+ g_headerObj.resultValsView
886
+ .filter(key => hasVal(g_localStorage.highscores?.[scoreName]?.[g_presetObj.resultVals?.[key]]))
887
+ .forEach((key, j) => {
888
+ multiAppend(detailHighScore,
889
+ createScoreLabel(key, g_presetObj.resultVals[key], { xPos: 1, yPos: j + 5, dx: 20 }),
890
+ createScoreLabel(`${key}S`, g_localStorage.highscores?.[scoreName]?.[g_presetObj.resultVals[key]],
891
+ { xPos: 1, yPos: j + 5, dx: -25, align: C_ALIGN_RIGHT }),
892
+ );
893
+ });
894
+ // ランク、クリアランプ、特殊設定条件
895
+ multiAppend(detailHighScore,
896
+ createDivCss2Label(`lblHRank`, g_localStorage.highscores?.[scoreName]?.rankMark ?? `--`, {
897
+ ...g_lblPosObj.lblHRank,
898
+ color: g_localStorage.highscores?.[scoreName]?.rankColor ?? `#666666`,
899
+ fontFamily: getBasicFont(`"Bookman Old Style"`),
900
+ }),
901
+ createScoreLabel(`lblHDateTime`, g_localStorage.highscores?.[scoreName]?.dateTime ?? `----/--/-- --:--`, { yPos: 12 }),
902
+ createScoreLabel(`lblHMarks`,
903
+ `${g_localStorage.highscores?.[scoreName]?.fullCombo ?? '' ? '<span class="result_FullCombo">◆</span>' : ''}` +
904
+ `${g_localStorage.highscores?.[scoreName]?.perfect ?? '' ? '<span class="result_Perfect">◆</span>' : ''}` +
905
+ `${g_localStorage.highscores?.[scoreName]?.allPerfect ?? '' ? '<span class="result_AllPerfect">◆</span>' : ''}`, { xPos: 1, dx: 20, yPos: 12, w: 100, align: C_ALIGN_CENTER }),
906
+ createScoreLabel(`lblHClearLamps`, `Cleared: ` + (g_localStorage.highscores?.[scoreName]?.clearLamps?.join(', ') ?? C_FLG_HYPHEN), { yPos: 13, overflow: C_DIS_AUTO, w: g_sWidth / 2 + 40, h: 37 }),
907
+
908
+ createScoreLabel(`lblHShuffle`, g_stateObj.shuffle.indexOf(`Mirror`) < 0 ? `` : `Shuffle: <span class="common_shuffle">${g_stateObj.shuffle}</span>`, { yPos: 11.5, dx: -130 }),
909
+ createScoreLabel(`lblHAssist`, g_autoPlaysBase.includes(g_stateObj.autoPlay) ? `` : `Assist: <span class="common_assist">${g_stateObj.autoPlay}</span>`, { yPos: 12.5, dx: -130 }),
910
+ createScoreLabel(`lblHAnother`, !hasVal(g_keyObj[`transKey${keyCtrlPtn}`]) ? `` : `A.Keymode: <span class="common_another">${g_keyObj[`transKey${keyCtrlPtn}`]}</span>`, { yPos: 13.5, dx: -130 }),
911
+ );
912
+
913
+ // 結果をクリップボードへコピー (ハイスコア保存分)
914
+ if (g_localStorage.highscores?.[scoreName] !== undefined) {
915
+ const twiturl = new URL(g_localStorageUrlOrg);
916
+ twiturl.searchParams.append(`scoreId`, _scoreId);
917
+ const baseTwitUrl = g_isLocal ? `` : `${twiturl.toString()}`.replace(/[\t\n]/g, ``);
918
+
919
+ let tweetFrzJdg = ``;
920
+ let tweetMaxCombo = `${g_localStorage.highscores?.[scoreName]?.maxCombo}`;
921
+ if (sumData(g_detailObj.frzCnt[_scoreId]) > 0) {
922
+ tweetFrzJdg = `${g_localStorage.highscores?.[scoreName]?.kita}-${g_localStorage.highscores?.[scoreName]?.iknai}`;
923
+ tweetMaxCombo += `-${g_localStorage.highscores?.[scoreName]?.fmaxCombo}`;
924
+ }
925
+
926
+ const musicTitle = g_headerObj.musicTitles[g_headerObj.musicNos[_scoreId]] || g_headerObj.musicTitle;
927
+ let tweetDifData = `${getKeyName(g_headerObj.keyLabels[_scoreId])}${transKeyName}${getStgDetailName('k-')}${g_headerObj.difLabels[_scoreId]}${assistFlg}`;
928
+ if (g_stateObj.shuffle !== `OFF`) {
929
+ tweetDifData += `:${getStgDetailName(g_stateObj.shuffle)}`;
930
+ }
931
+
932
+ const storedExcessive = g_localStorage.highscores[scoreName]?.excessive;
933
+ const resultParams = {
934
+ hashTag: (hasVal(g_headerObj.hashTag) ? ` ${g_headerObj.hashTag}` : ``),
935
+ tuning: g_headerObj.creatorNames[_scoreId],
936
+ rankMark: g_localStorage.highscores?.[scoreName]?.rankMark || `--`,
937
+ playStyleData: g_localStorage.highscores[scoreName]?.playStyle || `--`,
938
+ highscore: g_localStorage.highscores[scoreName],
939
+ tweetExcessive: hasVal(storedExcessive, C_FLG_HYPHEN) ? `(+${storedExcessive})` : ``,
940
+ musicTitle, tweetDifData, tweetFrzJdg, tweetMaxCombo, baseTwitUrl,
941
+ };
942
+ const resultCommon = unEscapeHtml(makeResultText(g_templateObj.resultFormatDf, resultParams));
943
+ let tweetResultTmp = makeResultText(g_headerObj.resultFormat, resultParams);
944
+ if (g_presetObj.resultVals !== undefined) {
945
+ Object.keys(g_presetObj.resultVals).forEach(key =>
946
+ tweetResultTmp = tweetResultTmp.split(`[${key}]`).join(g_localStorage.highscores[scoreName][g_presetObj.resultVals[key]] || ``));
947
+ }
948
+ const resultText = `${unEscapeHtml(tweetResultTmp)}`;
949
+ multiAppend(detailHighScore,
950
+ makeDifLblCssButton(`lnkResetHighScore`, g_lblNameObj.s_resetResult, 7, () => {
951
+ if (window.confirm(g_msgObj.highscResetConfirm)) {
952
+ delete g_localStorage.highscores[scoreName];
953
+ makeHighScore(_scoreId);
954
+ }
955
+ }, { ...g_lblPosObj.lnkHighScore, btnStyle: `Reset` }),
956
+ makeDifLblCssButton(`lnkHighScore`, g_lblNameObj.s_result, 8, () => {
957
+ copyTextToClipboard(keyIsShift() ? resultCommon : resultText, g_msgInfoObj.I_0001);
958
+ }, g_lblPosObj.lnkHighScore),
959
+ );
960
+ }
961
+ };
962
+
963
+ /**
964
+ * 譜面ミニマップを描画し、適切なスクロール位置へ調整する。
965
+ * デフォルトはスクロール反転で、現在位置を保つように上下反転する。
966
+ * @param {string} _scoreId - 描画対象の譜面ID
967
+ * @param {object} [_options={}] - オプションパラメータ
968
+ * @param {boolean} [_options._initFlg=false] - 譜面切替モード
969
+ * - 前の譜面での演奏進行度(時間軸の比率)を算出し、新しい譜面でも同じ進行位置を表示する。
970
+ * 位置記憶がない場合はフェードイン設定に基づく初期位置を表示。
971
+ * @param {boolean} [_options._fadeinFlg=false] - フェードインモード。フェードイン設定値を優先して移動する。
972
+ */
973
+ const drawMinimap = (_scoreId, { _initFlg = false, _fadeinFlg = false } = {}) => {
974
+ const detailMiniMap = document.getElementById(`detailMiniMap`);
975
+ if (detailMiniMap === null) return; // scoreDetailUse=false 等で未生成の場合は何もしない
976
+
977
+ const isRev = g_stateObj.miniMapRevFlg;
978
+ const subEl = document.getElementById(`detailMiniMapSub`);
979
+ const currentScrollTop = subEl ? subEl.scrollTop : 0;
980
+
981
+ // 前の譜面でスクロール可能な状態だったかを判定
982
+ const hasPreviousScrollRange = subEl && subEl.scrollHeight > subEl.clientHeight;
983
+
984
+ // 前の譜面での「スクロール位置の比率」を計算
985
+ // 完全に一番上のときは 0、一番下のときは 1 となる比率
986
+ let progressRatio = 0;
987
+ if (hasPreviousScrollRange) {
988
+ const rawRatio = currentScrollTop / (subEl.scrollHeight - subEl.clientHeight);
989
+ // リバース時は「上が終点」なので、進行度としては反転させる
990
+ progressRatio = isRev ? (1.0 - rawRatio) : rawRatio;
991
+ }
992
+
993
+ // 再描画のため一度クリア
994
+ deleteChildspriteAll(`detailMiniMap`);
995
+
996
+ // --- ミニマップ生成/取得 (Lazy Generation) ---
997
+ let savedCanvases = isRev
998
+ ? g_detailObj.scoreMinimapReverse[_scoreId]
999
+ : g_detailObj.scoreMinimap[_scoreId];
1000
+
1001
+ const params = g_detailObj.miniMapParams[_scoreId];
1002
+ const kPtn = params._keyCtrlPtn;
1003
+ if (!g_detailObj.scoreMinimapHeader[kPtn]) {
1004
+ // ヘッダーはキー種ごとに共通なので、未作成の場合のみ生成してキャッシュ
1005
+ g_detailObj.scoreMinimapHeader[kPtn] = createMinimapHeader(params.config, kPtn, params._keyNum);
1006
+ }
1007
+ if (!savedCanvases) {
1008
+ // 未作成の場合のみミニマップを生成(Lazy Generation)
1009
+ savedCanvases = generateMinimapData(params, isRev);
1010
+
1011
+ // 生成したものをキャッシュに保存
1012
+ if (isRev) {
1013
+ g_detailObj.scoreMinimapReverse[_scoreId] = savedCanvases;
1014
+ } else {
1015
+ g_detailObj.scoreMinimap[_scoreId] = savedCanvases;
1016
+ }
1017
+ }
1018
+
1019
+ // --- ヘッダー部分 ---
1020
+ const detailMiniMapHeader = createEmptySprite(detailMiniMap, `detailMiniMapHeader`, g_windowObj.detailMiniMapHeader);
1021
+ $id(`detailMiniMapHeader`).top = wUnit(g_stateObj.miniMapRevFlg ? 230 + g_sHeight - 500 : 0);
1022
+ detailMiniMapHeader.appendChild(g_detailObj.scoreMinimapHeader[kPtn]);
1023
+
1024
+ // --- メイン(譜面)部分 ---
1025
+ const detailMiniMapSub = createEmptySprite(detailMiniMap, `detailMiniMapSub`, g_windowObj.detailMiniMapSub);
1026
+ $id(`detailMiniMapSub`).top = wUnit(g_stateObj.miniMapRevFlg ? 0 : 15);
1027
+
1028
+ Object.assign(detailMiniMapSub.style, {
1029
+ overflowX: 'hidden',
1030
+ overflowY: 'auto',
1031
+ pointerEvents: 'auto',
1032
+ display: 'block',
1033
+ textAlign: 'left',
1034
+ });
1035
+
1036
+ if (savedCanvases && Array.isArray(savedCanvases)) {
1037
+ // 退避したCanvasそのものをDOMに追加(再描画不要で高速)
1038
+ savedCanvases.forEach(canvas => {
1039
+ Object.assign(canvas.style, { position: 'static', display: 'block', height: 'auto' });
1040
+ detailMiniMapSub.appendChild(canvas);
1041
+ });
1042
+ }
1043
+ const scrollHeight = Math.max(detailMiniMapSub.scrollHeight - detailMiniMapSub.clientHeight, 0);
1044
+ const playingFrame = Math.max(g_detailObj.playingFrame[_scoreId], 1);
1045
+ const lastFrame = g_detailObj.startFrame[_scoreId] + g_detailObj.playingFrameWithBlank[_scoreId];
1046
+ const firstArrowFrame = lastFrame - g_detailObj.playingFrame[_scoreId];
1047
+ const fadeinFrameOffset = Math.max(0, Math.min(
1048
+ playingFrame,
1049
+ getStartFrame(lastFrame, g_stateObj.fadein, _scoreId) - firstArrowFrame
1050
+ ));
1051
+ const fadeinScrollTop = scrollHeight * fadeinFrameOffset / playingFrame;
1052
+ const visualFadeinPos = isRev ? scrollHeight - fadeinScrollTop : fadeinScrollTop;
1053
+
1054
+ // --- スクロール位置の決定ロジック
1055
+ let targetScrollTop = 0;
1056
+
1057
+ if (_fadeinFlg) {
1058
+ // 【最優先】フェードイン操作時:設定値を強制適用
1059
+ targetScrollTop = visualFadeinPos;
1060
+ } else if (_initFlg) {
1061
+ // 【譜面切替時】
1062
+ if (hasPreviousScrollRange) {
1063
+ // 以前の進行度を継承
1064
+ const visualRatio = isRev ? (1.0 - progressRatio) : progressRatio;
1065
+ targetScrollTop = scrollHeight * visualRatio;
1066
+ } else {
1067
+ // 初回はフェードイン位置
1068
+ targetScrollTop = visualFadeinPos;
1069
+ }
1070
+ } else {
1071
+ // 【リバース切替時】物理反転
1072
+ targetScrollTop = scrollHeight - currentScrollTop;
1073
+ }
1074
+ detailMiniMapSub.scrollTop = targetScrollTop;
1075
+
1076
+ if (document.getElementById(`lnkMiniMapRev`) === null) {
1077
+ scoreDetail.appendChild(
1078
+ makeDifLblCssButton(`lnkMiniMapRev`, g_lblNameObj.s_rev + `${g_stateObj.miniMapRevFlg ? `↑` : `↓`}`, 8, () => {
1079
+ g_stateObj.miniMapRevFlg = !g_stateObj.miniMapRevFlg;
1080
+ lnkMiniMapRev.textContent = g_lblNameObj.s_rev + `${g_stateObj.miniMapRevFlg ? `↑` : `↓`}`;
1081
+ drawMinimap(g_stateObj.scoreId);
1082
+ createScText(lnkMiniMapRev, `MiniMapRev`, { targetLabel: `lnkMiniMapRev`, x: -12 });
1083
+ }, g_lblPosObj.lnkMiniMapRev)
1084
+ );
1085
+ createScText(lnkMiniMapRev, `MiniMapRev`, { targetLabel: `lnkMiniMapRev`, x: -12 });
1086
+ }
1087
+ };
1088
+
1089
+ /**
1090
+ * 譜面ミニマップ:キー名を表示するヘッダーキャンバスを作成する
1091
+ * @param {object} _config ミニマップの基本設定
1092
+ * @param {number} _config.timeMargin 時間軸のマージン
1093
+ * @param {number} _config.laneWidth レーンの幅
1094
+ * @param {number} _config.logicalWidth キャンバスの論理幅
1095
+ * @param {string} _keyCtrlPtn キーコントロールパターン
1096
+ * @param {number} _keyNum キー数
1097
+ * @return {HTMLCanvasElement} ヘッダー用のキャンバス要素
1098
+ */
1099
+ const createMinimapHeader = (_config, _keyCtrlPtn, _keyNum) => {
1100
+ const { timeMargin, laneWidth, logicalWidth } = _config;
1101
+ const headerHeight = 15; // ヘッダーの固定高
1102
+
1103
+ const canvas = document.createElement('canvas');
1104
+ const ctx = canvas.getContext('2d');
1105
+
1106
+ // 解像度と表示サイズの設定
1107
+ canvas.width = logicalWidth * g_dpr;
1108
+ canvas.height = headerHeight * g_dpr;
1109
+ canvas.style.width = wUnit(logicalWidth);
1110
+ canvas.style.height = wUnit(headerHeight);
1111
+ canvas.style.display = 'block';
1112
+
1113
+ ctx.scale(g_dpr, g_dpr);
1114
+
1115
+ // テキストのスタイル設定
1116
+ ctx.fillStyle = '#999';
1117
+ ctx.font = `10px ${getBasicFont()}`;
1118
+ ctx.textAlign = 'center';
1119
+ ctx.textBaseline = 'middle';
1120
+
1121
+ // 各レーンのキー名を描画
1122
+ for (let j = 0; j < _keyNum; j++) {
1123
+ // config.laneWidth を使って中央座標を計算
1124
+ const x = timeMargin + j * laneWidth + laneWidth / 2;
1125
+ const keyText = g_kCd[g_keyObj[`keyCtrl${_keyCtrlPtn}`][j][0]].split(` `).join(``);
1126
+
1127
+ ctx.fillText(keyText, x, headerHeight / 2 + 2); // 視覚的な中央調整で +2px
1128
+ }
1129
+
1130
+ return canvas;
1131
+ };
1132
+
1133
+ /**
1134
+ * 譜面ミニマップ:譜面ミニマップ本体生成
1135
+ * @param {object} _params ミニマップ生成のためのパラメータオブジェクト
1136
+ * @param {object} _params._scoreObj 譜面データオブジェクト
1137
+ * @param {number} _params._keyNum キー数
1138
+ * @param {number} _params._playingFrame 演奏時間(フレーム数)
1139
+ * @param {number} _params._firstArrowFrame 最初の矢印のフレーム位置
1140
+ * @param {string} _params._keyCtrlPtn キーコントロールパターン
1141
+ * @param {object} _params.config ミニマップの基本設定
1142
+ * @param {number} _params.config.scale ミニマップの時間軸のスケール
1143
+ * @param {number} _params.config.timeMargin 時間軸のマージン
1144
+ * @param {number} _params.config.laneWidth レーンの幅
1145
+ * @param {number} _params.config.logicalWidth キャンバスの論理幅
1146
+ * @param {number} _params.config.mmMarginY ミニマップの上下マージン
1147
+ * @param {boolean} _isReverse ミニマップのリバース表示フラグ
1148
+ * @returns {HTMLCanvasElement[]} ミニマップ用のキャンバス要素の配列
1149
+ */
1150
+ const generateMinimapData = (_params, _isReverse) => {
1151
+ const { _scoreObj, _keyNum, _playingFrame, _firstArrowFrame, _keyCtrlPtn, config } = _params;
1152
+ const { scale, timeMargin, laneWidth, logicalWidth, mmMarginY } = config;
1153
+
1154
+ const mmHeightTotal = _playingFrame * scale + mmMarginY * 2;
1155
+ const canvases = createSplitCanvases(logicalWidth, mmHeightTotal);
1156
+
1157
+ const getY = (frame) => {
1158
+ const relativeFrame = frame - _firstArrowFrame;
1159
+ const rawY = relativeFrame * scale;
1160
+ // mmHeightTotalから引くのではなく、中身の演奏時間部分(_playingFrame * scale)を基準にリバース
1161
+ return _isReverse
1162
+ ? (_playingFrame * scale - rawY + mmMarginY)
1163
+ : (rawY + mmMarginY);
1164
+ };
1165
+
1166
+ // 1. 時間軸描画
1167
+ const interval = g_fps;
1168
+ for (let f = Math.ceil(_firstArrowFrame / interval) * interval; f <= _firstArrowFrame + _playingFrame; f += interval) {
1169
+ const y = getY(f);
1170
+ distributeDrawing(canvases, y - 5, 10, g_dpr, (ctx) => {
1171
+ ctx.strokeStyle = '#444';
1172
+ ctx.fillStyle = '#999';
1173
+ ctx.font = `10px ${getBasicFont()}`;
1174
+ ctx.textAlign = 'right';
1175
+ ctx.textBaseline = 'middle';
1176
+ ctx.beginPath(); ctx.moveTo(timeMargin, y); ctx.lineTo(timeMargin + laneWidth * _keyNum, y); ctx.stroke();
1177
+ const [m, s] = transFrameToTimer(f).split(':');
1178
+ ctx.fillText(`${m.padStart(2, '0')}:${s}`, timeMargin, y);
1179
+ });
1180
+ }
1181
+
1182
+ // 2. フリーズノート
1183
+ for (let j = 0; j < _keyNum; j++) {
1184
+ const frz = _scoreObj.frzData[j];
1185
+ for (let k = 0; k < frz.length; k += 2) {
1186
+ const start = frz[k];
1187
+ const end = frz[k + 1];
1188
+
1189
+ // 終了地点がない、またはどちらかが数値でない場合はスキップ
1190
+ if (end === undefined || isNaN(start) || isNaN(end)) {
1191
+ console.warn(`Minimap: Incomplete freeze note pair at lane ${j}, index ${k}`);
1192
+ continue;
1193
+ }
1194
+ const y1 = getY(start);
1195
+ const y2 = getY(end);
1196
+ const top = Math.min(y1, y2);
1197
+ const h = Math.abs(y2 - y1);
1198
+ const x = timeMargin + j * laneWidth;
1199
+ distributeDrawing(canvases, top, h, g_dpr, (ctx) => {
1200
+ ctx.fillStyle = 'rgba(0, 200, 255, 0.4)';
1201
+ ctx.fillRect(x + 2, top, laneWidth - 3, h);
1202
+ ctx.strokeStyle = 'rgba(0, 200, 255, 0.8)';
1203
+ ctx.strokeRect(x + 2, top, laneWidth - 3, h);
1204
+ });
1205
+ }
1206
+ }
1207
+
1208
+ // 3. 通常ノート
1209
+ for (let j = 0; j < _keyNum; j++) {
1210
+ const color = g_dfColorObj.setColorType2[g_keyObj[`color${_keyCtrlPtn}_0`][j]] || '#ffffff';
1211
+ _scoreObj.arrowData[j].forEach(note => {
1212
+ const y = getY(parseFloat(note));
1213
+ distributeDrawing(canvases, y - 1.5, 3, g_dpr, (ctx) => {
1214
+ ctx.fillStyle = color;
1215
+ ctx.fillRect(timeMargin + j * laneWidth + 1, y - 1.5, laneWidth - 1, 3);
1216
+ });
1217
+ });
1218
+ }
1219
+
1220
+ return canvases.map(item => item.canvas);
1221
+ };
1222
+
1223
+ /**
1224
+ * 描画対象のCanvasを判定して描画を実行する
1225
+ * @param {object[]} _canvases
1226
+ * @param {HTMLCanvasElement} _canvases[].canvas 分割されたCanvas要素
1227
+ * @param {CanvasRenderingContext2D} _canvases[].ctx Canvasの描画コンテキスト
1228
+ * @param {number} _canvases[].offsetTop Canvasの論理上のオフセット位置
1229
+ * @param {number} _canvases[].logicalHeight Canvasの論理上の高さ
1230
+ * @param {number} _y
1231
+ * @param {number} _h
1232
+ * @param {number} _dpr
1233
+ * @param {Function} _drawFunc
1234
+ */
1235
+ const distributeDrawing = (_canvases, _y, _h, _dpr, _drawFunc) => {
1236
+ _canvases.forEach(item => {
1237
+ const canvasHeight = item.logicalHeight;
1238
+ if (_y + _h >= item.offsetTop && _y <= item.offsetTop + canvasHeight) {
1239
+ item.ctx.save();
1240
+ item.ctx.translate(0, -item.offsetTop);
1241
+ _drawFunc(item.ctx);
1242
+ item.ctx.restore();
1243
+ }
1244
+ });
1245
+ };
1246
+
1247
+ /**
1248
+ * 指定したキー名のキー別ストレージオブジェクトを取得
1249
+ * @param {string} _keyName
1250
+ * @returns {[Object, string]} [storageObj, addKey]
1251
+ */
1252
+ const getKeyStorageObjByName = (_keyName) => {
1253
+ if (g_headerObj.keyExtraList.includes(_keyName)) {
1254
+ return [g_localStorage, _keyName];
1255
+ }
1256
+ return [parseStorageData(`danonicw-${_keyName}k`, {
1257
+ reverse: C_FLG_OFF, keyCtrl: [[]], keyCtrlPtn: 0, setColor: [],
1258
+ }), ``];
1259
+ };
1260
+
1261
+ /**
1262
+ * 別キーモード時、移行先キーが保持するSelf保存パターンの取得可否チェック
1263
+ * @param {string} _keyCtrlPtn 例: "7_2"
1264
+ * @returns {number[][]|undefined} 適用可能な場合はSelf保存済みkeyCtrl配列、不可ならundefined
1265
+ */
1266
+ const getTransKeySelfCtrl = (_keyCtrlPtn) => {
1267
+ const transKeyName = g_keyObj[`transKey${_keyCtrlPtn}`];
1268
+ const transKeyPtn = g_keyObj[`transKeyPtn${_keyCtrlPtn}`];
1269
+ if (!hasVal(transKeyName) || transKeyPtn === undefined) {
1270
+ return undefined;
1271
+ }
1272
+ const [storageObj, addKey] = getKeyStorageObjByName(transKeyName);
1273
+ const savedCtrl = storageObj[`keyCtrl${addKey}`];
1274
+ const savedPtn = storageObj[`keyCtrlPtn${addKey}`];
1275
+
1276
+ if (savedPtn !== transKeyPtn) {
1277
+ return undefined;
1278
+ }
1279
+
1280
+ // 現在のキー配置(基準形状)とレーン数・各レーン長が完全一致することを確認
1281
+ const baseCtrl = g_keyObj[`keyCtrl${_keyCtrlPtn}`];
1282
+ const isValidShape = hasArrayList(savedCtrl, baseCtrl.length) &&
1283
+ savedCtrl.length === baseCtrl.length &&
1284
+ baseCtrl.every((lane, j) => hasArrayList(savedCtrl[j], lane.length) && savedCtrl[j].length === lane.length);
1285
+
1286
+ return isValidShape ? savedCtrl : undefined;
1287
+ };
1288
+
1289
+ /**
1290
+ * 別キーモードの移行先Selfパターンを現在のキー配置へ反映(保存はしない)
1291
+ */
1292
+ const applyTransKeySelfPattern = () => {
1293
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
1294
+ const savedCtrl = getTransKeySelfCtrl(keyCtrlPtn);
1295
+ if (savedCtrl === undefined) {
1296
+ return;
1297
+ }
1298
+ const baseKeyNum = g_keyObj[`${g_keyObj.defaultProp}${keyCtrlPtn}`].length;
1299
+ for (let j = 0; j < baseKeyNum; j++) {
1300
+ for (let k = 0; k < (savedCtrl[j]?.length ?? 0); k++) {
1301
+ g_keyObj[`keyCtrl${keyCtrlPtn}`][j][k] = setIntVal(savedCtrl[j][k], 0);
1302
+ }
1303
+ }
1304
+ keyConfigInit();
1305
+ };
1306
+
1307
+ /**
1308
+ * 譜面初期化処理
1309
+ * - 譜面の基本設定(キー数、初期速度、リバース、ゲージ設定)をここで行う
1310
+ * - g_canLoadDifInfoFlg は譜面初期化フラグで、初期化したくない場合は対象画面にて false にしておく
1311
+ * (Display設定画面、キーコンフィグ画面では通常OFF)
1312
+ * この関数を実行後、このフラグはONに戻るようになっている
1313
+ * - [キーコン]->[初期化]->[名称設定]の順に配置する。
1314
+ * 初期化処理にてキー数関連の設定を行っているため、この順序で無いとデータが正しく格納されない
1315
+ *
1316
+ * @param {boolean} _initFlg
1317
+ */
1318
+ const setDifficulty = (_initFlg) => {
1319
+
1320
+ // ---------------------------------------------------
1321
+ // 1. キーコンフィグ設定 (KeyConfig)
1322
+ g_keyObj.currentKey = g_headerObj.keyLabels[g_stateObj.scoreId];
1323
+ const isNotSameKey = (g_keyObj.prevKey !== g_keyObj.currentKey);
1324
+
1325
+ if (g_headerObj.dummyScoreNos !== undefined) {
1326
+ g_stateObj.dummyId = setIntVal(g_headerObj.dummyScoreNos[g_stateObj.scoreId], ``);
1327
+ }
1328
+ // 特殊キーフラグ
1329
+ g_stateObj.extraKeyFlg = g_headerObj.keyExtraList.includes(g_keyObj.currentKey);
1330
+
1331
+ // ---------------------------------------------------
1332
+ // 2. 初期化設定
1333
+
1334
+ // 保存した設定の再読込条件(設定画面切り替え時はスキップ)
1335
+ // ローカルストレージで保存した設定を呼び出し
1336
+ if ((g_canLoadDifInfoFlg && (isNotSameKey && g_stateObj.dataSaveFlg)) || _initFlg) {
1337
+
1338
+ if (isNotSameKey && g_keyObj.prevKey !== `Dummy`) {
1339
+ // キーパターン初期化
1340
+ g_keyObj.currentPtn = 0;
1341
+ g_keycons.keySwitchNum = 0;
1342
+ }
1343
+ const [storageObj, addKey] = getKeyStorageObjByName(g_keyObj.currentKey);
1344
+ if (isNotSameKey) {
1345
+ if (!g_stateObj.extraKeyFlg) {
1346
+ g_localKeyStorage = storageObj;
1347
+ }
1348
+ getKeyReverse(storageObj, addKey);
1349
+
1350
+ // キーコンフィグ初期値設定
1351
+ if (storageObj[`keyCtrlPtn${addKey}`] === undefined) {
1352
+ storageObj[`keyCtrlPtn${addKey}`] = 0;
1353
+ }
1354
+ getKeyCtrl(storageObj, addKey);
1355
+
1356
+ // カラーセット初期値設定
1357
+ if (storageObj[`setColor${addKey}`] === undefined) {
1358
+ storageObj[`setColor${addKey}`] = [];
1359
+ }
1360
+ if (storageObj[`setColor${addKey}`].length > 0) {
1361
+ g_keycons.colorTypes = addValtoArray(g_keycons.colorTypes, g_keycons.colorSelf);
1362
+ resetColorType({ _fromObj: storageObj, _from: addKey, _to: g_keycons.colorSelf });
1363
+ resetColorType({ _fromObj: storageObj, _from: addKey, _toObj: g_dfColorObj, _to: g_keycons.colorSelf });
1364
+
1365
+ } else {
1366
+ if (g_localStorage.colorType === g_keycons.colorSelf) {
1367
+ g_colorType = `Default`;
1368
+ }
1369
+ g_keycons.colorTypes = g_keycons.colorTypes.filter(val => val !== g_keycons.colorSelf);
1370
+ }
1371
+
1372
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
1373
+
1374
+ // カラーグループ、シャッフルグループの設定
1375
+ g_keycons.groups.forEach(type => {
1376
+ resetGroupList(type, keyCtrlPtn);
1377
+ if (g_keyObj.currentPtn === -1) {
1378
+ const storageKeyName = storageObj[`${type}${addKey}`] || storageObj[`${type}${g_keyObj.currentKey}_-1_-1`];
1379
+ if (storageKeyName !== undefined) {
1380
+ g_keyObj[`${type}${g_keyObj.currentKey}_-1`] = structuredClone(storageKeyName);
1381
+ }
1382
+ g_keyObj[`${type}${g_keyObj.currentKey}_-1_-1`] = structuredClone(g_keyObj[`${type}${g_keyObj.currentKey}_-1`]);
1383
+ } else {
1384
+ g_keyObj[`${type}${keyCtrlPtn}`] = structuredClone(g_keyObj[`${type}${keyCtrlPtn}_0`]);
1385
+ }
1386
+ });
1387
+
1388
+ }
1389
+ // 曲中ショートカットキーの切り替え
1390
+ setPlayingShortcut();
1391
+ }
1392
+
1393
+ // スクロール設定用の配列を入れ替え
1394
+ g_settings.scrolls = structuredClone(
1395
+ typeof g_keyObj[`scrollDir${g_keyObj.currentKey}_${g_keyObj.currentPtn}`] === C_TYP_OBJECT ?
1396
+ Object.keys(g_keyObj[`scrollDir${g_keyObj.currentKey}_${g_keyObj.currentPtn}`]) : g_keyObj.scrollName_def
1397
+ );
1398
+
1399
+ // アシスト設定の配列を入れ替え
1400
+ g_settings.autoPlays = (typeof g_keyObj[`assistPos${g_keyObj.currentKey}_${g_keyObj.currentPtn}`] === C_TYP_OBJECT ?
1401
+ g_autoPlaysBase.concat(Object.keys(g_keyObj[`assistPos${g_keyObj.currentKey}_${g_keyObj.currentPtn}`])) :
1402
+ g_autoPlaysBase.concat());
1403
+
1404
+ // ゲージ設定及びカーソル位置調整
1405
+ setGauge(0, true);
1406
+
1407
+ // 速度、スクロール、アシスト設定のカーソル位置調整
1408
+ if (_initFlg) {
1409
+ g_stateObj.speed = g_headerObj.initSpeeds[g_stateObj.scoreId];
1410
+ g_settings.speedNum = getCurrentNo(g_settings.speeds, g_stateObj.speed);
1411
+ }
1412
+ g_settings.scrollNum = getCurrentNo(g_settings.scrolls, g_stateObj.scroll);
1413
+ g_settings.autoPlayNum = getCurrentNo(g_settings.autoPlays, g_stateObj.autoPlay);
1414
+
1415
+
1416
+ // 選択中のキーのヘルプ表示
1417
+ const targetKeymode = hasVal(g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`])
1418
+ ? g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`] : g_keyObj.currentKey;
1419
+ btnKeymodeHelp.classList.remove(g_cssObj.button_Setting, g_cssObj.button_Tweet);
1420
+ btnKeymodeHelp.classList.add(
1421
+ g_cssObj[`button_${targetKeymode !== g_keyObj.currentKey
1422
+ && (g_keyObj.defaultKeyList.includes(targetKeymode) || g_lblNameObj[`keyHelp${targetKeymode}`]) ? `Tweet` : `Setting`}`]
1423
+ );
1424
+ btnKeymodeHelp.style.display = (
1425
+ g_keyObj.defaultKeyList.includes(targetKeymode) || g_lblNameObj[`keyHelp${targetKeymode}`]
1426
+ ? `` : C_DIS_NONE
1427
+ );
1428
+
1429
+ // ---------------------------------------------------
1430
+ // 3. 名称の設定
1431
+
1432
+ // 譜面名設定 (Difficulty)
1433
+ const difWidth = parseFloat(lnkDifficulty.style.width) - 20;
1434
+ const transKeyName = getTransKeyName();
1435
+ const keyUnitName = getStgDetailName(getKeyUnitName(g_keyObj.currentKey));
1436
+
1437
+ const prefix = `${getKeyName(g_keyObj.currentKey)}${transKeyName} ${keyUnitName} / `;
1438
+ let difLabel = `${g_headerObj.difLabels[g_stateObj.scoreId]}`;
1439
+
1440
+ const [difName, difSiz] = getFontSizeMulti(difLabel, difWidth, { maxSiz: g_limitObj.setLblSiz, prefix });
1441
+ lnkDifficulty.style.fontSize = wUnit(difSiz);
1442
+
1443
+ const difNames = [difName];
1444
+ if (g_headerObj.makerView) {
1445
+ difNames.push(`(${g_headerObj.creatorNames[g_stateObj.scoreId]})`);
1446
+ difNames.forEach((difName, j) => {
1447
+ const tmpSize = getFontSize2(difName, difWidth);
1448
+ difNames[j] = `<span style="font-size:${wUnit(tmpSize)}">${difName}</span>`;
1449
+ });
1450
+ }
1451
+ lnkDifficulty.innerHTML = difNames.join(``);
1452
+
1453
+ // 速度設定 (Speed)
1454
+ setSetting(0, `speed`, { unitName: ` ${g_lblNameObj.multi}` });
1455
+
1456
+ // リバース設定 (Reverse, Scroll)
1457
+ if (g_headerObj.scrollUse) {
1458
+ g_stateObj.scroll = g_settings.scrolls[g_settings.scrollNum];
1459
+ const [visibleScr, hiddenScr] = (g_settings.scrolls.length > 1 ? [`scroll`, `reverse`] : [`reverse`, `scroll`]);
1460
+ document.getElementById(`${visibleScr}Sprite`).style.display = C_DIS_INHERIT;
1461
+ document.getElementById(`${hiddenScr}Sprite`).style.display = C_DIS_NONE;
1462
+ setSetting(0, visibleScr);
1463
+
1464
+ g_shortcutObj.option.KeyR.id = g_settings.scrolls.includes(C_FLG_REVERSE) ?
1465
+ g_shortcutObj.option.KeyR.exId : g_shortcutObj.option.KeyR.dfId;
1466
+
1467
+ if (g_settings.scrolls.length > 1) {
1468
+ setReverseView(document.getElementById(`btnReverse`));
1469
+ }
1470
+ } else {
1471
+ g_settings.scrolls = structuredClone(g_keyObj.scrollName_def);
1472
+ setSetting(0, `reverse`);
1473
+ }
1474
+
1475
+ // オート・アシスト設定 (AutoPlay)
1476
+ g_stateObj.autoPlay = g_settings.autoPlays[g_settings.autoPlayNum];
1477
+ lnkAutoPlay.textContent = getStgDetailName(g_stateObj.autoPlay);
1478
+
1479
+ // 譜面毎のExcessive再設定(意図的に変更した場合のみ元に戻さない)
1480
+ if (g_canLoadDifInfoFlg && (g_stateObj.excessiveScoreId !== g_stateObj.scoreId)) {
1481
+ g_headerObj.excessiveJdgUse = g_headerObj.excessiveJdgUses[g_stateObj.scoreId];
1482
+ g_stateObj.excessive = boolToSwitch(g_headerObj.excessiveJdgUse);
1483
+ }
1484
+ g_headerObj.excessiveUse = g_headerObj.excessiveUses[g_stateObj.scoreId];
1485
+ if (g_headerObj.excessiveUse) {
1486
+ setExcessive(document.getElementById(`lnkExcessive`), g_stateObj.excessive === C_FLG_ON);
1487
+ lblExcessive.style.display = C_DIS_NONE;
1488
+ lnkExcessive.style.display = C_DIS_INHERIT;
1489
+ } else {
1490
+ g_stateObj.excessiveChgFlg = false;
1491
+ lblExcessive.style.display = (g_headerObj.excessiveJdgUses[g_stateObj.scoreId] ? C_DIS_INHERIT : C_DIS_NONE);
1492
+ lnkExcessive.style.display = C_DIS_NONE;
1493
+ }
1494
+
1495
+ // 譜面明細画面の再描画
1496
+ if (g_settings.scoreDetails.length > 0) {
1497
+ drawSpeedGraph(g_stateObj.scoreId);
1498
+ drawDensityGraph(g_stateObj.scoreId);
1499
+ makeDifInfo(g_stateObj.scoreId);
1500
+ makeHighScore(g_stateObj.scoreId);
1501
+ drawMinimap(g_stateObj.scoreId, { _initFlg: true });
1502
+ }
1503
+
1504
+ // 楽曲データの表示
1505
+ lblMusicInfo.textContent = getMusicInfoView();
1506
+ lblMusicInfo.style.fontSize = wUnit(getFontSize2(lblMusicInfo.textContent, g_btnWidth(3 / 4), { maxSiz: 12 }));
1507
+
1508
+ // ユーザカスタムイベント(初期)
1509
+ safeExecuteCustomHooks(`g_customJsObj.difficulty`, g_customJsObj.difficulty, _initFlg, g_canLoadDifInfoFlg);
1510
+ resolveKeyFamily();
1511
+
1512
+ // 設定サマリー表示の更新
1513
+ updateSettingSummary();
1514
+
1515
+ // ---------------------------------------------------
1516
+ // 4. 譜面初期情報ロード許可フラグの設定
1517
+ g_canLoadDifInfoFlg = true;
1518
+ };
1519
+
1520
+ /**
1521
+ * keyLabelの系統ごとに、選択時に強制したい設定値・選択/離脱時の個別処理を登録する
1522
+ * @param {string} _name 系統名
1523
+ * @param {(key: string, context: { scoreId: number, ptn: string, prevKey: string }) => boolean} _match
1524
+ * この系統に属するkeyLabelかどうかの判定関数。第二引数のcontextは任意で利用可能
1525
+ * @param {object} [_fields] path -> 値 or 値を返す関数(単純代入で済むもの)
1526
+ * @param {object} [_hooks] { onAcquire, onRelease } 選択/離脱の瞬間に1回だけ呼ばれる(複合処理用)
1527
+ * @param {object} [_appliers] path -> 個別の適用関数(単純代入で済まないfields用)
1528
+ */
1529
+ const registerKeyFamily = (_name, _match, _fields = {}, _hooks = {}, _appliers = {}) => {
1530
+ g_familyObj.families.push({ name: _name, match: _match, fields: _fields, appliers: _appliers, ..._hooks });
1531
+ };
1532
+
1533
+ /**
1534
+ * 現在のキー状態に応じて適用すべき設定群を判定し、
1535
+ * 各パラメータの値・スナップショット・フックを管理・適用する
1536
+ */
1537
+ const resolveKeyFamily = () => {
1538
+ // match() へ渡す追加情報。第一引数(keyLabel)だけで判定できる場合は無視してよい
1539
+ const context = {
1540
+ scoreId: g_stateObj.scoreId,
1541
+ ptn: g_keyObj.currentPtn,
1542
+ prevKey: g_keyObj.prevKey,
1543
+ };
1544
+
1545
+ // 1. 現在のキーに一致するファミリーを検索(見つからなければ null = 標準状態)
1546
+ const newFamily = g_familyObj.families.find(f => f.match(g_keyObj.currentKey, context)) ?? null;
1547
+
1548
+ // 2. ファミリーが切り替わった瞬間(入場・離脱)にフック(固有の副作用)を実行
1549
+ if (newFamily !== g_familyObj.currentFamily) {
1550
+ g_familyObj.currentFamily?.onRelease?.();
1551
+ newFamily?.onAcquire?.();
1552
+ g_familyObj.currentFamily = newFamily;
1553
+ }
1554
+
1555
+ // 3. 登録されているすべてのファミリーが持つパス(プロパティ)の全リストを取得
1556
+ const allPaths = new Set(g_familyObj.families.flatMap(f => Object.keys(f.fields)));
1557
+ allPaths.forEach(path => {
1558
+ const spec = newFamily?.fields[path];
1559
+
1560
+ if (spec !== undefined) {
1561
+ // 【変更値の適用】現在のファミリーにそのプロパティの設定がある場合
1562
+ // 初めて書き換えるプロパティの場合のみ、元の値をスナップショット(初期値)として退避
1563
+ if (!g_familyObj.ownership[path]) {
1564
+ g_familyObj.snapshot[path] = getPathVal(path);
1565
+ }
1566
+ // 関数なら評価し、値ならそのまま適用値とする
1567
+ g_familyObj.ownership[path] = newFamily;
1568
+ const applier = newFamily.appliers[path] ?? (v => setPathVal(path, v));
1569
+ applyIfChanged(path, typeof spec === `function` ? spec() : spec, applier);
1570
+
1571
+ } else if (g_familyObj.ownership[path]) {
1572
+ // 【標準への復元】別のファミリーに移行し、かつ元々自分が書き換えていたプロパティの場合
1573
+ // スナップショットから元の値(初期値)を復元し、オーナーシップを解放
1574
+ const applier = g_familyObj.ownership[path].appliers[path] ?? (v => setPathVal(path, v));
1575
+ applyIfChanged(path, g_familyObj.snapshot[path], applier);
1576
+ g_familyObj.ownership[path] = null;
1577
+ }
1578
+ // ownership[path]がnullのままなら、標準同士の切り替えでも一切触らない
1579
+ });
1580
+ };
1581
+
1582
+ /**
1583
+ * 値に変更がある場合のみ、代入またはカスタム処理を実行する
1584
+ * - 「エンジンが前回適用した値」ではなく「実際に今その場所にある値」と比較することで、
1585
+ * ユーザーの手動操作など、本エンジンを介さずに値が変更されたケースでも
1586
+ * 正しく差分を検知できるようにしている
1587
+ * @param {string} _path 設定のパス
1588
+ * @param {string|number} _value 適用する値
1589
+ * @param {Function} _applier 実際の適用処理を行う関数
1590
+ */
1591
+ const applyIfChanged = (_path, _value, _applier) => {
1592
+ // 実際の現在値と適用したい値が一致していれば、無駄な再描画や再代入を防ぐためスキップ
1593
+ if (getPathVal(_path) === _value) return;
1594
+
1595
+ // 実際の反映処理(値の代入、または画像再描画などの副作用)を実行
1596
+ _applier(_value);
1597
+ };
1598
+
1599
+ /**
1600
+ * 設定・オプション画面のラベル・ボタン処理の描画
1601
+ * @param {Object} _sprite 基準とするスプライト(ここで指定する座標は、そのスプライトからの相対位置)
1602
+ */
1603
+ const createOptionWindow = _sprite => {
1604
+
1605
+ // 各ボタン用のスプライトを作成
1606
+ const optionsprite = createEmptySprite(_sprite, `optionsprite`, g_windowObj.optionSprite);
1607
+
1608
+ // 設定毎に個別のスプライトを作成し、その中にラベル・ボタン類を配置
1609
+ const spriteList = setSpriteList(g_settingPos.option);
1610
+
1611
+ // ---------------------------------------------------
1612
+ // 難易度 (Difficulty)
1613
+ // 縦位置: 0
1614
+ spriteList.difficulty.appendChild(createLblSetting(`Difficulty`, -5));
1615
+
1616
+ const lnkDifficulty = makeSettingLblCssButton(`lnkDifficulty`, ``, 0, () => changeDifficulty(), {
1617
+ y: -10, h: g_limitObj.setLblHeight + 10, cxtFunc: () => changeDifficulty(-1),
1618
+ });
1619
+
1620
+ // 譜面選択ボタン(メイン、右回し、左回し)
1621
+ multiAppend(spriteList.difficulty,
1622
+ lnkDifficulty,
1623
+ makeMiniCssButton(`lnkDifficulty`, `R`, 0, () => nextDifficulty(), { dy: -10, dh: 10 }),
1624
+ makeMiniCssButton(`lnkDifficulty`, `L`, 0, () => nextDifficulty(-1), { dy: -10, dh: 10 }),
1625
+ );
1626
+ createScText(spriteList.difficulty, `Difficulty`);
1627
+ if (g_headerObj.difSelectorUse) {
1628
+ createScText(spriteList.difficulty, `DifficultyList`, { x: 154, y: -10, targetLabel: `lnkDifficulty` });
1629
+ }
1630
+ multiAppend(difficultySprite,
1631
+ createCss2Button(`btnKeymodeHelp`, `?`, () => {
1632
+ const targetKeymode = hasVal(g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`])
1633
+ ? g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`] : g_keyObj.currentKey;
1634
+ openLink(
1635
+ g_keyObj.defaultKeyList.includes(targetKeymode)
1636
+ ? g_lblNameObj.keymodeUrl + targetKeymode
1637
+ : g_lblNameObj[`keyHelp${targetKeymode}`]);
1638
+ }, g_lblPosObj.btnKeymodeHelp, g_cssObj.button_Setting),
1639
+ )
1640
+
1641
+ // ---------------------------------------------------
1642
+ // ハイスコア機能実装時に使用予定のスペース
1643
+ // 縦位置: 1
1644
+
1645
+ // ---------------------------------------------------
1646
+ // 速度(Speed)
1647
+ // 縦位置: 2 短縮ショートカットあり
1648
+ createGeneralSetting(spriteList.speed, `speed`, {
1649
+ skipTerms: g_settings.speedTerms, hiddenBtn: true, scLabel: g_lblNameObj.sc_speed, roundNum: 5,
1650
+ unitName: ` ${g_lblNameObj.multi}`, addRFunc: () => calculateTotalSpeed(),
1651
+ });
1652
+ if (g_headerObj.baseSpeed !== 1) {
1653
+ divRoot.appendChild(
1654
+ createDivCss2Label(`lblBaseSpd`, `Δv: ${Math.round(g_headerObj.baseSpeed * 100) / 100}x`, g_lblPosObj.lblBaseSpd)
1655
+ );
1656
+ }
1657
+
1658
+ /**
1659
+ * 譜面明細子画面・グラフの作成
1660
+ * @param {string} _name
1661
+ * @param {boolean} _graphUseFlg
1662
+ * @returns {HTMLDivElement}
1663
+ */
1664
+ const createScoreDetail = (_name, _graphUseFlg = true, _graphNum = 1) => {
1665
+ const detailObj = createEmptySprite(scoreDetail, `detail${_name}`, g_windowObj.detailObj);
1666
+
1667
+ if (_graphUseFlg) {
1668
+ for (let j = 0; j < _graphNum; j++) {
1669
+ const graphObj = document.createElement(`canvas`);
1670
+ const textBaseObj = document.getElementById(`lnkDifficulty`);
1671
+ const bkColor = window.getComputedStyle(textBaseObj, ``).backgroundColor;
1672
+
1673
+ graphObj.id = `graph${_name}${j > 0 ? j + 1 : ``}`;
1674
+ graphObj.width = g_limitObj.graphWidth * g_dpr;
1675
+ graphObj.height = g_limitObj.graphHeight * g_dpr;
1676
+ graphObj.style.width = wUnit(g_limitObj.graphWidth);
1677
+ graphObj.style.height = wUnit(g_limitObj.graphHeight);
1678
+ graphObj.style.left = wUnit(125);
1679
+ graphObj.style.top = wUnit(0);
1680
+ graphObj.style.position = `absolute`;
1681
+ graphObj.style.background = j === 0 ? bkColor : `#ffffff00`;
1682
+ const ctx = graphObj.getContext(`2d`);
1683
+ ctx.scale(g_dpr, g_dpr);
1684
+
1685
+ detailObj.appendChild(graphObj);
1686
+ }
1687
+ }
1688
+
1689
+ return detailObj;
1690
+ };
1691
+
1692
+ if (g_settings.scoreDetails.length > 0) {
1693
+ multiAppend(spriteList.speed,
1694
+ createCss2Button(`btnGraphB`, ``, () => true, {
1695
+ x: -25, y: -60, w: 0, h: 0, opacity: 0, resetFunc: () => setScoreDetail(true),
1696
+ }, g_cssObj.button_Mini),
1697
+ createCss2Button(`btnGraph`, `i`, () => true, {
1698
+ x: -25, y: -60, w: 30, h: 30, siz: g_limitObj.jdgCharaSiz, title: g_msgObj.graph,
1699
+ resetFunc: () => setScoreDetail(), cxtFunc: () => setScoreDetail(),
1700
+ }, g_cssObj.button_Mini)
1701
+ );
1702
+ g_stateObj.scoreDetailViewFlg = false;
1703
+ const scoreDetail = createEmptySprite(optionsprite, `scoreDetail`, g_windowObj.scoreDetail, g_cssObj.settings_DifSelector);
1704
+
1705
+ /**
1706
+ * 譜面明細表示の切替
1707
+ * @param {number} _val
1708
+ */
1709
+ const changeScoreDetail = (_val = 0) => {
1710
+ if (g_currentPage === `difSelector`) {
1711
+ resetDifWindow();
1712
+ }
1713
+ g_currentPage = `scoreDetail`;
1714
+ setShortcutEvent(g_currentPage, () => true, { displayFlg: false });
1715
+ g_stateObj.scoreDetailViewFlg = true;
1716
+ scoreDetail.style.visibility = `visible`;
1717
+ visibleSettingSummary(false);
1718
+
1719
+ // 表示内容を非表示化、ボタン色をデフォルトに戻す
1720
+ $id(`detail${g_stateObj.scoreDetail}`).visibility = `hidden`;
1721
+ document.getElementById(`lnk${g_stateObj.scoreDetail}G`).classList.replace(g_cssObj.button_Setting, g_cssObj.button_Default);
1722
+
1723
+ // 選択先を表示、ボタン色を選択中に変更
1724
+ // Qキーを押したときのリンク先を次の明細へ変更
1725
+ g_stateObj.scoreDetail = g_settings.scoreDetails[_val];
1726
+ [`option`, `difSelector`, `scoreDetail`].forEach(page => g_shortcutObj[page].KeyQ.id = g_settings.scoreDetailCursors[nextPos(_val, 1, g_settings.scoreDetailCursors.length)]);
1727
+ g_shortcutObj.scoreDetail.ArrowDown.id = g_settings.scoreDetailCursorsOrg[nextPos(_val, 1, g_settings.scoreDetailCursorsOrg.length)];
1728
+ g_shortcutObj.scoreDetail.ArrowUp.id = g_settings.scoreDetailCursorsOrg[nextPos(_val, -1, g_settings.scoreDetailCursorsOrg.length)];
1729
+
1730
+ $id(`detail${g_stateObj.scoreDetail}`).visibility = `visible`;
1731
+ document.getElementById(`lnk${g_stateObj.scoreDetail}G`).classList.replace(g_cssObj.button_Default, g_cssObj.button_Setting);
1732
+
1733
+ document.getElementById(`lnkMiniMapRev`).style.display =
1734
+ g_stateObj.scoreDetail === `MiniMap` && g_stateObj.scoreDetailViewFlg ? C_DIS_INHERIT : C_DIS_NONE;
1735
+ };
1736
+
1737
+ multiAppend(scoreDetail,
1738
+ createScoreDetail(`Speed`, true, 2),
1739
+ createScoreDetail(`Density`),
1740
+ createScoreDetail(`ToolDif`, false),
1741
+ createScoreDetail(`HighScore`, false),
1742
+ createScoreDetail(`MiniMap`, false),
1743
+ );
1744
+ g_settings.scoreDetails.forEach((sd, j) => {
1745
+ scoreDetail.appendChild(
1746
+ makeDifLblCssButton(`lnk${sd}G`, getStgDetailName(sd), j, () => changeScoreDetail(j), {
1747
+ w: g_limitObj.difCoverWidth, h: 20, title: g_msgObj[`s_${sd}`],
1748
+ btnStyle: (g_stateObj.scoreDetail === sd ? `Setting` : `Default`),
1749
+ })
1750
+ );
1751
+ createScText(document.getElementById(`lnk${sd}G`), `${sd}G`, { targetLabel: `lnk${sd}G`, x: -5 });
1752
+ });
1753
+ }
1754
+
1755
+ /**
1756
+ * 譜面明細表示/非表示ボタンの処理
1757
+ * @param {boolean} _resetFlg
1758
+ */
1759
+ const setScoreDetail = (_resetFlg = false) => {
1760
+ if (g_currentPage === `difSelector`) {
1761
+ resetDifWindow();
1762
+ g_stateObj.scoreDetailViewFlg = false;
1763
+ g_shortcutObj.difSelector.KeyQ.id = g_settings.scoreDetailCursors[0];
1764
+ }
1765
+ const scoreDetail = document.getElementById(`scoreDetail`);
1766
+ const detailObj = document.getElementById(`detail${g_stateObj.scoreDetail}`);
1767
+ const visibles = [`hidden`, `visible`];
1768
+
1769
+ g_stateObj.scoreDetailViewFlg = !g_stateObj.scoreDetailViewFlg;
1770
+ scoreDetail.style.visibility = visibles[Number(g_stateObj.scoreDetailViewFlg)];
1771
+ detailObj.style.visibility = visibles[Number(g_stateObj.scoreDetailViewFlg)];
1772
+ visibleSettingSummary(g_stateObj.scoreDetailViewFlg ? false : g_stateObj.settingSummaryVisible);
1773
+
1774
+ g_currentPage = g_stateObj.scoreDetailViewFlg ? `scoreDetail` : `option`;
1775
+ setShortcutEvent(g_currentPage, () => true, { displayFlg: false });
1776
+
1777
+ // Qキーのカーソル位置は、ハードリセットまたはスコア明細を閉じるときに先頭へ戻す
1778
+ if (_resetFlg || !g_stateObj.scoreDetailViewFlg) {
1779
+ [`option`, `difSelector`, `scoreDetail`].forEach(page => g_shortcutObj[page].KeyQ.id = g_settings.scoreDetailCursors[0]);
1780
+ }
1781
+ document.getElementById(`lnkMiniMapRev`).style.display =
1782
+ g_stateObj.scoreDetail === `MiniMap` && g_stateObj.scoreDetailViewFlg ? C_DIS_INHERIT : C_DIS_NONE;
1783
+ };
1784
+
1785
+ // ---------------------------------------------------
1786
+ // 速度モーション (Motion)
1787
+ // 縦位置: 3
1788
+ createGeneralSetting(spriteList.motion, `motion`);
1789
+
1790
+ // ---------------------------------------------------
1791
+ // リバース (Reverse) / スクロール (Scroll)
1792
+ // 縦位置: 4
1793
+ createGeneralSetting(spriteList.reverse, `reverse`, {
1794
+ addRFunc: () => {
1795
+ if (g_headerObj.scrollUse && g_settings.scrolls.length > 1) {
1796
+ setReverseView(document.getElementById(`btnReverse`));
1797
+ }
1798
+ }
1799
+ });
1800
+ if (g_headerObj.scrollUse) {
1801
+ createGeneralSetting(spriteList.scroll, `scroll`, { scLabel: g_lblNameObj.sc_scroll });
1802
+ [$id(`lnkScroll`).left, $id(`lnkScroll`).width] = [
1803
+ wUnit(parseFloat($id(`lnkScroll`).left) + 90), wUnit(parseFloat($id(`lnkScroll`).width) - 90)
1804
+ ];
1805
+
1806
+ spriteList.scroll.appendChild(
1807
+ createCss2Button(`btnReverse`, `${g_lblNameObj.Reverse}:${getStgDetailName(g_stateObj.reverse)}`, evt => setReverse(evt.target), {
1808
+ ...g_lblPosObj.btnReverse,
1809
+ cxtFunc: evt => setReverse(evt.target),
1810
+ }, g_cssObj.button_Default, g_cssObj[`button_Rev${g_stateObj.reverse}`])
1811
+ );
1812
+ spriteList[g_settings.scrolls.length > 1 ? `reverse` : `scroll`].style.display = C_DIS_NONE;
1813
+ } else {
1814
+ spriteList.scroll.style.pointerEvents = C_DIS_NONE;
1815
+ }
1816
+
1817
+ // ---------------------------------------------------
1818
+ // ミラー・ランダム (Shuffle)
1819
+ // 縦位置: 5.5
1820
+ createGeneralSetting(spriteList.shuffle, `shuffle`, g_settings.scoreDetails.length > 0 ? {
1821
+ addRFunc: () => makeHighScore(g_stateObj.scoreId),
1822
+ } : {});
1823
+
1824
+ // ---------------------------------------------------
1825
+ // 鑑賞モード設定 (AutoPlay)
1826
+ // 縦位置: 6.5
1827
+ createGeneralSetting(spriteList.autoPlay, `autoPlay`, g_settings.scoreDetails.length > 0 ? {
1828
+ addRFunc: () => makeHighScore(g_stateObj.scoreId),
1829
+ } : {});
1830
+
1831
+ // ---------------------------------------------------
1832
+ // ゲージ設定 (Gauge)
1833
+ // 縦位置: 7.5
1834
+ spriteList.gauge.appendChild(createLblSetting(`Gauge`));
1835
+
1836
+ // ゲージ設定詳細 縦位置: ゲージ設定+1
1837
+ spriteList.gauge.appendChild(createDivCss2Label(`lblGauge2`, ``, g_lblPosObj.lblGauge2));
1838
+
1839
+ if (g_headerObj.gaugeUse) {
1840
+ multiAppend(spriteList.gauge,
1841
+ makeSettingLblCssButton(`lnkGauge`, ``, 0, () => setGauge(1), { cxtFunc: () => setGauge(-1) }),
1842
+ makeMiniCssButton(`lnkGauge`, `R`, 0, () => setGauge(1)),
1843
+ makeMiniCssButton(`lnkGauge`, `L`, 0, () => setGauge(-1)),
1844
+ );
1845
+ createScText(spriteList.gauge, `Gauge`);
1846
+ } else {
1847
+ lblGauge.classList.add(g_cssObj.settings_Disabled);
1848
+ spriteList.gauge.appendChild(makeDisabledLabel(`lnkGauge`, 0, getStgDetailName(g_stateObj.gauge)));
1849
+ }
1850
+
1851
+ // 空押し判定設定 (Excessive)
1852
+ spriteList.gauge.appendChild(
1853
+ createDivCss2Label(`lblExcessive`, `${g_lblNameObj.Excessive}:${C_FLG_ON}`,
1854
+ g_lblPosObj.lblExcessive, g_cssObj[`button_Disabled${C_FLG_ON}`]
1855
+ )
1856
+ );
1857
+ spriteList.gauge.appendChild(
1858
+ createCss2Button(`lnkExcessive`, g_lblNameObj.Excessive, evt => setExcessive(evt.target), {
1859
+ ...g_lblPosObj.btnExcessive,
1860
+ title: g_msgObj.excessive, cxtFunc: evt => setExcessive(evt.target),
1861
+ }, g_cssObj.button_Default, g_cssObj[`button_Rev${g_stateObj.excessive}`])
1862
+ );
1863
+ createScText(lnkExcessive, `Excessive`, { x: -13, targetLabel: `lnkExcessive` });
1864
+
1865
+ // ---------------------------------------------------
1866
+ // タイミング調整 (Adjustment)
1867
+ // 縦位置: 10.5 短縮ショートカットあり
1868
+ createGeneralSetting(spriteList.adjustment, `adjustment`, {
1869
+ skipTerms: g_settings.adjustmentTerms, hiddenBtn: true, scLabel: g_lblNameObj.sc_adjustment, roundNum: 5,
1870
+ unitName: g_lblNameObj.frame, addRFunc: () => viewAdjustment(),
1871
+ });
1872
+
1873
+ const viewAdjustment = () => {
1874
+ if (g_headerObj.playbackRate !== 1) {
1875
+ const adjustmentVal = isLocalMusicFile(g_stateObj.scoreId)
1876
+ ? Math.round(g_stateObj.adjustment / g_headerObj.playbackRate)
1877
+ : (g_stateObj.adjustment / g_headerObj.playbackRate).toFixed(1);
1878
+ document.getElementById(`lnkAdjustment`).textContent = ``;
1879
+ if (document.getElementById(`lnkAdjustment1`) === null) {
1880
+ multiAppend(
1881
+ adjustmentSprite,
1882
+ createDivCss2Label(`lnkAdjustment1`, ``, g_lblPosObj.lnkAdjustment1),
1883
+ createDivCss2Label(`lnkAdjustment2`, ``, g_lblPosObj.lnkAdjustment2),
1884
+ );
1885
+ }
1886
+ document.getElementById(`lnkAdjustment1`).textContent = `${adjustmentVal}${g_lblNameObj.frame}`;
1887
+ document.getElementById(`lnkAdjustment2`).textContent = `(${g_stateObj.adjustment.toFixed(1)}${g_localStorage.adjustment === g_stateObj.adjustment ? '*' : ''})`;
1888
+ }
1889
+ };
1890
+ viewAdjustment();
1891
+
1892
+ // ---------------------------------------------------
1893
+ // フェードイン (Fadein)
1894
+ // 縦位置: 11.5 スライダーあり
1895
+ spriteList.fadein.appendChild(createLblSetting(`Fadein`));
1896
+
1897
+ const lnkFadein = createDivCss2Label(`lnkFadein`, `${g_stateObj.fadein}${g_lblNameObj.percent}`,
1898
+ g_lblPosObj.lnkFadein, g_cssObj.settings_FadeinBar);
1899
+ spriteList.fadein.appendChild(lnkFadein);
1900
+
1901
+ const setFadein = _sign => {
1902
+ g_stateObj.fadein = nextPos(g_stateObj.fadein, _sign, 100);
1903
+ fadeinSlider.value = g_stateObj.fadein;
1904
+ lnkFadein.textContent = `${g_stateObj.fadein}${g_lblNameObj.percent}`;
1905
+ updateSettingSummary();
1906
+ drawMinimap(g_stateObj.scoreId, { _fadeinFlg: true });
1907
+ };
1908
+
1909
+ multiAppend(spriteList.fadein,
1910
+
1911
+ // 右回し・左回しボタン
1912
+ makeMiniCssButton(`lnkFadein`, `R`, 0, () => setFadein(1)),
1913
+ makeMiniCssButton(`lnkFadein`, `L`, 0, () => setFadein(-1)),
1914
+
1915
+ // フェードインのスライダー処理
1916
+ createDivCss2Label(`lblFadeinBar`, `<input id="fadeinSlider" type="range" value="${g_stateObj.fadein}" min="0" max="99" step="1">`,
1917
+ g_lblPosObj.lblFadeinBar),
1918
+
1919
+ );
1920
+
1921
+ const fadeinSlider = document.getElementById(`fadeinSlider`);
1922
+ g_handler.addListener(fadeinSlider, `input`, () => {
1923
+ g_stateObj.fadein = inputSlider(fadeinSlider, lnkFadein, `fadein`);
1924
+ updateSettingSummary();
1925
+ drawMinimap(g_stateObj.scoreId, { _fadeinFlg: true });
1926
+ }, false);
1927
+
1928
+ // ---------------------------------------------------
1929
+ // ボリューム (Volume)
1930
+ // 縦位置: 12.5
1931
+ createGeneralSetting(spriteList.volume, `volume`, { unitName: g_lblNameObj.percent });
1932
+
1933
+ // 譜面番号の再取得
1934
+ g_stateObj.scoreId = getNextDifficulty(g_stateObj.scoreId, 0);
1935
+ const keyLists = makeDedupliArray(g_headerObj.viewLists.map(j => g_headerObj.keyLabels[j]));
1936
+ g_headerObj.viewKeyLists = keyLists.sort((a, b) => parseInt(a) - parseInt(b));
1937
+ g_headerObj.difSelectorUse = getDifSelectorUse(g_rootObj.difSelectorUse);
1938
+
1939
+ // 設定画面の一通りのオブジェクトを作成後に譜面・速度・ゲージ設定をまとめて行う
1940
+ setDifficulty(false);
1941
+ optionsprite.oncontextmenu = () => false;
1942
+ };
1943
+
1944
+ /**
1945
+ * 汎用設定
1946
+ * @param {HTMLDivElement} _obj
1947
+ * @param {string} _settingName
1948
+ * @param {string} [object.unitName=''] 設定名の単位
1949
+ * @param {number[]} [object.skipTerms] ボタンの設定スキップ間隔(デフォルト:[1(外側), 1(内側), 1(最内側)])
1950
+ * @param {boolean} [object.hiddenBtn=false] 隠しボタン(ショートカットキーのみ)の利用有無
1951
+ * @param {Function} [object.addRFunc] 右側のボタンを押したときの追加処理
1952
+ * @param {Function} [object.addLFunc] 左側のボタンを押したときの追加処理
1953
+ * @param {string} [object.settingLabel=_settingName] 設定名
1954
+ * @param {string} [object.displayName] 画面名
1955
+ * @param {string} [object.scLabel=''] ショートカットキーの表示名
1956
+ * @param {number} [object.roundNum=0] 設定スキップ間隔の丸め基準数
1957
+ * @param {number} [object.adjY=0] 設定ボタンのY座標位置
1958
+ */
1959
+ const createGeneralSetting = (_obj, _settingName, { unitName = ``,
1960
+ skipTerms = fillArray(3, 1), hiddenBtn = false, addRFunc = () => { }, addLFunc = addRFunc,
1961
+ settingLabel = _settingName, displayName = g_currentPage, scLabel = ``, roundNum = 0, adjY = 0 } = {}) => {
1962
+
1963
+ const settingUpper = toCapitalize(_settingName);
1964
+ const linkId = `lnk${settingUpper}`;
1965
+ const initName = `${getStgDetailName(g_stateObj[_settingName])}${unitName}`;
1966
+ _obj.appendChild(createLblSetting(settingUpper, adjY, toCapitalize(settingLabel)));
1967
+
1968
+ if (g_headerObj[`${_settingName}Use`] === undefined || g_headerObj[`${_settingName}Use`]) {
1969
+
1970
+ multiAppend(_obj,
1971
+ makeSettingLblCssButton(linkId, `${initName}${g_localStorage[_settingName] === g_stateObj[_settingName] ? ' *' : ''}`, 0,
1972
+ () => {
1973
+ setSetting(skipTerms[1], _settingName, { func: () => addRFunc(), unitName, roundNum });
1974
+ }, {
1975
+ cxtFunc: () => {
1976
+ setSetting(skipTerms[1] * (-1), _settingName, { func: () => addLFunc(), unitName, roundNum });
1977
+ }
1978
+ }),
1979
+
1980
+ // 右回し・左回しボタン(外側)
1981
+ makeMiniCssButton(linkId, `R`, 0, () =>
1982
+ setSetting(skipTerms[0], _settingName, { func: () => addRFunc(), unitName, roundNum })),
1983
+ makeMiniCssButton(linkId, `L`, 0, () =>
1984
+ setSetting(skipTerms[0] * (-1), _settingName, { func: () => addLFunc(), unitName, roundNum })),
1985
+ );
1986
+
1987
+ // 右回し・左回しボタン(内側)
1988
+ if (skipTerms[1] > 1) {
1989
+ multiAppend(_obj,
1990
+ makeMiniCssButton(linkId, `RR`, 0, () =>
1991
+ setSetting(skipTerms[1], _settingName, { func: () => addRFunc(), unitName, roundNum })),
1992
+ makeMiniCssButton(linkId, `LL`, 0, () =>
1993
+ setSetting(skipTerms[1] * (-1), _settingName, { func: () => addLFunc(), unitName, roundNum })),
1994
+ );
1995
+ }
1996
+
1997
+ // 右回し・左回しボタン(最内側)
1998
+ if (skipTerms[2] > 1) {
1999
+ multiAppend(_obj,
2000
+ makeMiniCssButton(linkId, `RRR`, 0, () =>
2001
+ setSetting(skipTerms[2], _settingName, { func: () => addRFunc(), unitName, roundNum })
2002
+ , { dw: -g_limitObj.setMiniWidth / 2 }),
2003
+ makeMiniCssButton(linkId, `LLL`, 0, () =>
2004
+ setSetting(skipTerms[2] * (-1), _settingName, { func: () => addLFunc(), unitName, roundNum })
2005
+ , { dw: -g_limitObj.setMiniWidth / 2 }),
2006
+ );
2007
+ }
2008
+
2009
+ // 右回し・左回しボタン(不可視)
2010
+ if (hiddenBtn) {
2011
+ multiAppend(_obj,
2012
+ makeMiniCssButton(linkId, `HR`, 0, () => setSetting(1, _settingName, { func: () => addRFunc(), unitName, roundNum }), { visibility: `hidden` }),
2013
+ makeMiniCssButton(linkId, `HL`, 0, () => setSetting(-1, _settingName, { func: () => addLFunc(), unitName, roundNum }), { visibility: `hidden` }),
2014
+ );
2015
+ }
2016
+
2017
+ // ショートカットキー表示
2018
+ createScText(_obj, settingUpper, { displayName: displayName, dfLabel: scLabel });
2019
+
2020
+ } else {
2021
+ document.getElementById(`lbl${settingUpper}`).classList.add(g_cssObj.settings_Disabled);
2022
+ _obj.appendChild(makeDisabledLabel(linkId, 0, initName));
2023
+ }
2024
+ };
2025
+
2026
+ /**
2027
+ * 設定画面用ラベルの作成
2028
+ * @param {string} _settingName
2029
+ * @param {number} _adjY
2030
+ * @param {string} _settingLabel
2031
+ * @returns {HTMLDivElement}
2032
+ */
2033
+ const createLblSetting = (_settingName, _adjY = 0, _settingLabel = _settingName) => {
2034
+ const lbl = createDivCss2Label(`lbl${_settingName}`, g_lblNameObj[_settingLabel], {
2035
+ x: -5, y: _adjY, w: 110, pointerEvents: C_DIS_AUTO,
2036
+ }, `settings_${_settingName}`);
2037
+ lbl.title = g_msgObj[`${_settingName.charAt(0).toLowerCase()}${_settingName.slice(1)}`];
2038
+ return lbl;
2039
+ };
2040
+
2041
+ /**
2042
+ * 設定名の置き換え処理(未指定時は原則g_lblNameObjの値を使用、明示的な指定がある場合のみ元の名前を返却)
2043
+ * @param {string} _name
2044
+ * @returns {string}
2045
+ */
2046
+ const getStgDetailName = _name => {
2047
+ return g_lblNameObj[`u_${_name}`] !== undefined && (
2048
+ g_presetObj.lblRenames === undefined ||
2049
+ g_presetObj.lblRenames[g_currentPage] === undefined ||
2050
+ g_presetObj.lblRenames[g_currentPage]
2051
+ ) ? g_lblNameObj[`u_${_name}`] : _name;
2052
+ };
2053
+
2054
+ /**
2055
+ * 設定メイン・汎用
2056
+ * @param {number} _scrollNum
2057
+ * @param {string} _settingName
2058
+ * @param {Function} [func=()=>true] 設定ボタンを押した後の追加処理
2059
+ * @param {string} [unitName=''] 設定の単位名
2060
+ * @param {number} [roundNum=0] 設定スキップ間隔の丸め基準数
2061
+ */
2062
+ const setSetting = (_scrollNum, _settingName, { func = () => true, unitName = ``, roundNum = 0, maxSiz = g_limitObj.setLblSiz } = {}) => {
2063
+ let settingNum = g_settings[`${_settingName}Num`];
2064
+ const settingList = g_settings[`${_settingName}s`];
2065
+ const settingMax = settingList.length - 1;
2066
+
2067
+ // _roundNum単位で丸める
2068
+ if (roundNum > 0 && _scrollNum >= roundNum) {
2069
+ settingNum = Math.floor(settingNum / roundNum) * roundNum;
2070
+ } else if (roundNum > 0 && -_scrollNum >= roundNum) {
2071
+ settingNum = Math.ceil(settingNum / roundNum) * roundNum;
2072
+ }
2073
+
2074
+ if (_scrollNum > 0) {
2075
+ settingNum = (settingNum === settingMax ? 0 : Math.min(settingNum + _scrollNum, settingMax));
2076
+ } else if (_scrollNum < 0) {
2077
+ settingNum = (settingNum === 0 ? settingMax : Math.max(settingNum + _scrollNum, 0));
2078
+ }
2079
+ g_stateObj[_settingName] = settingList[settingNum];
2080
+ g_settings[`${_settingName}Num`] = settingNum;
2081
+
2082
+ const settingName = `${getStgDetailName(g_stateObj[_settingName])}${unitName}${g_localStorage[_settingName] === g_stateObj[_settingName] ? ' *' : ''}`;
2083
+ const lnkName = `lnk${toCapitalize(_settingName)}`;
2084
+ document.getElementById(lnkName).textContent = settingName;
2085
+ $id(lnkName).fontSize = wUnit(getFontSize2(settingName, parseFloat($id(lnkName).width), { maxSiz }));
2086
+
2087
+ func();
2088
+
2089
+ if (document.getElementById(`settingSumSprite`) !== null) {
2090
+ updateSettingSummary();
2091
+ }
2092
+ };
2093
+
2094
+ /**
2095
+ * 無効化用ラベル作成
2096
+ * @param {string} _id
2097
+ * @param {number} _heightPos
2098
+ * @param {string} _defaultStr
2099
+ * @returns {HTMLDivElement}
2100
+ */
2101
+ const makeDisabledLabel = (_id, _heightPos, _defaultStr) =>
2102
+ createDivCss2Label(_id, _defaultStr, {
2103
+ x: g_limitObj.setLblLeft, y: g_limitObj.setLblHeight * _heightPos,
2104
+ }, g_cssObj.settings_Disabled);
2105
+
2106
+ /**
2107
+ * 保存済みリバース取得処理
2108
+ * @param {object} _localStorage 保存先のローカルストレージ名
2109
+ * @param {string} _extraKeyName 特殊キー名(通常キーは省略)
2110
+ */
2111
+ const getKeyReverse = (_localStorage, _extraKeyName = ``) => {
2112
+ if (_localStorage[`reverse${_extraKeyName}`] !== undefined && g_headerObj.reverseUse) {
2113
+ g_stateObj.reverse = _localStorage[`reverse${_extraKeyName}`] ?? C_FLG_OFF;
2114
+ g_settings.reverseNum = roundZero(g_settings.reverses.findIndex(reverse => reverse === g_stateObj.reverse));
2115
+ } else {
2116
+ setReverseDefault();
2117
+ }
2118
+ };
2119
+
2120
+ /**
2121
+ * リバースのデフォルト化処理
2122
+ */
2123
+ const setReverseDefault = () => {
2124
+ g_stateObj.reverse = C_FLG_OFF;
2125
+ g_settings.reverseNum = 0;
2126
+ };
2127
+
2128
+ const setReverse = _btn => {
2129
+ if (!g_settings.scrolls.includes(C_FLG_REVERSE) && g_headerObj.reverseUse) {
2130
+ g_settings.reverseNum = (g_settings.reverseNum + 1) % 2;
2131
+ g_stateObj.reverse = g_settings.reverses[g_settings.reverseNum];
2132
+ setReverseView(_btn);
2133
+ updateSettingSummary();
2134
+ }
2135
+ };
2136
+
2137
+ const setReverseView = _btn => {
2138
+ _btn.classList.replace(g_cssObj[`button_Rev${g_settings.reverses[(g_settings.reverseNum + 1) % 2]}`],
2139
+ g_cssObj[`button_Rev${g_settings.reverses[g_settings.reverseNum]}`]);
2140
+ if (!g_settings.scrolls.includes(C_FLG_REVERSE) && g_headerObj.reverseUse) {
2141
+ _btn.textContent = `${g_lblNameObj.Reverse}:${getStgDetailName(g_stateObj.reverse)}`;
2142
+ } else {
2143
+ _btn.textContent = `X`;
2144
+ setReverseDefault();
2145
+ }
2146
+ };
2147
+
2148
+ /**
2149
+ * ゲージ設定メイン
2150
+ * @param {number} _scrollNum
2151
+ * @param {boolean} _gaugeInitFlg
2152
+ */
2153
+ const setGauge = (_scrollNum, _gaugeInitFlg = false) => {
2154
+
2155
+ /**
2156
+ * 数式からゲージ値に変換
2157
+ * arrow[] -> 矢印数, frz[] -> フリーズアロー数, all[] -> 矢印+フリーズアロー数に置換する
2158
+ * @param {string} _val
2159
+ * @param {string} _defaultVal
2160
+ * @returns {number}
2161
+ */
2162
+ const getGaugeCalc = (_val, _defaultVal) => {
2163
+ return setVal(convertStrToVal(
2164
+ replaceStr(_val, g_escapeStr.gaugeParamName)?.split(`{0}`).join(g_stateObj.scoreId)
2165
+ ), _defaultVal, C_TYP_CALC);
2166
+ };
2167
+ /**
2168
+ * ゲージ詳細一括変更
2169
+ * @param {object} _baseObj
2170
+ * @param {number} object.magInit
2171
+ * @param {number} object.magRcv
2172
+ * @param {number} object.magDmg
2173
+ */
2174
+ const setLifeCategory = (_baseObj, { _magInit = 1, _magRcv = 1, _magDmg = 1 } = {}) => {
2175
+ g_stateObj.lifeInit = getGaugeCalc(_baseObj.lifeInits[g_stateObj.scoreId], g_stateObj.lifeInit) * _magInit;
2176
+ g_stateObj.lifeRcv = getGaugeCalc(_baseObj.lifeRecoverys[g_stateObj.scoreId], g_stateObj.lifeRcv) * _magRcv;
2177
+ g_stateObj.lifeDmg = getGaugeCalc(_baseObj.lifeDamages[g_stateObj.scoreId], g_stateObj.lifeDmg) * _magDmg;
2178
+ };
2179
+
2180
+ /**
2181
+ * ライフモード切替
2182
+ * @param {object} _baseObj
2183
+ */
2184
+ const changeLifeMode = (_baseObj) => {
2185
+ if (_baseObj.lifeBorders[g_stateObj.scoreId] === `x`) {
2186
+ g_stateObj.lifeBorder = 0;
2187
+ g_stateObj.lifeMode = C_LFE_SURVIVAL;
2188
+ } else {
2189
+ g_stateObj.lifeBorder = getGaugeCalc(_baseObj.lifeBorders[g_stateObj.scoreId], g_stateObj.lifeBorder);
2190
+ g_stateObj.lifeMode = C_LFE_BORDER;
2191
+ }
2192
+ };
2193
+
2194
+ // ゲージ初期化
2195
+
2196
+ // カスタムゲージの設定取得
2197
+ const defaultCustomGauge = g_gaugeOptionObj.custom0 || g_gaugeOptionObj.customDefault;
2198
+ if (hasVal(defaultCustomGauge)) {
2199
+ g_gaugeOptionObj.custom = (g_gaugeOptionObj[`custom${g_stateObj.scoreId}`] || defaultCustomGauge).concat();
2200
+ g_gaugeOptionObj.varCustom = (g_gaugeOptionObj[`varCustom${g_stateObj.scoreId}`] || g_gaugeOptionObj.varCustom0 || g_gaugeOptionObj.varCustomDefault).concat();
2201
+ }
2202
+
2203
+ // ゲージタイプの設定
2204
+ changeLifeMode(g_headerObj);
2205
+ g_gaugeType = (g_gaugeOptionObj.custom.length > 0 ? C_LFE_CUSTOM : g_stateObj.lifeMode);
2206
+
2207
+ // ゲージ配列を入れ替え
2208
+ g_settings.gauges = structuredClone(g_gaugeOptionObj[g_gaugeType.toLowerCase()]);
2209
+ g_settings.gaugeNum = getCurrentNo(g_settings.gauges, g_stateObj.gauge);
2210
+ g_stateObj.gauge = g_settings.gauges[g_settings.gaugeNum];
2211
+
2212
+ setSetting(_scrollNum, `gauge`);
2213
+ g_stateObj.lifeVariable = g_gaugeOptionObj[`var${g_gaugeType}`][g_settings.gaugeNum];
2214
+
2215
+ // デフォルトゲージの設定を適用(g_gaugeOptionObjから取得)
2216
+ if (g_settings.gaugeNum !== 0 &&
2217
+ (g_gaugeOptionObj.custom.length === 0 ||
2218
+ g_gaugeOptionObj.defaultList.includes(g_gaugeOptionObj[`defaultGauge${g_stateObj.scoreId}`]))) {
2219
+
2220
+ const gType = (g_gaugeType === C_LFE_CUSTOM ?
2221
+ toCapitalize(g_gaugeOptionObj[`defaultGauge${g_stateObj.scoreId}`]) : g_gaugeType);
2222
+ const getGaugeVal = _type => g_gaugeOptionObj[`${_type}${gType}`][g_settings.gaugeNum];
2223
+ g_stateObj.lifeMode = getGaugeVal(`type`);
2224
+ g_stateObj.lifeBorder = getGaugeVal(`clear`);
2225
+ g_stateObj.lifeInit = getGaugeVal(`init`);
2226
+ g_stateObj.lifeRcv = getGaugeVal(`rcv`);
2227
+ g_stateObj.lifeDmg = getGaugeVal(`dmg`);
2228
+ }
2229
+
2230
+ // デフォルトゲージの初期設定(Light, Easyでは回復量を2倍にする)
2231
+ if ([`Original`, `Light`, `Normal`, `Easy`].includes(g_stateObj.gauge)) {
2232
+ setLifeCategory(g_headerObj, { _magRcv: [`Light`, `Easy`].includes(g_stateObj.gauge) ? 2 : 1 });
2233
+ }
2234
+
2235
+ // ゲージ設定別に個別設定した場合はここで設定を上書き
2236
+ // 譜面ヘッダー:gaugeXXX で設定した値がここで適用される
2237
+ if (hasVal(g_gaugeOptionObj[`gauge${g_stateObj.gauge}s`])) {
2238
+ const tmpGaugeObj = g_gaugeOptionObj[`gauge${g_stateObj.gauge}s`];
2239
+ if (hasVal(tmpGaugeObj.lifeBorders[g_stateObj.scoreId])) {
2240
+ changeLifeMode(tmpGaugeObj);
2241
+ }
2242
+ setLifeCategory(tmpGaugeObj);
2243
+ }
2244
+
2245
+ // ゲージ詳細情報を表示
2246
+ lblGauge2.innerHTML = gaugeFormat(g_stateObj.lifeMode,
2247
+ g_stateObj.lifeBorder, g_stateObj.lifeRcv, g_stateObj.lifeDmg, g_stateObj.lifeInit, g_stateObj.lifeVariable);
2248
+ };
2249
+
2250
+ /**
2251
+ * ゲージ設定の詳細表示を整形
2252
+ * @param {string} _mode
2253
+ * @param {number} _border
2254
+ * @param {number} _rcv
2255
+ * @param {number} _dmg
2256
+ * @param {number} _init
2257
+ * @param {string} _lifeValFlg
2258
+ * @returns {string}
2259
+ */
2260
+ const gaugeFormat = (_mode, _border, _rcv, _dmg, _init, _lifeValFlg) => {
2261
+ const initVal = g_headerObj.maxLifeVal * _init / 100;
2262
+ const borderVal = g_headerObj.maxLifeVal * _border / 100;
2263
+
2264
+ // 整形用にライフ初期値を整数、回復・ダメージ量を小数第2位に丸める
2265
+ const init = Math.round(initVal);
2266
+ const borderText = (_mode === C_LFE_BORDER && _border !== 0 ? Math.round(borderVal) : `-`);
2267
+ const round2 = _val => Math.round(_val * 100) / 100;
2268
+
2269
+ let rcvText = round2(_rcv), dmgText = round2(_dmg);
2270
+ let realRcv = _rcv, realDmg = _dmg;
2271
+ const allCnt = sumData(g_detailObj.arrowCnt[g_stateObj.scoreId]) +
2272
+ (g_headerObj.frzStartjdgUse ? 2 : 1) * sumData(g_detailObj.frzCnt[g_stateObj.scoreId]);
2273
+
2274
+ // ゲージ設定が矢印数依存の場合、実際の値に変換して表示する
2275
+ // 表示上、計算した値は小数第二位までの表示とする(それ以外はそのまま)
2276
+ if (_lifeValFlg === C_FLG_ON) {
2277
+ rcvText = ``, dmgText = ``;
2278
+ if (allCnt > 0) {
2279
+ realRcv = Math.min(calcLifeVal(_rcv, allCnt), g_headerObj.maxLifeVal);
2280
+ realDmg = Math.min(calcLifeVal(_dmg, allCnt), g_headerObj.maxLifeVal);
2281
+ rcvText = `${realRcv.toFixed(2)}<br>`;
2282
+ dmgText = `${realDmg.toFixed(2)}<br>`;
2283
+ }
2284
+ rcvText += `<span class="settings_lifeVal">(${round2(_rcv)})</span>`;
2285
+ dmgText += `<span class="settings_lifeVal">(${round2(_dmg)})</span>`;
2286
+ }
2287
+
2288
+ // 達成率(Accuracy)・許容ミス数の計算
2289
+ const [rateText, allowableCntsText] = getAccuracy(borderVal, realRcv, realDmg, initVal, allCnt);
2290
+ g_workObj.requiredAccuracy = rateText;
2291
+
2292
+ // このテーブルのみpointer-eventsを有効にする(オンマウス許可)
2293
+ return `<div id="gaugeDivCover" class="settings_gaugeDivCover" style="pointer-events: auto;">
2294
+ <div id="lblGaugeDivTable" class="settings_gaugeDivTable">
2295
+ <div id="lblGaugeStart" class="settings_gaugeDivTableCol settings_gaugeStart">
2296
+ ${g_lblNameObj.g_start}
2297
+ </div>
2298
+ <div id="lblGaugeBorder" class="settings_gaugeDivTableCol settings_gaugeEtc">
2299
+ ${g_lblNameObj.g_border}
2300
+ </div>
2301
+ <div id="lblGaugeRecovery" class="settings_gaugeDivTableCol settings_gaugeEtc">
2302
+ ${g_lblNameObj.g_recovery}
2303
+ </div>
2304
+ <div id="lblGaugeDamage" class="settings_gaugeDivTableCol settings_gaugeEtc">
2305
+ ${g_lblNameObj.g_damage}
2306
+ </div>
2307
+ <div id="lblGaugeRate" class="settings_gaugeDivTableCol settings_gaugeEtc">
2308
+ ${g_lblNameObj.g_rate}
2309
+ </div>
2310
+ </div>
2311
+ <div id="dataGaugeDivTable" class="settings_gaugeDivTable">
2312
+ <div id="dataGaugeStart" class="settings_gaugeDivTableCol settings_gaugeVal settings_gaugeStart">
2313
+ ${init}/${g_headerObj.maxLifeVal}
2314
+ </div>
2315
+ <div id="dataGaugeBorder" class="settings_gaugeDivTableCol settings_gaugeVal settings_gaugeEtc">
2316
+ ${borderText}
2317
+ </div>
2318
+ <div id="dataGaugeRecovery" class="settings_gaugeDivTableCol settings_gaugeVal settings_gaugeEtc">
2319
+ ${rcvText}
2320
+ </div>
2321
+ <div id="dataGaugeDamage" class="settings_gaugeDivTableCol settings_gaugeVal settings_gaugeEtc">
2322
+ ${dmgText}
2323
+ </div>
2324
+ <div id="dataGaugeRate" class="settings_gaugeDivTableCol settings_gaugeVal settings_gaugeEtc" style="line-height: 12px;">
2325
+ ${rateText}<br><span style="font-size: 10px;">${allowableCntsText}</span>
2326
+ </div>
2327
+ </div>
2328
+ </div>
2329
+ `;
2330
+ };
2331
+
2332
+ /**
2333
+ * 達成率、許容ミス数の取得
2334
+ * @param {number} _border
2335
+ * @param {number} _rcv
2336
+ * @param {number} _dmg
2337
+ * @param {number} _init
2338
+ * @param {number} _allCnt
2339
+ * @returns {string[]}
2340
+ */
2341
+ const getAccuracy = (_border, _rcv, _dmg, _init, _allCnt) => {
2342
+ const justPoint = _rcv + _dmg > 0 ? Math.max(_border - _init + _dmg * _allCnt, 0) / (_rcv + _dmg) : 0;
2343
+ const minRecovery = (_border === 0 ? Math.floor(justPoint + 1) : Math.ceil(justPoint));
2344
+ const rate = Math.max(minRecovery / _allCnt * 100, 0);
2345
+ let rateText = _allCnt > 0 ? (rate <= 100 ? `${rate.toFixed(2)}%` : `<span class="settings_lifeVal">${rate.toFixed(2)}%</span>`) : `----`;
2346
+
2347
+ // 許容ミス数の計算
2348
+ const allowableCnts = Math.min(_allCnt - minRecovery, _allCnt);
2349
+ let allowableCntsText = _allCnt > 0 && allowableCnts !== 0 ? (allowableCnts > 0 ? `${allowableCnts}miss↓` : `(${allowableCnts}miss)`) : ``;
2350
+
2351
+ if ((_rcv === 0 && _dmg === 0) || _rcv < 0 || _dmg < 0) {
2352
+ rateText = `----`;
2353
+ allowableCntsText = ``;
2354
+ }
2355
+ return [rateText, allowableCntsText];
2356
+ };
2357
+
2358
+ /**
2359
+ * 空押し判定の設定
2360
+ * @param {HTMLDivElement} _btn
2361
+ */
2362
+ const setExcessive = (_btn, _val) => {
2363
+ const curExcessive = Number(g_settings.excessiveNum);
2364
+ g_settings.excessiveNum = _val !== undefined ? Number(_val) : (curExcessive + 1) % 2;
2365
+ g_stateObj.excessiveChgFlg = _val === undefined;
2366
+ if (g_stateObj.excessiveChgFlg) {
2367
+ g_stateObj.excessiveScoreId = g_stateObj.scoreId;
2368
+ }
2369
+ g_stateObj.excessive = g_settings.excessives[g_settings.excessiveNum];
2370
+ updateSettingSummary();
2371
+ _btn.classList.replace(g_cssObj[`button_Rev${g_settings.excessives[curExcessive]}`],
2372
+ g_cssObj[`button_Rev${g_settings.excessives[g_settings.excessiveNum]}`]);
2373
+ };
2374
+
2375
+ /**
2376
+ * 保存済みキーコンフィグ取得処理
2377
+ * @param {object} _localStorage 保存先のローカルストレージ名
2378
+ * @param {string} _extraKeyName 特殊キー名(通常キーは省略)
2379
+ */
2380
+ const getKeyCtrl = (_localStorage, _extraKeyName = ``) => {
2381
+ g_keyObj.storagePtn = _localStorage[`keyCtrlPtn${_extraKeyName}`];
2382
+ const basePtn = `${g_keyObj.currentKey}_${g_keyObj.storagePtn}`;
2383
+ if (g_keyObj[`keyCtrl${basePtn}`] === undefined || hasVal(g_keyObj[`transKey${basePtn}`])) {
2384
+ return;
2385
+ }
2386
+ const baseKeyNum = g_keyObj[`${g_keyObj.defaultProp}${basePtn}`].length;
2387
+
2388
+ if (_localStorage[`keyCtrl${_extraKeyName}`]?.[0].length > 0) {
2389
+ const prevPtn = g_keyObj.currentPtn;
2390
+ g_keyObj.currentPtn = -1;
2391
+ const copyPtn = `${g_keyObj.currentKey}_-1`;
2392
+ g_keyObj[`keyCtrl${copyPtn}`] = [...Array(baseKeyNum)].map(() => []);
2393
+ g_keyObj[`keyCtrl${copyPtn}d`] = [...Array(baseKeyNum)].map(() => []);
2394
+
2395
+ for (let j = 0; j < baseKeyNum; j++) {
2396
+ for (let k = 0; k < g_keyObj[`keyCtrl${basePtn}`][j].length; k++) {
2397
+ g_keyObj[`keyCtrl${copyPtn}d`][j][k] = g_keyObj[`keyCtrl${copyPtn}`][j][k] = _localStorage[`keyCtrl${_extraKeyName}`][j][k];
2398
+ }
2399
+ }
2400
+
2401
+ const isUpdate = prevPtn !== -1 && g_keyObj.prevKey !== g_keyObj.currentKey;
2402
+ g_keyCopyLists.multiple.filter(header => g_keyObj[`${header}${basePtn}`] !== undefined && isUpdate)
2403
+ .forEach(header => g_keyObj[`${header}${copyPtn}`] = structuredClone(g_keyObj[`${header}${basePtn}`]));
2404
+ g_keyCopyLists.simple.filter(header => g_keyObj[`${header}${basePtn}`] !== undefined && isUpdate)
2405
+ .forEach(header => g_keyObj[`${header}${copyPtn}`] = g_keyObj[`${header}${basePtn}`]);
2406
+
2407
+ g_keycons.groups.forEach(type => {
2408
+ let maxPtn = 0;
2409
+ while (g_keyObj[`${type}${basePtn}_${maxPtn}`] !== undefined) {
2410
+ maxPtn++;
2411
+ }
2412
+ for (let j = 0; j < maxPtn; j++) {
2413
+ g_keyObj[`${type}${copyPtn}_${j}`] = structuredClone(g_keyObj[`${type}${basePtn}_${j}`]);
2414
+ }
2415
+ g_keyObj[`${type}${copyPtn}_0d`] = structuredClone(g_keyObj[`${type}${copyPtn}_0`]);
2416
+ });
2417
+ }
2418
+ };
2419
+
2420
+ /**
2421
+ * 設定・オプション表示用ボタン
2422
+ * @param {string} _id
2423
+ * @param {string} _name 初期設定文字
2424
+ * @param {number} _heightPos 上からの配置順
2425
+ * @param {Function} _func 通常ボタン処理
2426
+ * @param {number} [object.x]
2427
+ * @param {number} [object.y]
2428
+ * @param {number} [object.w]
2429
+ * @param {number} [object.h]
2430
+ * @param {number} [object.siz]
2431
+ * @param {Function} [object.cxtFunc] 右クリック時の処理
2432
+ * @param {...any} [object.rest]
2433
+ * @param {...any} _classes 追加するクラス
2434
+ * @returns {HTMLDivElement}
2435
+ */
2436
+ const makeSettingLblCssButton = (_id, _name, _heightPos, _func, {
2437
+ x = g_limitObj.setLblLeft, y = g_limitObj.setLblHeight * _heightPos,
2438
+ w = g_limitObj.setLblWidth, h = g_limitObj.setLblHeight, siz = g_limitObj.setLblSiz,
2439
+ cxtFunc = () => true, ...rest } = {}, ..._classes) =>
2440
+ createCss2Button(_id, _name, _func, { x, y, w, h, siz: getFontSize2(_name, w, { maxSiz: siz }), cxtFunc, ...rest }, g_cssObj.button_Default, ..._classes);
2441
+
2442
+ /**
2443
+ * 譜面変更セレクター用ボタン
2444
+ * @param {string} _id
2445
+ * @param {string} _name 初期設定文字
2446
+ * @param {number} _heightPos 上からの配置順
2447
+ * @param {Function} _func
2448
+ * @param {number} [object.x]
2449
+ * @param {number} [object.h]
2450
+ * @param {number} [object.y=h*_heightPos]
2451
+ * @param {number} [object.w]
2452
+ * @param {number} [object.siz]
2453
+ * @param {string} [object.btnStyle='Default']
2454
+ * @returns {HTMLDivElement}
2455
+ */
2456
+ const makeDifLblCssButton = (_id, _name, _heightPos, _func, {
2457
+ x = 0, h = g_limitObj.setLblHeight, y = h * _heightPos,
2458
+ w = g_limitObj.difSelectorWidth, siz = g_limitObj.difSelectorSiz, btnStyle = `Default` } = {}) =>
2459
+ createCss2Button(_id, _name, _func, {
2460
+ x, y, w, h, siz, borderStyle: `solid`, title: g_msgObj[_id] ?? ``,
2461
+ }, g_cssObj[`button_${btnStyle}`], g_cssObj.button_ON);
2462
+
2463
+ /**
2464
+ * 設定・オプション用の設定変更ミニボタン
2465
+ * @param {string} _id
2466
+ * @param {string} _directionFlg 表示用ボタンのどちら側に置くかを設定。(R, RR:右、L, LL:左)
2467
+ * @param {number} _heightPos 上からの配置順
2468
+ * @param {Function} _func
2469
+ * @param {number} [object.dx=0]
2470
+ * @param {number} [object.dy=0]
2471
+ * @param {number} [object.dw=0]
2472
+ * @param {number} [object.dh=0]
2473
+ * @param {number} [object.dsiz=0]
2474
+ * @param {string} [object.visibility='visible']
2475
+ * @returns {HTMLDivElement}
2476
+ */
2477
+ const makeMiniCssButton = (_id, _directionFlg, _heightPos, _func, { dx = 0, dy = 0, dw = 0, dh = 0, dsiz = 0, visibility = `visible` } = {}) =>
2478
+ createCss2Button(`${_id}${_directionFlg}`, g_settingBtnObj.chara[_directionFlg], _func, {
2479
+ x: g_settingBtnObj.pos[_directionFlg] + dx, y: g_limitObj.setLblHeight * _heightPos + dy,
2480
+ w: g_limitObj.setMiniWidth + dw, h: g_limitObj.setLblHeight + dh, siz: g_limitObj.setLblSiz + dsiz, visibility,
2481
+ }, g_cssObj.button_Mini);
2482
+
2483
+ /*-----------------------------------------------------------*/
2484
+ /* Scene : SETTINGS-DISPLAY [lemon] */
2485
+ /*-----------------------------------------------------------*/
2486
+
2487
+ const settingsDisplayInit = () => {
2488
+
2489
+ clearWindow();
2490
+ const divRoot = document.getElementById(`divRoot`);
2491
+ g_currentPage = `settingsDisplay`;
2492
+
2493
+ // 譜面初期情報ロード許可フラグ
2494
+ g_canLoadDifInfoFlg = false;
2495
+
2496
+ // タイトル文字描画
2497
+ divRoot.appendChild(getTitleDivLabel(`lblTitle`, g_lblNameObj.display, 0, 15, `settings_Display`));
2498
+
2499
+ // オプションボタン用の設置
2500
+ createSettingsDisplayWindow(divRoot);
2501
+
2502
+ // ショートカットキーメッセージ
2503
+ multiAppend(divRoot,
2504
+ createDescDiv(`scMsg`, g_lblNameObj.sdShortcutDesc),
2505
+ createCss2Button(`btnDisplayPreview`, `↓ Preview`, _evt => {
2506
+ toggleDisplayPreview();
2507
+ }, g_lblPosObj.btnDisplayPreview, g_cssObj.button_Setting),
2508
+ );
2509
+ createScText(btnDisplayPreview, `DisplayPreview`, { displayName: `settingsDisplay`, targetLabel: `btnDisplayPreview`, x: -25 });
2510
+
2511
+ // ユーザカスタムイベント(初期)
2512
+ safeExecuteCustomHooks(`g_customJsObj.settingsDisplay`, g_customJsObj.settingsDisplay);
2513
+
2514
+ // ボタン描画
2515
+ commonSettingBtn(`Settings`);
2516
+
2517
+ // キー操作イベント(デフォルト)
2518
+ setShortcutEvent(g_currentPage);
2519
+ document.oncontextmenu = () => true;
2520
+
2521
+ safeExecuteCustomHooks(`g_skinJsObj.settingsDisplay`, g_skinJsObj.settingsDisplay);
2522
+ };
2523
+
2524
+ /** プレビューウィンドウのルートdiv */
2525
+ let g_previewRoot = null;
2526
+
2527
+ /** プレビュー内の各UIオブジェクトの現在座標 */
2528
+ const g_previewPos = {
2529
+ arrowJdg: { x: null, y: null }, // 通常判定キャラクタ・コンボ
2530
+ frzJdg: { x: null, y: null }, // フリーズ判定キャラクタ・コンボ
2531
+ shortcut: { x: null, y: null },
2532
+ };
2533
+
2534
+ /**
2535
+ * プレビューのトグル(表示 / 非表示)
2536
+ */
2537
+ const toggleDisplayPreview = () => {
2538
+ if (g_previewRoot && document.getElementById(`displayPreviewOverlay`)) {
2539
+ closeDisplayPreview();
2540
+ g_currentPage = `settingsDisplay`;
2541
+ } else {
2542
+ openDisplayPreview();
2543
+ g_currentPage = `displayPreview`;
2544
+ }
2545
+ setShortcutEvent(g_currentPage, () => true, { displayFlg: false });
2546
+ };
2547
+
2548
+ /**
2549
+ * プレビューオーバーレイを開く
2550
+ */
2551
+ const openDisplayPreview = () => {
2552
+ const divRoot = document.getElementById(`divRoot`);
2553
+ if (!divRoot) return;
2554
+
2555
+ // 既存があれば削除
2556
+ closeDisplayPreview();
2557
+
2558
+ // ============================================================
2559
+ // オーバーレイ本体
2560
+ // ============================================================
2561
+ const overlay = createEmptySprite(divRoot, `displayPreviewOverlay`, {
2562
+ w: g_sWidth, h: g_sHeight, background: g_headerObj.baseBrightFlg ? `#eeeeeeee` : `#111111dd`, pointerEvents: C_DIS_AUTO,
2563
+ });
2564
+ g_previewRoot = overlay;
2565
+ multiAppend(overlay,
2566
+ createDivCss2Label(`lblDisplayPreview`, `Display Preview`,
2567
+ g_lblPosObj.lblDisplayPreview, g_cssObj.settings_Display),
2568
+ createCss2Button(`btnDisplayPreview2`, `↑ Preview`, _evt => {
2569
+ toggleDisplayPreview();
2570
+ }, g_lblPosObj.btnDisplayPreview, g_cssObj.button_Setting),
2571
+ createCss2Button(`btnDisplayReset`, `Reset`, _evt => {
2572
+ if (window.confirm(g_msgObj.displayPreviewResetConfirm)) {
2573
+ Object.assign(g_diffObj, g_diffInitObj);
2574
+ Object.keys(g_diffInitObj).forEach(key => {
2575
+ g_localStorage[key] = g_diffInitObj[key];
2576
+ });
2577
+ Object.keys(g_previewPos).forEach(key => g_previewPos[key] = { x: null, y: null });
2578
+ closeDisplayPreview();
2579
+ openDisplayPreview();
2580
+ }
2581
+ }, g_lblPosObj.btnDisplayReset, g_cssObj.button_Reset),
2582
+ createDescDiv(`lblDisplayPreviewMsg`, g_lblNameObj.displayPreviewDesc),
2583
+ );
2584
+ if (g_headerObj.scAreaWidth > 0) {
2585
+ overlay.appendChild(createDescDiv(`lblDisplayPreviewMsg2`, g_lblNameObj.displayPreviewDesc2));
2586
+ }
2587
+ createScText(btnDisplayReset, `DisplayPreviewReset`, { displayName: `displayPreview`, targetLabel: `btnDisplayReset`, x: -15 });
2588
+
2589
+ // ============================================================
2590
+ // プレイ画面フレーム(白枠)
2591
+ // ============================================================
2592
+ const rate = 0.8;
2593
+ const playW = g_headerObj.playingWidth || g_sWidth;
2594
+ const playH = g_headerObj.playingHeight || g_sHeight;
2595
+ const frameX = Math.round((g_sWidth - playW) / 2);
2596
+ const frameY = Math.round((g_sHeight - playH) / 2);
2597
+
2598
+ const frame = createEmptySprite(overlay, `previewFrame`, {
2599
+ x: frameX, y: frameY, w: playW, h: playH,
2600
+ background: g_headerObj.baseBrightFlg ? `#eeeeee` : `#111111`,
2601
+ border: `1px solid #444444`,
2602
+ boxSizing: `border-box`,
2603
+ transform: `scale(${rate})`,
2604
+ });
2605
+
2606
+ // ============================================================
2607
+ // Display設定に応じたUIを描画
2608
+ // ============================================================
2609
+ buildPreviewUI(frame, playW, playH);
2610
+ };
2611
+
2612
+ /**
2613
+ * プレビューオーバーレイを閉じる
2614
+ */
2615
+ const closeDisplayPreview = () => {
2616
+ const overlay = document.getElementById(`displayPreviewOverlay`);
2617
+ if (overlay) {
2618
+ deleteChildspriteAll(`displayPreviewOverlay`);
2619
+ overlay.remove();
2620
+ }
2621
+ g_previewRoot = null;
2622
+ };
2623
+
2624
+ /**
2625
+ * プレビュー内のUI要素を構築する
2626
+ * @param {HTMLElement} _frame プレイ画面フレーム要素
2627
+ * @param {number} _playW プレイ幅(px)
2628
+ * @param {number} _playH プレイ高さ(px)
2629
+ */
2630
+ const buildPreviewUI = (_frame, _playW, _playH) => {
2631
+
2632
+ // --- Display設定の現在値を取得 ---
2633
+ const d = {
2634
+ stepzone: g_stateObj.d_stepzone,
2635
+ judgment: g_stateObj.d_judgment,
2636
+ lifegauge: g_stateObj.d_lifegauge,
2637
+ score: g_stateObj.d_score,
2638
+ musicinfo: g_stateObj.d_musicinfo,
2639
+ };
2640
+
2641
+ const disableBox = (_name, { x, y, w, h } = {}) =>
2642
+ createDivCss2Label(`previewDisableBox_${_name}`, `${g_emojiObj.crossMark} ${_name}`, {
2643
+ x, y, w, h, background: `rgba(0,0,0,0.5)`, border: `1px dashed #555555`,
2644
+ size: 32, color: `#cccccc`, align: C_ALIGN_LEFT,
2645
+ });
2646
+
2647
+ // ============================================================
2648
+ // ステップゾーン(中央横帯)
2649
+ // ============================================================
2650
+ const stepY = g_posObj.stepY ?? C_STEP_Y;
2651
+ const revStepY = g_posObj.reverseStepY;
2652
+ const hitPos = g_stateObj.hitPosition ?? 0;
2653
+
2654
+ // 簡易ステップゾーン(7レーン分)
2655
+ const laneCount = 7;
2656
+ const laneW = 50;
2657
+ const totalW = laneCount * laneW;
2658
+ const startX = Math.round((_playW - totalW) / 2);
2659
+
2660
+ if (d.stepzone === C_FLG_OFF) {
2661
+ multiAppend(_frame,
2662
+ disableBox(`StepZone`, { x: Math.round(_playW / 2 - 200), y: stepY, w: 400, h: 50 }),
2663
+ disableBox(`StepZone_Rev`, { x: Math.round(_playW / 2 - 200), y: C_STEP_Y + revStepY, w: 400, h: 50 }),
2664
+ );
2665
+ } else {
2666
+ for (let j = 0; j < laneCount; j++) {
2667
+ createEmptySprite(_frame, `previewStep${j}`, {
2668
+ x: startX + j * laneW + 2, y: stepY + 2, w: laneW - 4, h: laneW - 4,
2669
+ background: `rgba(100,100,200,0.25)`,
2670
+ border: `1px solid rgba(150,150,255,0.5)`,
2671
+ });
2672
+ createEmptySprite(_frame, `previewStepR${j}`, {
2673
+ x: startX + j * laneW + 2, y: C_STEP_Y + revStepY + 2, w: laneW - 4, h: laneW - 4,
2674
+ background: `rgba(200,100,100,0.20)`,
2675
+ border: `1px solid rgba(255,150,150,0.4)`,
2676
+ });
2677
+ }
2678
+ }
2679
+
2680
+ // ============================================================
2681
+ // HitPosition を視覚化する判定基準ライン
2682
+ // ============================================================
2683
+ // 通常譜面用の判定ライン(赤または目立つ色で、レーン幅全体をカバー)
2684
+ // 上から下に流れる場合、hitPosがプラスなら「ステップゾーンより下」にラインが来る
2685
+ const lineNormal = createEmptySprite(_frame, `previewHitPosLine`, {
2686
+ x: startX,
2687
+ y: stepY + Math.round(laneW / 2) + hitPos, // ステップゾーンの中心 + hitPos
2688
+ w: totalW,
2689
+ h: 2, // 2pxの横線
2690
+ background: `#33aaff`,
2691
+ boxShadow: `0 0 4px #33aaff`, // ネオンっぽく光らせて目立たせる
2692
+ });
2693
+
2694
+ // 青いラインの右端(totalW から10pxほど外側)に数値を表示
2695
+ multiAppend(
2696
+ lineNormal,
2697
+ createDivCss2Label(`previewHitPosTitle`, `Hit`, {
2698
+ ...g_lblPosObj.previewHitPosText, x: -60, y: -14, align: C_ALIGN_RIGHT,
2699
+ }),
2700
+ createDivCss2Label(`previewHitPosTitle2`, `Position`, {
2701
+ ...g_lblPosObj.previewHitPosText, x: -60, y: -2, align: C_ALIGN_RIGHT,
2702
+ }),
2703
+ createDivCss2Label(`previewHitPosText`, `${hitPos > 0 ? '+' : ''}${hitPos}px↑↓`, {
2704
+ ...g_lblPosObj.previewHitPosText, x: totalW + 5, y: -8, align: C_ALIGN_LEFT,
2705
+ }),
2706
+ );
2707
+
2708
+ // リバース譜面用の判定ライン
2709
+ // 下から上に流れる場合、hitPosがプラスなら「ステップゾーンより上(座標としてはマイナス)」に来る
2710
+ createEmptySprite(_frame, `previewHitPosLineRev`, {
2711
+ x: startX,
2712
+ y: (C_STEP_Y + revStepY) + Math.round(laneW / 2) - hitPos, // ステップゾーンの中心 - hitPos
2713
+ w: totalW,
2714
+ h: 2,
2715
+ background: `#ffaa00`,
2716
+ boxShadow: `0 0 4px #ffaa00`,
2717
+ });
2718
+
2719
+ // ============================================================
2720
+ // 判定エリア(ドラッグ可能)
2721
+ // ============================================================
2722
+ if (d.judgment === C_FLG_OFF) {
2723
+ _frame.appendChild(disableBox(`Judgment`, {
2724
+ x: Math.round(_playW / 2 - 220), y: Math.round((_playH + (g_posObj?.stepYR ?? 0)) / 2 - 60), w: 440, h: 120,
2725
+ }));
2726
+ } else {
2727
+ const opacity = g_stateObj.opacity / 100;
2728
+ const jdgCenterY = Math.round((_playH + (g_posObj?.stepYR ?? 0)) / 2);
2729
+ const groupW = 370;
2730
+ const groupH = 51;
2731
+
2732
+ const jdgSettings = [
2733
+ {
2734
+ key: `arrowJdg`,
2735
+ stdYOffset: -60,
2736
+ offsetX: g_diffObj.arrowJdgX ?? 0,
2737
+ offsetY: g_diffObj.arrowJdgY ?? 0,
2738
+ chara: d.judgment === C_FLG_ON ? g_lblNameObj.j_ii : ``,
2739
+ css: [g_cssObj.common_ii, g_cssObj.common_kita], // [chara, combo]
2740
+ toast: g_lblNameObj.arrowJdgUpdate,
2741
+ },
2742
+ {
2743
+ key: `frzJdg`,
2744
+ stdYOffset: 10,
2745
+ offsetX: g_diffObj.frzJdgX ?? 0,
2746
+ offsetY: g_diffObj.frzJdgY ?? 0,
2747
+ chara: d.judgment === C_FLG_ON ? g_lblNameObj.j_kita : ``,
2748
+ css: [g_cssObj.common_kita, g_cssObj.common_ii],
2749
+ toast: g_lblNameObj.frzJdgUpdate,
2750
+ }
2751
+ ];
2752
+
2753
+ jdgSettings.forEach(item => {
2754
+
2755
+ const stdX = Math.round(_playW / 2 - (item.key === `arrowJdg` ? 220 : 120));
2756
+ const initX = g_previewPos[item.key].x ?? (stdX + item.offsetX);
2757
+ const initY = g_previewPos[item.key].y ?? (jdgCenterY + item.offsetY + item.stdYOffset);
2758
+
2759
+ // グループコンテナの生成
2760
+ const group = createEmptySprite(_frame, `previewGrp_${item.key}`, {
2761
+ x: initX, y: initY, w: groupW, h: groupH, pointerEvents: C_DIS_AUTO,
2762
+ });
2763
+
2764
+ // ラベル類の配置
2765
+ multiAppend(group,
2766
+ createDivCss2Label(`previewChara_${item.key}`, item.chara, {
2767
+ x: 0, y: 0, w: g_limitObj.jdgCharaWidth, h: g_limitObj.jdgCharaHeight,
2768
+ siz: g_limitObj.jdgCharaSiz, opacity,
2769
+ }, item.css[0]),
2770
+ createDivCss2Label(`previewCombo_${item.key}`, d.judgment === C_FLG_ON ? `5 Combo!!` : ``, {
2771
+ x: 170, y: 0, w: g_limitObj.jdgCharaWidth, h: g_limitObj.jdgCharaHeight,
2772
+ siz: g_limitObj.jdgCharaSiz, opacity,
2773
+ }, item.css[1]),
2774
+ createDivCss2Label(`previewDiff_${item.key}`, `Fast 3 Frames`, {
2775
+ x: 170, y: 25, w: g_limitObj.jdgCharaWidth, h: g_limitObj.jdgCharaHeight,
2776
+ siz: g_limitObj.mainSiz, color: `#ff9966`, opacity,
2777
+ }, g_cssObj.common_fast),
2778
+ );
2779
+
2780
+ // ドラッグ機能を付与
2781
+ makeElementDraggable(group, item.key, _playW, _playH, { w: groupW, h: groupH }, {
2782
+ toastTitle: item.toast,
2783
+ getStdPos: () => ({
2784
+ x: stdX,
2785
+ y: Math.round(jdgCenterY + item.stdYOffset),
2786
+ }),
2787
+ });
2788
+ });
2789
+ }
2790
+
2791
+ // ============================================================
2792
+ // ライフゲージ(左縦帯)
2793
+ // ============================================================
2794
+ if (d.lifegauge === C_FLG_OFF) {
2795
+ _frame.appendChild(disableBox(`LifeGauge`, g_lblPosObj.previewLifeDisabled));
2796
+ } else {
2797
+ multiAppend(_frame,
2798
+ createDivCss2Label(`previewLifeBack`, ``, g_lblPosObj.previewLifeBack, g_cssObj.life_Background),
2799
+ createDivCss2Label(`previewLifeBar`, ``, g_lblPosObj.previewLifeBar, g_cssObj.life_Cleared),
2800
+ createDivCss2Label(`previewLifeNum`, `700`, g_lblPosObj.previewLifeNum, g_cssObj.life_Cleared),
2801
+ );
2802
+ }
2803
+
2804
+ // ============================================================
2805
+ // スコア・判定カウンタ(右端縦列)
2806
+ // ============================================================
2807
+ if (d.score === C_FLG_OFF) {
2808
+ _frame.appendChild(disableBox(`Score`, g_lblPosObj.previewScoreDisabled));
2809
+
2810
+ if (g_stateObj.frzReturn !== C_FLG_OFF) {
2811
+ _frame.appendChild(disableBox(`FrzReturnBar`, g_lblPosObj.previewLifeFrzDisabled));
2812
+ }
2813
+ } else {
2814
+ const scoreItems = [
2815
+ { name: `ii`, cnt: `5` },
2816
+ { name: `shakin`, cnt: `0` },
2817
+ { name: `matari`, cnt: `0` },
2818
+ { name: `shobon`, cnt: `0` },
2819
+ { name: `uwan`, cnt: `0` },
2820
+ { name: `combo`, cnt: `5` },
2821
+ {},
2822
+ { name: `kita`, cnt: `5` },
2823
+ { name: `iknai`, cnt: `0` },
2824
+ { name: `combo`, cnt: `5` },
2825
+ ];
2826
+ const sx = _playW - 110 + g_headerObj.scAreaWidth;
2827
+ scoreItems.forEach((item, i) => {
2828
+ _frame.appendChild(
2829
+ createDivCss2Label(`previewScore${i}`, item.cnt || ``, {
2830
+ x: sx + 50, y: 20 * (i + 1), w: 50, h: 20,
2831
+ siz: 16, align: `right`,
2832
+ }, g_cssObj[`common_${item.name}`]),
2833
+ );
2834
+ });
2835
+
2836
+ // FrzReturn用ゲージ
2837
+ if (g_stateObj.frzReturn !== C_FLG_OFF) {
2838
+ multiAppend(_frame,
2839
+ createDivCss2Label(`previewFrzLifeBack`, ``, g_lblPosObj.previewFrzLifeBack, g_cssObj.life_Background),
2840
+ createDivCss2Label(`previewFrzLifeBar`, ``, g_lblPosObj.previewFrzLifeBar, g_cssObj.life_frzNormal),
2841
+ );
2842
+ }
2843
+ }
2844
+
2845
+ // ============================================================
2846
+ // 曲名・制作者(左下)
2847
+ // ============================================================
2848
+ if (d.musicinfo === C_FLG_OFF) {
2849
+ _frame.appendChild(disableBox(`MusicInfo`, g_lblPosObj.previewMusicInfoDisabled));
2850
+ _frame.appendChild(disableBox(`GaugeName`, g_lblPosObj.previewGaugeNameDisabled));
2851
+ } else {
2852
+ const creditName = `Sample Music / Artist Name`;
2853
+ const difName = `[7key / Normal]`;
2854
+ const checkMusicSiz = (_text, _siz) => getFontSize2(_text, g_headerObj.playingWidth - g_headerObj.customViewWidth - 125, { maxSiz: _siz });
2855
+
2856
+ multiAppend(_frame,
2857
+ createDivCss2Label(`previewCredit`, creditName, {
2858
+ ...g_lblPosObj.lblCredit, siz: checkMusicSiz(creditName, g_limitObj.musicTitleSiz)
2859
+ }),
2860
+ createDivCss2Label(`previewDifName`, difName, {
2861
+ ...g_lblPosObj.lblDifName, siz: checkMusicSiz(difName, 12)
2862
+ }),
2863
+ createDivCss2Label(`previewTime1`, `0:04 /`, {
2864
+ ...g_lblPosObj.lblTime1,
2865
+ }),
2866
+ createDivCss2Label(`previewTime2`, `2:54`, g_lblPosObj.lblTime2),
2867
+
2868
+ // ゲージ設定名
2869
+ createDivCss2Label(`previewGauge`, `Original`, g_lblPosObj.lblGaugeMode),
2870
+ )
2871
+ }
2872
+
2873
+ // ============================================================
2874
+ // ショートカット表示
2875
+ // ============================================================
2876
+ const scGroup = createEmptySprite(_frame, `previewScGroup`, {
2877
+ x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
2878
+ y: _playH - 100 + g_diffObj.shortcutY, w: 80, h: 100, pointerEvents: C_DIS_AUTO,
2879
+ });
2880
+ multiAppend(scGroup,
2881
+ createDivCss2Label(`lblRetry`, `[${g_lblNameObj.l_retry}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 0 }),
2882
+ createDivCss2Label(`lblRetrySc`, g_kCd[g_headerObj.keyRetry],
2883
+ { ...g_lblPosObj.lblMainScKey, x: 0, y: 15, fontWeight: g_headerObj.keyRetry === C_KEY_RETRY ? `normal` : `bold` }),
2884
+ createDivCss2Label(`lblTitleBack`, `[${g_lblNameObj.l_titleBack}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 35 }),
2885
+ createDivCss2Label(`lblTitleBackSc`, g_isMac ? `Shift+${g_kCd[g_headerObj.keyRetry]}` : g_kCd[g_headerObj.keyTitleBack],
2886
+ { ...g_lblPosObj.lblMainScKey, x: 0, y: 50, fontWeight: g_headerObj.keyTitleBack === C_KEY_TITLEBACK ? `normal` : `bold` }),
2887
+ createDivCss2Label(`lblPause`, `[${g_lblNameObj.l_pause}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 70 }),
2888
+ createDivCss2Label(`lblPauseSc`, g_kCd[g_headerObj.keyPause],
2889
+ { ...g_lblPosObj.lblMainScKey, x: 0, y: 85, fontWeight: g_headerObj.keyPause === C_KEY_PAUSE ? `normal` : `bold` }),
2890
+ );
2891
+ const scConfig = {
2892
+ toastTitle: g_lblNameObj.shortcutUpdate,
2893
+ getStdPos: () => ({
2894
+ x: g_sWidth + g_headerObj.scAreaWidth - 85,
2895
+ y: _playH - 100
2896
+ }),
2897
+ };
2898
+ makeElementDraggable(scGroup, `shortcut`, _playW, _playH, { w: 80, h: 100 }, scConfig);
2899
+
2900
+ // ユーザカスタムイベント(プレビュー表示用)
2901
+ safeExecuteCustomHooks(`g_customJsObj.displayPreview`, g_customJsObj.displayPreview, _frame, _playW, _playH);
2902
+ };
2903
+
2904
+ /**
2905
+ * プレビューが表示されたまま、HitPositionのラインだけを動かす
2906
+ * @param {number} _newHitPos 新しい g_stateObj.hitPosition の値
2907
+ */
2908
+ const updatePreviewHitPositionLine = (_newHitPos) => {
2909
+
2910
+ // 表示用の文字列を作成(例: "+15px", "-8px", "0px")
2911
+ const sign = _newHitPos > 0 ? `+` : ``;
2912
+ const textValue = `${sign}${_newHitPos}px↑↓`;
2913
+
2914
+ // 1. 各種基準座標を再取得(buildPreviewUI 内の計算ロジックと同期)
2915
+ const stepY = g_posObj.stepY ?? C_STEP_Y;
2916
+ const revStepY = g_posObj.reverseStepY;
2917
+ const laneW = 50;
2918
+
2919
+ // 2. DOM要素を直接取得
2920
+ const lineNormal = document.getElementById(`previewHitPosLine`);
2921
+ const lineReverse = document.getElementById(`previewHitPosLineRev`);
2922
+ const textNormal = document.getElementById(`previewHitPosText`);
2923
+
2924
+ // 3. プレビューが表示されている場合のみ、style.top を直接書き換える
2925
+ if (lineNormal) {
2926
+ const newY = stepY + Math.round(laneW / 2) + _newHitPos;
2927
+ lineNormal.style.top = wUnit(newY);
2928
+ }
2929
+ if (textNormal) {
2930
+ textNormal.textContent = textValue;
2931
+ }
2932
+
2933
+ if (lineReverse) {
2934
+ const newY = (C_STEP_Y + revStepY) + Math.round(laneW / 2) - _newHitPos;
2935
+ lineReverse.style.top = wUnit(newY);
2936
+ }
2937
+ };
2938
+
2939
+ /**
2940
+ * 要素をドラッグ可能にする(汎用ユーティリティ)
2941
+ * @param {HTMLElement} _target ドラッグ対象の要素
2942
+ * @param {string} _key 座標保存用のキー(g_previewPosオブジェクトのプロパティ名)
2943
+ * @param {number} _playW 制限範囲の幅
2944
+ * @param {number} _playH 制限範囲の高さ
2945
+ * @param {object} _bounds 要素自体のサイズ { w, h, scale } (はみ出し防止用)
2946
+ * @param {object} _config 座標反映ルールオブジェクト
2947
+ */
2948
+ const makeElementDraggable = (_target, _key, _playW, _playH, _bounds, _config) => {
2949
+ let dragging = false;
2950
+ let dragStartX = 0, dragStartY = 0;
2951
+ let elemStartX = 0, elemStartY = 0;
2952
+
2953
+ const boundsW = _bounds?.w ?? _target.offsetWidth ?? 0;
2954
+ const boundsH = _bounds?.h ?? _target.offsetHeight ?? 0;
2955
+ const scale = _bounds?.scale ?? 0.8;
2956
+
2957
+ // ドラッグハンドル(薄い枠)を作成
2958
+ const handleId = _target.id ? `handle_${_target.id}` : `dragHandle_${Math.random().toString(36).slice(2, 9)}`;
2959
+ _target.style.cursor = `grab`;
2960
+
2961
+ const bgColor = g_headerObj.baseBrightFlg ? `0,0,0` : `255,255,255`;
2962
+ createEmptySprite(_target, handleId, {
2963
+ x: 0, y: 0, w: boundsW, h: boundsH,
2964
+ border: `1px dashed rgba(${bgColor},0.3)`,
2965
+ boxSizing: `border-box`, borderRadius: `2px`,
2966
+ background: `rgba(${bgColor},0.04)`,
2967
+ });
2968
+
2969
+ g_handler.addListener(_target, `pointerdown`, _evt => {
2970
+ dragging = true;
2971
+ dragStartX = _evt.clientX;
2972
+ dragStartY = _evt.clientY;
2973
+ elemStartX = parseInt(_target.style.left, 10) || 0;
2974
+ elemStartY = parseInt(_target.style.top, 10) || 0;
2975
+ _target.style.cursor = `grabbing`;
2976
+ _target.setPointerCapture(_evt.pointerId);
2977
+ _evt.stopPropagation();
2978
+ });
2979
+
2980
+ g_handler.addListener(_target, `pointermove`, _evt => {
2981
+ if (!dragging) return;
2982
+
2983
+ // 1. マウスの実際の移動量を計算
2984
+ const mouseDx = _evt.clientX - dragStartX;
2985
+ const mouseDy = _evt.clientY - dragStartY;
2986
+
2987
+ // 2. 【最重要】スケール逆算して、縮小空間内の移動量に変換
2988
+ const dx = mouseDx / scale;
2989
+ const dy = mouseDy / scale;
2990
+
2991
+ // 3. 境界値制限
2992
+ const minX = g_headerObj.playingLayout ? -g_headerObj.scAreaWidth : 0;
2993
+ const newX = Math.max(minX, Math.min(_playW + g_headerObj.scAreaWidth - boundsW, elemStartX + dx));
2994
+ const newY = Math.max(0, Math.min(_playH - boundsH, elemStartY + dy));
2995
+
2996
+ _target.style.left = wUnit(newX);
2997
+ _target.style.top = wUnit(newY);
2998
+ _evt.stopPropagation();
2999
+ });
3000
+
3001
+ g_handler.addListener(_target, `pointerup`, _evt => {
3002
+ if (!dragging) return;
3003
+ dragging = false;
3004
+ _target.style.cursor = `grab`;
3005
+
3006
+ const finalX = parseInt(_target.style.left, 10) || 0;
3007
+ const finalY = parseInt(_target.style.top, 10) || 0;
3008
+
3009
+ if (g_previewPos[_key]) {
3010
+ g_previewPos[_key].x = finalX;
3011
+ g_previewPos[_key].y = finalY;
3012
+ }
3013
+ if (_config) {
3014
+ applyElementPositionToGame(finalX, finalY, _config, _key);
3015
+ }
3016
+ _evt.stopPropagation();
3017
+ });
3018
+
3019
+ g_handler.addListener(_target, `pointercancel`, _evt => {
3020
+ dragging = false;
3021
+ _target.style.cursor = `grab`;
3022
+ });
3023
+ };
3024
+
3025
+ /**
3026
+ * ドラッグ結果の座標をゲーム本体の設定に汎用的に反映する
3027
+ * @param {number} _x 確定したframe相対X
3028
+ * @param {number} _y 確定したframe相対Y
3029
+ * @param {object} _config 反映用の設定オブジェクト
3030
+ * @param {string} _key 保存用キー(g_diffObjのプロパティ名の接頭辞)
3031
+ */
3032
+ const applyElementPositionToGame = (_x, _y, _config, _key) => {
3033
+
3034
+ // 1. 各要素固有の「標準座標(基準点)」を計算
3035
+ const std = _config.getStdPos();
3036
+
3037
+ // 2. オフセット(差分)を計算
3038
+ const diffX = _x - std.x;
3039
+ const diffY = _y - std.y;
3040
+
3041
+ // 3. 指定された保存先にオフセットを格納
3042
+ g_diffObj[`${_key}X`] = diffX;
3043
+ g_diffObj[`${_key}Y`] = diffY;
3044
+ g_localStorage[`${_key}X`] = diffX;
3045
+ g_localStorage[`${_key}Y`] = diffY;
3046
+
3047
+ // 4. トースト表示 (通知が不要な要素なら省略可能にする)
3048
+ if (_config.toastTitle) {
3049
+ showToast(`${_config.toastTitle}: dX=${diffX}, dY=${diffY}`);
3050
+ }
3051
+ };
3052
+
3053
+ /**
3054
+ * 画面上部に一時的なトーストメッセージを表示する
3055
+ * @param {string} _msg
3056
+ */
3057
+ const showToast = _msg => {
3058
+ const existing = document.getElementById(`previewToast`);
3059
+ if (existing) existing.remove();
3060
+
3061
+ const toast = createDivCss2Label(`previewToast`, _msg, {
3062
+ x: g_btnX() + g_btnWidth() / 2, y: 50, w: g_btnWidth() / 2, h: 10, siz: 12,
3063
+ transform: `translateX(-50%)`,
3064
+ background: `rgba(0,40,80,0.92)`,
3065
+ color: `#aaddff`,
3066
+ border: `1px solid #3366aa`,
3067
+ borderRadius: `6px`,
3068
+ padding: `6px 16px`,
3069
+ fontFamily: `monospace`,
3070
+ whiteSpace: `nowrap`,
3071
+ pointerEvents: C_DIS_NONE,
3072
+ transition: `opacity 0.4s`,
3073
+ opacity: `1`,
3074
+ });
3075
+ divRoot.appendChild(toast);
3076
+ g_timerHandler.setTimeout(() => { toast.style.opacity = `0`; }, 2200);
3077
+ g_timerHandler.setTimeout(() => { if (toast.parentNode) toast.remove(); }, 2700);
3078
+ };
3079
+
3080
+ /**
3081
+ * 設定・オプション画面のラベル・ボタン処理の描画
3082
+ * @param {Object} _sprite 基準とするスプライト(ここで指定する座標は、そのスプライトからの相対位置)
3083
+ */
3084
+ const createSettingsDisplayWindow = _sprite => {
3085
+
3086
+ /**
3087
+ * Display表示/非表示ボタン
3088
+ * @param {string} _name
3089
+ * @param {number} _heightPos 縦位置
3090
+ * @param {number} _widthPos 横位置
3091
+ */
3092
+ const makeDisplayButton = (_name, _heightPos, _widthPos) => {
3093
+
3094
+ const flg = g_stateObj[`d_${_name.toLowerCase()}`];
3095
+ const linkId = `lnk${_name}`;
3096
+
3097
+ if (g_headerObj[`${_name}Use`]) {
3098
+
3099
+ // 設定名、CSS名(2種)、表示名
3100
+ const list = [C_FLG_OFF, C_FLG_ON].concat(g_settings[`d_${_name}s`] || []);
3101
+ const cssBarList = [C_FLG_OFF, C_FLG_ON].concat(Array(g_settings[`d_${_name}s`]?.length).fill(g_settings.d_cssBarExName) || []);
3102
+ const cssBgList = [g_settings.d_cssBgName, g_settings.d_cssBgName].concat(Array(g_settings[`d_${_name}s`]?.length).fill(g_settings.d_cssBgExName) || []);
3103
+ const lbls = [toCapitalize(_name), toCapitalize(_name)].concat(g_settings[`d_${_name}s`] || []);
3104
+
3105
+ const dispView = () => [C_FLG_OFF, C_FLG_ON].includes(g_stateObj[`d_${_name.toLowerCase()}`]) ?
3106
+ g_lblNameObj[`d_${toCapitalize(_name)}`] : getStgDetailName(lbls[g_settings.displayNum[_name]]);
3107
+
3108
+ const withShortCutDesc = () => createScText(document.getElementById(linkId), `${toCapitalize(_name)}`,
3109
+ { displayName: g_currentPage, targetLabel: linkId, x: -5 });
3110
+
3111
+ /**
3112
+ * Displayボタン処理
3113
+ * @param {number} _scrollNum
3114
+ * @param {boolean} _filterFlg
3115
+ */
3116
+ const switchDisplay = (_scrollNum = 1, _filterFlg = true) => {
3117
+ const prevDisp = g_settings.displayNum[_name];
3118
+ const [prevBarColor, prevBgColor] = [cssBarList[prevDisp], cssBgList[prevDisp]];
3119
+
3120
+ g_settings.displayNum[_name] = nextPos(prevDisp, _scrollNum, _filterFlg ? 2 : list.length);
3121
+ const nextDisp = g_settings.displayNum[_name];
3122
+ const [nextBarColor, nextBgColor] = [cssBarList[nextDisp], cssBgList[nextDisp]];
3123
+
3124
+ g_stateObj[`d_${_name.toLowerCase()}`] = list[g_settings.displayNum[_name]];
3125
+ document.getElementById(linkId).innerHTML = dispView();
3126
+ document.getElementById(linkId).classList.replace(g_cssObj[`button_${prevBarColor}`], g_cssObj[`button_${nextBarColor}`]);
3127
+ document.getElementById(linkId).classList.replace(g_cssObj[`button_${prevBgColor}`], g_cssObj[`button_${nextBgColor}`]);
3128
+
3129
+ withShortCutDesc();
3130
+ interlockingButton(g_headerObj, _name, nextBarColor, prevBarColor, true);
3131
+ updateSettingSummary();
3132
+ };
3133
+
3134
+ // Displayボタン初期化
3135
+ g_settings.displayNum[_name] = list.findIndex(flg => flg === g_stateObj[`d_${_name.toLowerCase()}`]);
3136
+ displaySprite.appendChild(
3137
+ makeSettingLblCssButton(linkId, dispView(), _heightPos, () => switchDisplay(), {
3138
+ x: 30 + 180 * _widthPos, y: 20 * _heightPos, w: 170, h: 18,
3139
+ title: g_msgObj[`d_${_name.toLowerCase()}`], borderStyle: `solid`,
3140
+ cxtFunc: () => switchDisplay(-1),
3141
+ }, `button_${cssBgList[g_settings.displayNum[_name]]}`, `button_${cssBarList[g_settings.displayNum[_name]]}`)
3142
+ );
3143
+ withShortCutDesc();
3144
+
3145
+ // Display切替ボタン(ON/OFF以外用)
3146
+ if (g_settings[`d_${_name}s`] !== undefined) {
3147
+ displaySprite.appendChild(
3148
+ makeSettingLblCssButton(`${linkId}R`, `>`, _heightPos, () => switchDisplay(1, false), {
3149
+ x: 175 + 180 * _widthPos, w: 25, y: 2 + 20 * _heightPos, h: 18, cxtFunc: () => switchDisplay(-1, false),
3150
+ }, g_cssObj.button_Mini)
3151
+ );
3152
+ }
3153
+ } else {
3154
+ displaySprite.appendChild(
3155
+ createDivCss2Label(linkId, g_lblNameObj[`d_${toCapitalize(_name)}`] + `:${g_headerObj[`${_name}Set`]}`, {
3156
+ x: 30 + 180 * _widthPos, y: 3 + 20 * _heightPos,
3157
+ w: 170, siz: g_limitObj.difSelectorSiz,
3158
+ }, g_cssObj[`button_Disabled${flg}`])
3159
+ );
3160
+ }
3161
+ };
3162
+
3163
+ // 各ボタン用のスプライトを作成
3164
+ const optionsprite = createEmptySprite(_sprite, `optionsprite`, g_windowObj.optionSprite);
3165
+
3166
+ // 設定毎に個別のスプライトを作成し、その中にラベル・ボタン類を配置
3167
+ const displaySprite = createEmptySprite(optionsprite, `displaySprite`, g_windowObj.displaySprite);
3168
+ const spriteList = setSpriteList(g_settingPos.settingsDisplay);
3169
+
3170
+ _sprite.appendChild(createDivCss2Label(`sdDesc`, g_lblNameObj.sdDesc, g_lblPosObj.sdDesc));
3171
+ g_displays.forEach((name, j) => makeDisplayButton(name, j % 6, Math.floor(j / 6)));
3172
+
3173
+ // ---------------------------------------------------
3174
+ // 矢印の見え方 (Appearance)
3175
+ // 縦位置: 5.8
3176
+ createGeneralSetting(spriteList.appearance, `appearance`, {
3177
+ addRFunc: () => dispAppearanceSlider(),
3178
+ });
3179
+
3180
+ // Hidden+/Sudden+初期値用スライダー、ロックボタン
3181
+ multiAppend(spriteList.appearance,
3182
+ createDivCss2Label(`lblAppearancePos`, `${g_hidSudObj.distH[g_stateObj.appearance](g_hidSudObj.filterPos)}`, g_lblPosObj.lblAppearancePos),
3183
+ createDivCss2Label(`lblAppearanceBar`, `<input id="appearanceSlider" type="range" value="${g_hidSudObj.filterPos}" min="0" max="100" step="1">`,
3184
+ g_lblPosObj.lblAppearanceBar),
3185
+ createCss2Button(`lnkLockBtn`, g_lblNameObj.filterLock, evt => setLockView(evt.target), {
3186
+ ...g_lblPosObj.lnkLockBtn, cxtFunc: evt => setLockView(evt.target),
3187
+ }, g_cssObj.button_Default, g_cssObj[`button_Rev${g_stateObj.filterLock}`]),
3188
+ );
3189
+
3190
+ const setLockView = (_btn) => {
3191
+ const prevLock = g_stateObj.filterLock;
3192
+ g_settings.filterLockNum = (g_settings.filterLockNum + 1) % 2;
3193
+ g_stateObj.filterLock = g_settings.filterLocks[g_settings.filterLockNum];
3194
+
3195
+ _btn.classList.replace(g_cssObj[`button_Rev${prevLock}`],
3196
+ g_cssObj[`button_Rev${g_stateObj.filterLock}`]);
3197
+ updateSettingSummary();
3198
+ };
3199
+
3200
+ const appearanceSlider = document.getElementById(`appearanceSlider`);
3201
+ g_handler.addListener(appearanceSlider, `input`, () => {
3202
+ g_hidSudObj.filterPos = inputSlider(appearanceSlider, lblAppearancePos, `appearance`);
3203
+ updateSettingSummary();
3204
+ }, false);
3205
+
3206
+ const dispAppearanceSlider = () => {
3207
+ [`lblAppearanceBar`, `lnkLockBtn`, `lnkfilterLine`].forEach(obj =>
3208
+ $id(obj).visibility = g_appearanceRanges.includes(g_stateObj.appearance) ? `Visible` : `Hidden`);
3209
+ inputSlider(appearanceSlider, lblAppearancePos, `appearance`);
3210
+ };
3211
+ dispAppearanceSlider();
3212
+
3213
+ // ---------------------------------------------------
3214
+ // 判定表示系の不透明度 (Opacity)
3215
+ // 縦位置: 7.4
3216
+ g_headerObj.opacityUse = g_headerObj.judgmentUse || g_headerObj.judgmentSet === C_FLG_ON;
3217
+
3218
+ createGeneralSetting(spriteList.opacity, `opacity`, { unitName: g_lblNameObj.percent });
3219
+
3220
+ // ---------------------------------------------------
3221
+ // タイミング調整 (HitPosition)
3222
+ // 縦位置: 8.4
3223
+ createGeneralSetting(spriteList.hitPosition, `hitPosition`, {
3224
+ skipTerms: g_settings.hitPositionTerms, scLabel: g_lblNameObj.sc_hitPosition, roundNum: 5,
3225
+ unitName: g_lblNameObj.pixel,
3226
+ addRFunc: () => updatePreviewHitPositionLine(g_stateObj.hitPosition),
3227
+ });
3228
+ };
3229
+
3230
+ /**
3231
+ * Displayボタンを切り替えたときに連動して切り替えるボタンの設定
3232
+ * @param {object} _headerObj
3233
+ * @param {string} _name
3234
+ * @param {string} _current 変更元
3235
+ * @param {string} _next 変更先
3236
+ * @param {boolean} _buttonFlg ボタンフラグ (false: 初期, true: ボタン)
3237
+ */
3238
+ const interlockingButton = (_headerObj, _name, _current, _next, _buttonFlg = false) => {
3239
+ let includeDefaults = [];
3240
+ if (g_stateObj[`d_${_name.toLowerCase()}`] === C_FLG_OFF) {
3241
+ g_displays.forEach(option => {
3242
+ if (option === _name) {
3243
+ return;
3244
+ }
3245
+ if (g_stateObj[`d_${option.toLowerCase()}`] === C_FLG_ON && _headerObj[`${option}Default`] !== undefined) {
3246
+ includeDefaults = includeDefaults.concat(_headerObj[`${option}ChainOFF`]);
3247
+ }
3248
+ });
3249
+ }
3250
+
3251
+ if (_headerObj[`${_name}ChainOFF`].length !== 0) {
3252
+
3253
+ // 連動してOFFにするボタンの設定
3254
+ _headerObj[`${_name}ChainOFF`].filter(defaultOption => !includeDefaults.includes(defaultOption))
3255
+ .forEach(defaultOption => {
3256
+ g_stateObj[`d_${defaultOption.toLowerCase()}`] = _next;
3257
+ if (_buttonFlg) {
3258
+ let txtDisabled = ``;
3259
+ if (!g_headerObj[`${defaultOption}Use`]) {
3260
+ txtDisabled = `Disabled`;
3261
+ document.getElementById(`lnk${defaultOption}`).textContent = `${toCapitalize(defaultOption)}:${_next}`;
3262
+ }
3263
+ document.getElementById(`lnk${defaultOption}`).classList.replace(g_cssObj[`button_${txtDisabled}${_current}`], g_cssObj[`button_Disabled${_next}`]);
3264
+ }
3265
+ // さらに連動する場合は設定を反転
3266
+ interlockingButton(_headerObj, defaultOption, _next, _current, _buttonFlg);
3267
+ });
3268
+ }
3269
+ };
3270
+
3271
+ /*-----------------------------------------------------------*/
3272
+ /* Scene : EX-SETTINGS [apple] */
3273
+ /*-----------------------------------------------------------*/
3274
+
3275
+ const exSettingInit = () => {
3276
+ clearWindow();
3277
+ g_currentPage = `exSetting`;
3278
+
3279
+ multiAppend(divRoot,
3280
+
3281
+ // 画面タイトル
3282
+ getTitleDivLabel(`lblTitle`,
3283
+ `<div class="settings_Title">EX-</div><div class="settings_Title2">SETTINGS</div>`
3284
+ .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering),
3285
+
3286
+ );
3287
+
3288
+ // 各ボタン用のスプライトを作成
3289
+ createEmptySprite(divRoot, `optionsprite`, g_windowObj.optionSprite);
3290
+ const spriteList = setSpriteList(g_settingPos.exSetting);
3291
+
3292
+ createGeneralSettingEx(spriteList, `playWindow`, { defaultList: [`Default`] });
3293
+ createGeneralSetting(spriteList.stepArea, `stepArea`);
3294
+ createGeneralSettingEx(spriteList, `frzReturn`);
3295
+ createGeneralSetting(spriteList.shaking, `shaking`);
3296
+ createGeneralSetting(spriteList.effect, `effect`, {
3297
+ addRFunc: () => {
3298
+ g_stateObj.d_arroweffect = boolToSwitch(g_stateObj.effect !== C_FLG_OFF || g_headerObj.arrowEffectSetFlg);
3299
+ g_headerObj.arrowEffectUse = g_stateObj.effect === C_FLG_OFF && g_headerObj.arrowEffectUseOrg;
3300
+ g_headerObj.arrowEffectSet = g_stateObj.d_arroweffect;
3301
+ },
3302
+ });
3303
+ createGeneralSettingEx(spriteList, `camoufrage`, { defaultList: [] });
3304
+ createGeneralSetting(spriteList.swapping, `swapping`);
3305
+ createGeneralSetting(spriteList.judgRange, `judgRange`, {
3306
+ addRFunc: () => {
3307
+ [g_judgObj.arrowJ, g_judgObj.frzJ] = g_judgRanges[g_stateObj.judgRange];
3308
+ lblJudgRangeView.innerHTML = getJudgRangeView();
3309
+ }
3310
+ });
3311
+ createGeneralSetting(spriteList.autoRetry, `autoRetry`);
3312
+
3313
+ // 判定範囲の設定を表示
3314
+ const getJudgRangeView = () => `| ` +
3315
+ `<span class="common_ii">${g_lblNameObj.j_ii}</span>: ≦ <b>±${g_judgObj.arrowJ[0]} f</b> | ` +
3316
+ `<span class="common_shakin">${g_lblNameObj.j_shakin}</span>: ≦ <b>±${g_judgObj.arrowJ[1]} f</b> | ` +
3317
+ `<span class="common_matari">${g_lblNameObj.j_matari}</span>: ≦ <b>±${g_judgObj.arrowJ[2]} f</b> | <br>| ` +
3318
+ `<span class="common_shobon">${g_lblNameObj.j_shobon}</span>: ≦ <b>±${g_judgObj.arrowJ[3]} f</b> | ` +
3319
+ `<span class="common_uwan">${g_lblNameObj.j_uwan}</span>: > <b>±${g_judgObj.arrowJ[3]} f</b> | <br>| ` +
3320
+ `<span class="common_kita">${g_lblNameObj.j_kita}</span>: ≦ <b>±${g_judgObj.frzJ[1]} f</b> | ` +
3321
+ `<span class="common_iknai">${g_lblNameObj.j_iknai}</span>: > <b>±${g_judgObj.frzJ[1]} f</b> |`;
3322
+
3323
+ multiAppend(judgRangeSprite,
3324
+ createDivCss2Label(`lblJudgRangeView`, getJudgRangeView(), {
3325
+ x: parseFloat($id(`lblJudgRange`).left) + 10, y: parseFloat($id(`lblJudgRange`).top) + 25, w: 300, h: 30, siz: 13,
3326
+ align: C_ALIGN_LEFT,
3327
+ }),
3328
+ );
3329
+
3330
+ // ユーザカスタムイベント(初期)
3331
+ safeExecuteCustomHooks(`g_customJsObj.exSetting`, g_customJsObj.exSetting);
3332
+
3333
+ // 設定系のボタン群をまとめて作成(Data Save, Display切替, Back, KeyConfig, Playボタン)
3334
+ commonSettingBtn(g_currentPage);
3335
+
3336
+ // キー操作イベント(デフォルト)
3337
+ setShortcutEvent(g_currentPage, () => true, { dfEvtFlg: true });
3338
+ document.oncontextmenu = () => true;
3339
+
3340
+ safeExecuteCustomHooks(`g_skinJsObj.exSetting`, g_skinJsObj.exSetting);
3341
+ };
3342
+
3343
+ /**
3344
+ * 拡張設定込みの標準設定
3345
+ * @param {any[]} _spriteList
3346
+ * @param {string} _name
3347
+ * @param {{ defaultList?: string[], displayName?: string, func?: Function, funcEx?: Function }} [options={}]
3348
+ * @param {string[]} [options.defaultList=[C_FLG_OFF]] 拡張設定未使用の設定リスト
3349
+ * @param {string} [options.displayName='exSetting']
3350
+ * @param {Function} [options.func=()=>true] 通常ボタン用追加関数
3351
+ * @param {Function} [options.funcEx=()=>true] 拡張ボタン用追加関数
3352
+ */
3353
+ const createGeneralSettingEx = (_spriteList, _name, { defaultList = [C_FLG_OFF], displayName = `exSetting`,
3354
+ func = () => true, funcEx = () => true } = {}) => {
3355
+ if (_spriteList?.[_name] === undefined) return;
3356
+
3357
+ /**
3358
+ * 拡張ボタンの表示・非表示と通常ボタンの幅変更
3359
+ */
3360
+ const setExpandedBtnSiz = () => {
3361
+ const camelH = toCapitalize(_name);
3362
+ const isDefault = defaultList.includes(g_stateObj[_name]);
3363
+ if (isDefault) {
3364
+ $id(`lnk${camelH}Type`).display = C_DIS_NONE;
3365
+ $id(`lnk${camelH}`).left = wUnit(g_limitObj.setLblLeft);
3366
+ $id(`lnk${camelH}`).width = wUnit(g_limitObj.setLblWidth);
3367
+ } else {
3368
+ $id(`lnk${camelH}Type`).display = C_DIS_INHERIT;
3369
+ $id(`lnk${camelH}`).left = wUnit(g_limitObj.setLblLeftShort);
3370
+ $id(`lnk${camelH}`).width = wUnit(g_limitObj.setLblWidthShort);
3371
+ }
3372
+ const labelWidth = isDefault ? g_limitObj.setLblWidth : g_limitObj.setLblWidthShort - 10;
3373
+ $id(`lnk${camelH}`).fontSize = wUnit(getFontSize2(getStgDetailName(g_stateObj[_name]), labelWidth, { maxSiz: g_limitObj.setLblSiz }));
3374
+ };
3375
+
3376
+ /**
3377
+ * 拡張ボタンの作成
3378
+ * @returns {HTMLDivElement}
3379
+ */
3380
+ const createExpandedBtn = () =>
3381
+ createCss2Button(`lnk${toCapitalize(_name)}Type`, getStgDetailName(g_stateObj[`${_name}Type`]),
3382
+ () => {
3383
+ setSetting(1, `${_name}Type`, { maxSiz: g_limitObj.difSelectorSiz });
3384
+ funcEx();
3385
+ createExpandedScView(_name);
3386
+ },
3387
+ {
3388
+ ...g_lblPosObj.btnReverse,
3389
+ cxtFunc: () => {
3390
+ setSetting(-1, `${_name}Type`, { maxSiz: g_limitObj.difSelectorSiz });
3391
+ funcEx();
3392
+ createExpandedScView(_name);
3393
+ },
3394
+ title: g_msgObj[`${_name}Type`] ?? ``,
3395
+ }, g_cssObj.button_Default, g_cssObj.button_RevON);
3396
+
3397
+ /**
3398
+ * 拡張ボタンのショートカット表示、拡張ボタンのCSS切り替え
3399
+ */
3400
+ const createExpandedScView = () => {
3401
+ const settingLabel = `${toCapitalize(_name)}Type`;
3402
+ if (document.getElementById(`sc${settingLabel}`) === null) {
3403
+ createScText(document.getElementById(`lnk${settingLabel}`), settingLabel, {
3404
+ displayName, targetLabel: `lnk${settingLabel}`, x: -13
3405
+ });
3406
+ }
3407
+ document.getElementById(`lnk${settingLabel}`).classList.remove(g_cssObj.button_RevON, g_cssObj.button_RevOFF);
3408
+ document.getElementById(`lnk${settingLabel}`).classList.add(g_cssObj[`button_Rev${boolToSwitch(g_stateObj[`${_name}Type`] !== C_FLG_HYPHEN)}`]);
3409
+ };
3410
+
3411
+ // TypeUse 未定義時は true 扱いにする
3412
+ const typeEnabled = setBoolVal(g_headerObj[`${_name}TypeUse`], true);
3413
+ createGeneralSetting(_spriteList[_name], _name, {
3414
+ addRFunc: () => {
3415
+ if (typeEnabled) {
3416
+ setExpandedBtnSiz();
3417
+ }
3418
+ func();
3419
+ },
3420
+ });
3421
+ if (typeEnabled) {
3422
+ _spriteList[_name].appendChild(createExpandedBtn());
3423
+ setExpandedBtnSiz();
3424
+ createExpandedScView();
3425
+ }
3426
+ };