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,1274 @@
1
+ /**
2
+ * Dancing☆Onigiri (CW Edition)
3
+ * タイトル画面(+選曲処理)、データ管理画面、デバッグ画面
4
+ * - ページ: title, dataMgt, precondition
5
+ *
6
+ * Source by tickle
7
+ * Created : 2026/09/13
8
+ * Revised :
9
+ *
10
+ * https://github.com/cwtickle/danoniplus
11
+ */
12
+
13
+ /*-----------------------------------------------------------*/
14
+ /* Scene : TITLE [melon] */
15
+ /*-----------------------------------------------------------*/
16
+
17
+ /**
18
+ * タイトル画面初期化
19
+ * @param {boolean} _initFlg 初期化フラグ
20
+ */
21
+ const titleInit = (_initFlg = false) => {
22
+
23
+ clearWindow();
24
+ g_currentPage = `title`;
25
+ g_stateObj.settingSummaryVisible = false;
26
+
27
+ // タイトル用フレーム初期化
28
+ g_scoreObj.titleFrameNum = 0;
29
+
30
+ // 設定画面位置初期化
31
+ g_settings.settingWindowNum = 0;
32
+
33
+ // タイトルアニメーション用フレーム初期化、ループカウンター設定
34
+ g_animationData.forEach(sprite => {
35
+ g_scoreObj[`${sprite}TitleFrameNum`] = 0;
36
+ g_scoreObj[`${sprite}TitleLoopCount`] = 0;
37
+ });
38
+
39
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
40
+
41
+ // 譜面初期情報ロード許可フラグ
42
+ // (初回読み込み時はローカルストレージのロードが必要なため、
43
+ // ローカルストレージ保存時はフラグを解除しない)
44
+ if (!g_stateObj.dataSaveFlg || hasVal(g_keyObj[`transKey${keyCtrlPtn}`])) {
45
+ g_canLoadDifInfoFlg = false;
46
+ }
47
+ const divRoot = document.getElementById(`divRoot`);
48
+
49
+ // 曲時間制御変数
50
+ let thisTime;
51
+ let buffTime;
52
+ let titleStartTime = performance.now();
53
+
54
+ // 背景スプライトを作成
55
+ createMultipleSprite(`backTitleSprite`, g_headerObj.backTitleMaxDepth);
56
+
57
+ // タイトル文字描画
58
+ divRoot.appendChild(
59
+ getTitleDivLabel(`lblTitle`,
60
+ `<div class="settings_Title">${g_lblNameObj.dancing}</div>
61
+ <div class="settings_TitleStar">${g_lblNameObj.star}</div>
62
+ <div class="settings_Title2">${g_lblNameObj.onigiri}</div>`
63
+ .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering)
64
+ );
65
+
66
+ // 背景の矢印オブジェクトを表示
67
+ const tmpCreatorList = [];
68
+ if (g_headerObj.musicSelectUse) {
69
+ if (getQueryParamVal(`scoreId`) !== null) {
70
+ g_headerObj.viewLists = [];
71
+ g_headerObj.musicNos.forEach((val, j) => {
72
+ if ((g_headerObj.musicGroups?.[val] ?? val) === g_settings.musicIdxNum) {
73
+ g_headerObj.viewLists.push(j);
74
+ tmpCreatorList.push(g_headerObj.creatorNames[j]);
75
+ }
76
+ });
77
+ divRoot.appendChild(drawBackArrow(g_headerObj.viewLists[0] + 1));
78
+ loadLocalStorage(g_settings.musicIdxNum);
79
+ makeInfoWindow(g_msgInfoObj.W_0041.split(`{0}`).join(g_localStorageUrl), ``,
80
+ { _backColor: `#333333`, _textColor: `#cccccc`, _pointerEvents: C_DIS_INHERIT });
81
+ }
82
+ } else if (!g_headerObj.customTitleArrowUse) {
83
+ divRoot.appendChild(drawBackArrow());
84
+ }
85
+
86
+ let wheelHandler;
87
+ if (g_headerObj.musicSelectUse && getQueryParamVal(`scoreId`) === null) {
88
+
89
+ // 選曲画面の初期化
90
+ const wheelCycle = 2;
91
+ g_settings.musicLoopNum = 0;
92
+
93
+ /**
94
+ * メイン以外の選曲ボタンの作成
95
+ * @param {number} _heightPos
96
+ * @returns {HTMLDivElement}
97
+ */
98
+ const createMSelectBtn = (_heightPos) => createCss2Button(`btnMusicSelect${_heightPos}`,
99
+ ``, () => changeMSelect(_heightPos), {
100
+ x: g_btnX(1 / 3) + Math.abs(_heightPos) * 10,
101
+ y: g_sHeight / 2 + _heightPos * 30 + (_heightPos > 0 ? 1 : -1) * 90,
102
+ w: g_btnWidth(1 / 2), h: 27, siz: 14, border: `solid 1px #666666`,
103
+ align: C_ALIGN_LEFT, padding: `0 10px`,
104
+ }, g_cssObj.button_Default_NoColor, g_cssObj.title_base);
105
+
106
+ /**
107
+ * 選曲画面上の音量調整
108
+ * @param {number} _num
109
+ */
110
+ const setBGMVolume = (_num = 1) => {
111
+ g_settings.bgmVolumeNum = nextPos(g_settings.bgmVolumeNum, _num, g_settings.volumes.length);
112
+ g_stateObj.bgmVolume = g_settings.volumes[g_settings.bgmVolumeNum];
113
+ if (g_audioForMS) {
114
+ g_audioForMS.volume = g_stateObj.bgmVolume / 100;
115
+ }
116
+ btnBgmVolume.textContent = `${g_stateObj.bgmVolume}${g_lblNameObj.percent}`;
117
+ };
118
+
119
+ for (let j = -g_settings.mSelectableTerms; j <= g_settings.mSelectableTerms; j++) {
120
+ if (j !== 0) {
121
+ divRoot.appendChild(createMSelectBtn(j));
122
+ }
123
+ }
124
+ createEmptySprite(divRoot, `keyTitleSprite`, g_windowObj.keyTitleSprite);
125
+ multiAppend(divRoot,
126
+ createDivCss2Label(`lblMusicSelect`, ``, g_lblPosObj.lblMusicSelect),
127
+ createDivCss2Label(`lblMusicSelectDetail`, ``, g_lblPosObj.lblMusicSelectDetail),
128
+ createCss2Button(`btnStart`,
129
+ `>`, () => {
130
+ g_timerHandler.clearTimeout(g_timeoutEvtTitleId);
131
+ g_handler.removeListener(wheelHandler);
132
+ g_keyObj.prevKey = `Dummy${g_settings.musicIdxNum}`;
133
+ g_langStorage.bgmVolume = g_stateObj.bgmVolume;
134
+ localStorage.setItem(`danoni-locale`, JSON.stringify(g_langStorage));
135
+ }, { ...g_lblPosObj.btnStart_music, resetFunc: () => optionInit() }, g_cssObj.button_Tweet),
136
+ createCss2Button(`btnMusicSelectPrev`, `↑`, () => changeMSelect(-1),
137
+ g_lblPosObj.btnMusicSelectPrev, g_cssObj.button_Setting),
138
+ createCss2Button(`btnMusicSelectNext`, `↓`, () => changeMSelect(1),
139
+ g_lblPosObj.btnMusicSelectNext, g_cssObj.button_Setting),
140
+ createCss2Button(`btnMusicSelectRandom`, `Random`, () =>
141
+ changeMSelect(Math.floor(Math.random() * (g_headerObj.musicIdxList.length - 1)) + 1),
142
+ g_lblPosObj.btnMusicSelectRandom, g_cssObj.button_Default),
143
+ createDivCss2Label(`lblMusicCnt`, ``, g_lblPosObj.lblMusicCnt),
144
+ );
145
+ createEmptySprite(divRoot, `lblCommentM`, g_lblPosObj.lblComment_music);
146
+ multiAppend(lblCommentM,
147
+ createDivCss2Label(`lblDifNameInfoM`, ``, g_lblPosObj.lblDifNameInfoM),
148
+ createDivCss2Label(`lblDiffiInfoM`, ``, g_lblPosObj.lblDiffiInfoM),
149
+ createDivCss2Label(`lblNotesInfoM`, ``, g_lblPosObj.lblNotesInfoM),
150
+ createDivCss2Label(`lblCommentInfoM`, ``, g_lblPosObj.lblCommentInfoM),
151
+ );
152
+
153
+ if (g_headerObj.bgmUseFlg) {
154
+ multiAppend(divRoot,
155
+ createDivCss2Label(`lblBgmVolume`, g_lblNameObj.bgmVolume, g_lblPosObj.lblBgmVolume),
156
+ createCss2Button(`btnBgmMute`, g_stateObj.bgmMuteFlg ? g_emojiObj.muted : g_emojiObj.speaker, evt => {
157
+ g_stateObj.bgmMuteFlg = !g_stateObj.bgmMuteFlg;
158
+ g_stateObj.bgmMuteFlg ? pauseBGM() : playBGM(0);
159
+ evt.target.innerHTML = g_stateObj.bgmMuteFlg ? g_emojiObj.muted : g_emojiObj.speaker;
160
+ }, g_lblPosObj.btnBgmMute, g_cssObj.button_Default),
161
+ createCss2Button(`btnBgmVolume`, `${g_stateObj.bgmVolume}${g_lblNameObj.percent}`, () => setBGMVolume(), {
162
+ ...g_lblPosObj.btnBgmVolume, cxtFunc: () => setBGMVolume(-1),
163
+ }, g_cssObj.button_Default),
164
+ createCss2Button(`btnBgmVolumeL`, `<`, () => setBGMVolume(-1),
165
+ g_lblPosObj.btnBgmVolumeL, g_cssObj.button_Setting),
166
+ createCss2Button(`btnBgmVolumeR`, `>`, () => setBGMVolume(),
167
+ g_lblPosObj.btnBgmVolumeR, g_cssObj.button_Setting),
168
+ );
169
+ } else {
170
+ multiAppend(divRoot,
171
+ createDivCss2Label(`lblBgmVolume`, `${g_lblNameObj.bgmMuted} ${g_emojiObj.muted}`, g_lblPosObj.btnBgmVolume),
172
+ );
173
+ }
174
+ changeMSelect(0, _initFlg);
175
+
176
+ let wheelCnt = 0;
177
+ wheelHandler = g_handler.addListener(divRoot, `wheel`, e => {
178
+
179
+ if (document.getElementById(`lblComment`) !== null && lblComment.style.display === C_DIS_INHERIT) {
180
+ return;
181
+ }
182
+ // コメント欄(lblCommentM)のスクロール可能性をチェック
183
+ const isScrollable = lblCommentM.scrollHeight > lblCommentM.clientHeight;
184
+
185
+ // マウスがコメント欄上にあり、スクロールが可能ならイベントをスキップ
186
+ if (lblCommentM.contains(e.target) && isScrollable) {
187
+ // スクロール位置の判定
188
+ const atTop = lblCommentM.scrollTop === 0 && e.deltaY < 0;
189
+ const atBottom = (lblCommentM.scrollTop + lblCommentM.clientHeight >= lblCommentM.scrollHeight) && e.deltaY > 0;
190
+
191
+ // スクロール可能&上端または下端ではないなら処理をスキップ
192
+ if (!atTop && !atBottom) {
193
+ return;
194
+ }
195
+ }
196
+ e.preventDefault();
197
+ if (g_stateObj.keyInitial && wheelCnt === 0) {
198
+ changeMSelect(e.deltaY > 0 ? 1 : -1);
199
+ }
200
+ wheelCnt = (wheelCnt + 1) % wheelCycle;
201
+ });
202
+
203
+ // 初期表示用 (2秒後に選曲画面を表示)
204
+ if (_initFlg && !g_headerObj.customTitleUse) {
205
+ if (g_audioForMS) {
206
+ g_audioForMS.muted = true;
207
+ }
208
+ const mSelectTitleSprite = createEmptySprite(divRoot, `mSelectTitleSprite`,
209
+ g_windowObj.mSelectTitleSprite, g_cssObj.settings_DifSelector);
210
+ multiAppend(mSelectTitleSprite,
211
+ drawBackArrow(),
212
+ drawTitle(g_headerObj.packageNames),
213
+ );
214
+
215
+ let spriteOpacity = 1;
216
+ let fadeOpacity = null;
217
+ const fadeStartOpacity = g_timerHandler.setTimeout(() => {
218
+ g_timerHandler.clearTimeout(fadeStartOpacity);
219
+ setOpacity(spriteOpacity);
220
+ }, 2000);
221
+
222
+ const setOpacity = (_opacity) => {
223
+ if (_opacity <= 0) {
224
+ g_timerHandler.clearTimeout(fadeOpacity);
225
+ mSelectTitleSprite.style.display = C_DIS_NONE;
226
+ if (!g_stateObj.bgmMuteFlg && g_audioForMS) {
227
+ g_audioForMS.muted = false;
228
+ g_audioForMS.currentTime = g_headerObj.musicStarts[g_headerObj.musicIdxList[g_settings.musicIdxNum]] ?? 0;
229
+ if (g_audioForMS instanceof AudioPlayer) {
230
+ // AudioPlayerはシークを適用するために再起動が必要
231
+ g_audioForMS.pause();
232
+ g_audioForMS.play();
233
+ }
234
+ }
235
+ } else {
236
+ mSelectTitleSprite.style.opacity = _opacity;
237
+ fadeOpacity = g_timerHandler.setTimeout(() => {
238
+ spriteOpacity -= 0.25;
239
+ setOpacity(spriteOpacity);
240
+ }, 50);
241
+ }
242
+ };
243
+ }
244
+ } else if (!g_headerObj.customTitleUse) {
245
+ // 曲名文字描画(曲名は譜面データから取得)
246
+ divRoot.appendChild(
247
+ g_headerObj.musicSelectUse
248
+ ? drawTitle(g_headerObj.musicTitlesForView[g_settings.musicIdxNum], g_headerObj.viewLists[0] + 1)
249
+ : drawTitle()
250
+ );
251
+ }
252
+
253
+ // クレジット表示
254
+ externalWebTitle();
255
+
256
+ if (g_errMsgObj.title !== ``) {
257
+ makeWarningWindow();
258
+ }
259
+
260
+ // ユーザカスタムイベント(初期)
261
+ safeExecuteCustomHooks(`g_customJsObj.title`, g_customJsObj.title);
262
+
263
+ // バージョン情報取得
264
+ let customVersion = ``;
265
+ if (g_localVersion !== ``) {
266
+ customVersion = ` / ${g_localVersion}`;
267
+ }
268
+ if (g_localVersion2 !== ``) {
269
+ customVersion += ` / ${g_localVersion2}`;
270
+ }
271
+ const releaseDate = (g_headerObj.releaseDate !== `` ? ` @${g_headerObj.releaseDate}` : ``);
272
+ const remoteDomainInfo = g_remoteDomain !== null ? ` (${g_remoteDomain})` : ``;
273
+ const versionName = `&copy; 2018-${g_revisedDate.slice(0, 4)} ティックル, CW ${g_version}${remoteDomainInfo}${customVersion}${releaseDate}`;
274
+ const getLinkSiz = _name => getFontSize2(_name, g_sWidth / 2 - 20, { maxSiz: g_limitObj.lnkSiz, minSiz: 12 });
275
+
276
+ /**
277
+ * クレジット用リンク作成
278
+ * @param {string} _id
279
+ * @param {string} _text
280
+ * @param {string} _url
281
+ * @returns {HTMLDivElement}
282
+ */
283
+ const createCreditBtn = (_id, _text, _url) =>
284
+ createCss2Button(_id, _text, () => true,
285
+ { ...g_lblPosObj[_id], siz: getLinkSiz(_text), whiteSpace: `normal`, resetFunc: () => openLink(_url) }, g_cssObj.button_Default);
286
+
287
+ if (g_headerObj.musicSelectUse && getQueryParamVal(`scoreId`) === null) {
288
+ // 選曲モードではクレジット表示は別で行われているため表示しない
289
+ } else {
290
+ if (tmpCreatorList.length === 0) {
291
+ tmpCreatorList.push(g_headerObj.creatorNames[0]);
292
+ }
293
+ const [creatorName, creatorUrl] = getCreatorInfo(tmpCreatorList);
294
+
295
+ multiAppend(divRoot,
296
+
297
+ // Click Here
298
+ createCss2Button(`btnStart`, g_lblNameObj.clickHere, () => {
299
+ g_timerHandler.clearTimeout(g_timeoutEvtTitleId);
300
+ g_keyObj.prevKey = `Dummy${g_settings.musicIdxNum}`;
301
+ }, {
302
+ x: g_btnX(), w: g_btnWidth(), siz: g_limitObj.titleSiz, resetFunc: () => optionInit(),
303
+ }, g_cssObj.button_Start),
304
+
305
+ // 製作者表示
306
+ createCreditBtn(`lnkMaker`, `${g_lblNameObj.maker}: ${g_headerObj.musicSelectUse ? creatorName : g_headerObj.tuningInit}`,
307
+ g_headerObj.musicSelectUse ? creatorUrl : g_headerObj.creatorUrl),
308
+
309
+ // アーティスト表示
310
+ createCreditBtn(`lnkArtist`, `${g_lblNameObj.artist}: ${g_headerObj.artistNames[g_settings.musicIdxNum]}`, g_headerObj.artistUrls[g_settings.musicIdxNum]),
311
+ );
312
+ }
313
+
314
+ multiAppend(divRoot,
315
+
316
+ // Reset
317
+ createCss2Button(`btnReset`, g_lblNameObj.dataReset, () => {
318
+ g_timerHandler.clearTimeout(g_timeoutEvtTitleId);
319
+ g_handler.removeListener(wheelHandler);
320
+ dataMgtInit();
321
+ }, g_lblPosObj.btnReset, g_cssObj.button_Reset),
322
+
323
+ // ロケール切替
324
+ createCss2Button(`btnReload`, g_localeObj.val, () => true, {
325
+ ...g_lblPosObj.btnReload,
326
+ resetFunc: () => {
327
+ g_localeObj.num = (++g_localeObj.num) % g_localeObj.list.length;
328
+ g_langStorage.locale = g_localeObj.list[g_localeObj.num];
329
+ localStorage.setItem(`danoni-locale`, JSON.stringify(g_langStorage));
330
+ location.reload();
331
+ },
332
+ }, g_cssObj.button_Start),
333
+
334
+ // ヘルプ
335
+ createCss2Button(`btnHelp`, `?`, () => true, {
336
+ ...g_lblPosObj.btnHelp,
337
+ resetFunc: () => openLink(g_lblNameObj.helpUrl),
338
+ }, g_cssObj.button_Setting),
339
+
340
+ // バージョン描画
341
+ createCss2Button(`lnkVersion`, versionName, () => true, {
342
+ ...g_lblPosObj.lnkVersion,
343
+ siz: getFontSize2(versionName, g_sWidth * 3 / 4 - 20, { maxSiz: 12 }),
344
+ resetFunc: () => openLink(`https://github.com/cwtickle/danoniplus`),
345
+ }, g_cssObj.button_Tweet),
346
+
347
+ // セキュリティリンク
348
+ createCss2Button(`lnkComparison`, g_emojiObj.shield, () => true, {
349
+ ...g_lblPosObj.lnkComparison,
350
+ resetFunc: () => openLink(g_lblNameObj.securityUrl),
351
+ }, g_cssObj.button_Tweet),
352
+ );
353
+
354
+ // コメントエリア作成
355
+ if (g_headerObj.commentVal !== ``) {
356
+
357
+ // コメント文の加工
358
+ const convCommentVal = convertStrToVal(g_headerObj.commentVal);
359
+ if (g_headerObj.commentExternal) {
360
+ if (document.getElementById(`commentArea`) !== null) {
361
+ commentArea.innerHTML = convCommentVal;
362
+ }
363
+ } else {
364
+ multiAppend(divRoot,
365
+ createDivCss2Label(`lblComment`, convCommentVal, g_lblPosObj.lblComment),
366
+ createCss2Button(`btnComment`, g_lblNameObj.comment, () => {
367
+ const lblCommentDef = lblComment.style.display;
368
+ lblComment.style.display = (lblCommentDef === C_DIS_NONE ? C_DIS_INHERIT : C_DIS_NONE);
369
+ }, g_lblPosObj.btnComment, g_cssObj.button_Default),
370
+ );
371
+ if (g_headerObj.musicSelectUse && getQueryParamVal(`scoreId`) === null) {
372
+ lblComment.style.height = wUnit(g_sHeight - 100);
373
+ }
374
+ setUserSelect(lblComment.style, `text`);
375
+ }
376
+ }
377
+
378
+ // マスクスプライトを作成
379
+ const maskTitleSprite = createMultipleSprite(`maskTitleSprite`, g_headerObj.maskTitleMaxDepth);
380
+ maskTitleSprite.style.pointerEvents = g_headerObj.masktitleButton ? C_DIS_AUTO : C_DIS_NONE;
381
+
382
+ /**
383
+ * タイトルのモーション設定
384
+ */
385
+ const flowTitleTimeline = () => {
386
+
387
+ // ユーザカスタムイベント(フレーム毎)
388
+ safeExecuteCustomHooks(`g_customJsObj.titleEnterFrame`, g_customJsObj.titleEnterFrame);
389
+
390
+ // 背景・マスクモーション、スキン変更
391
+ drawTitleResultMotion(g_currentPage);
392
+
393
+ thisTime = performance.now();
394
+ buffTime = thisTime - titleStartTime - g_scoreObj.titleFrameNum * 1000 / g_fps;
395
+
396
+ g_scoreObj.titleFrameNum++;
397
+ g_animationData.forEach(sprite => g_scoreObj[`${sprite}TitleFrameNum`]++);
398
+ g_timeoutEvtTitleId = g_timerHandler.setTimeout(flowTitleTimeline, 1000 / g_fps - buffTime);
399
+ };
400
+
401
+ g_timeoutEvtTitleId = g_timerHandler.setTimeout(flowTitleTimeline, 1000 / g_fps);
402
+
403
+ // キー操作イベント(デフォルト)
404
+ setShortcutEvent(g_currentPage, () => true, { dfEvtFlg: true });
405
+
406
+ document.oncontextmenu = () => true;
407
+ divRoot.oncontextmenu = () => false;
408
+
409
+ safeExecuteCustomHooks(`g_skinJsObj.title`, g_skinJsObj.title);
410
+ };
411
+
412
+ /**
413
+ * 外部のタイトル表示
414
+ */
415
+ const externalWebTitle = () => {
416
+ if (document.getElementById(`webMusicTitle`) !== null) {
417
+ webMusicTitle.innerHTML =
418
+ `<span style="font-size:${wUnit(32)}">${g_headerObj.musicTitlesForView[g_settings.musicIdxNum].join(`<br>`)}</span><br>
419
+ <span style="font-size:${wUnit(16)}">(Artist: <a href="${g_headerObj.artistUrls[g_settings.musicIdxNum]}" target="_blank">${g_headerObj.artistNames[g_settings.musicIdxNum]}</a>)</span>`;
420
+ }
421
+ };
422
+
423
+ /**
424
+ * 背景矢印の表示
425
+ * @param {string|number} _scoreId
426
+ * @returns {HTMLDivElement}
427
+ */
428
+ const drawBackArrow = (_scoreId = ``) =>
429
+ createColorObject2(`lblArrow`, {
430
+ x: (g_sWidth - 500) / 2, y: -15 + (g_sHeight - 500) / 2,
431
+ w: 500, h: 500, rotateEnabled: true,
432
+ background: makeColorGradation(g_headerObj.titlearrowgrds[0] ||
433
+ g_headerObj[`setColor${_scoreId}Org`]?.[0] || g_headerObj.setColorOrg[0], {
434
+ _defaultColorgrd: [false, `#eeeeee`],
435
+ _objType: `titleArrow`,
436
+ }), rotate: `titleArrow:${g_headerObj.titleArrowRotate}`,
437
+ });
438
+
439
+ /**
440
+ * タイトル文字の表示
441
+ * @param {string[]} _titleName
442
+ * @returns {HTMLDivElement}
443
+ */
444
+ const drawTitle = (_titleName = g_headerObj.musicTitleForView, _scoreId = ``) => {
445
+
446
+ // グラデーションの指定がない場合、
447
+ // 矢印色の1番目と3番目を使ってタイトルをグラデーション
448
+ const titlegrd1 = g_headerObj.titlegrds[0] || (g_headerObj[`setColor${_scoreId}Org`] ?
449
+ `${g_headerObj[`setColor${_scoreId}Org`][0]}:${g_headerObj[`setColor${_scoreId}Org`][2]}` : `${g_headerObj.setColorOrg[0]}:${g_headerObj.setColorOrg[2]}`);
450
+ const titlegrd2 = g_headerObj.titlegrds[1] || titlegrd1;
451
+
452
+ const titlegrds = [];
453
+ [titlegrd1, titlegrd2].forEach((titlegrd, j) =>
454
+ titlegrds[j] = makeColorGradation(titlegrd, { _defaultColorgrd: false, _objType: `titleMusic` }));
455
+
456
+ let titlefontsize = 64;
457
+ for (let j = 0; j < _titleName.length; j++) {
458
+ if (_titleName[j] !== ``) {
459
+ titlefontsize = getFontSize2(_titleName[j], g_sWidth - 100, { font: g_headerObj.titlefonts[j], maxSiz: titlefontsize });
460
+ }
461
+ }
462
+
463
+ // 変数 titlesize の定義 (使用例: |titlesize=40$20|)
464
+ const titlefontsizes = (g_headerObj.titlesize?.split(`$`).join(`,`).split(`,`) || [titlefontsize, titlefontsize]);
465
+ const titlefontsize1 = setIntVal(titlefontsizes[0], titlefontsize);
466
+ const titlefontsize2 = setIntVal(titlefontsizes[1], titlefontsize1);
467
+
468
+ // 変数 titlelineheight の定義 (使用例: |titlelineheight=50|)
469
+ const titlelineheight = (g_headerObj.titlelineheight !== `` ? g_headerObj.titlelineheight - (titlefontsize2 + 10) : 0);
470
+
471
+ const txtAnimations = [``, ``];
472
+ if (!g_headerObj.customTitleAnimationUse) {
473
+ for (let j = 0; j < txtAnimations.length; j++) {
474
+ txtAnimations[j] = `animation-name:${g_headerObj.titleAnimationName[j]};
475
+ animation-duration:${g_headerObj.titleAnimationDuration[j]}s;
476
+ animation-delay:${g_headerObj.titleAnimationDelay[j]}s;
477
+ animation-timing-function:${g_headerObj.titleAnimationTimingFunction[j]};`;
478
+ }
479
+ }
480
+ return createDivCss2Label(`lblmusicTitle`,
481
+ `<div id="lblmusicTitle1" style="
482
+ font-family:${g_headerObj.titlefonts[0]};
483
+ background: ${titlegrds[0]};
484
+ background-clip: text;
485
+ -webkit-background-clip: text;
486
+ color: rgba(255,255,255,0.0);
487
+ ${txtAnimations[0]}
488
+ " class="${g_headerObj.titleAnimationClass[0]}">
489
+ ${_titleName[0]}
490
+ </div>
491
+ <div id="lblmusicTitle2" style="
492
+ font-size:${wUnit(titlefontsize2)};
493
+ position:relative;left:${wUnit(g_headerObj.titlepos[1][0])};
494
+ top:${wUnit(g_headerObj.titlepos[1][1] + titlelineheight)};
495
+ font-family:${g_headerObj.titlefonts[1]};
496
+ background: ${titlegrds[1]};
497
+ background-clip: text;
498
+ -webkit-background-clip: text;
499
+ color: rgba(255,255,255,0.0);
500
+ ${txtAnimations[1]}
501
+ " class="${g_headerObj.titleAnimationClass[1]}">
502
+ ${_titleName[1] ?? ``}
503
+ </div>
504
+ `,
505
+ {
506
+ x: Number(g_headerObj.titlepos[0][0]), y: Number(g_headerObj.titlepos[0][1]),
507
+ w: g_sWidth, h: g_sHeight - 40, siz: titlefontsize1,
508
+ display: `flex`, flexDirection: `column`, justifyContent: `center`, alignItems: `center`,
509
+ }
510
+ );
511
+ };
512
+
513
+ /**
514
+ * 製作者情報の取得
515
+ * @param {string[]} _creatorList
516
+ * @returns {[string, string, number]}
517
+ */
518
+ const getCreatorInfo = (_creatorList) => {
519
+ const creatorName = makeDedupliArray(_creatorList).length === 1 ? _creatorList[0] : `Various`;
520
+ g_headerObj.makerView = g_headerObj.makerViewOrg ? true : creatorName === `Various`;
521
+ const creatorIdx = g_headerObj.tuningNames.findIndex(val => val === creatorName);
522
+ const creatorUrl = creatorIdx >= 0 ? g_headerObj.tuningUrls[creatorIdx] : ``;
523
+ return [creatorName, creatorUrl, creatorIdx];
524
+ };
525
+
526
+ /**
527
+ * BGMの停止
528
+ */
529
+ const pauseBGM = () => {
530
+ if (g_audioForMS) {
531
+ g_handler.removeListener(g_stateObj.bgmTimeupdateEvtId);
532
+ g_audioForMS.pause();
533
+ if (!(g_audioForMS instanceof AudioPlayer)) {
534
+ g_audioForMS.removeAttribute('src');
535
+ g_audioForMS.load();
536
+ }
537
+ }
538
+ [`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`, `bgmRestart`].forEach(id => {
539
+ if (g_stateObj[id]) {
540
+ g_timerHandler.clearTimeout(g_stateObj[id]);
541
+ g_stateObj[id] = null;
542
+ }
543
+ });
544
+ };
545
+
546
+ /**
547
+ * BGM再生処理
548
+ * @param {number} _num
549
+ * @param {number} _currentLoopNum
550
+ */
551
+ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
552
+ const FADE_STEP = 0.05 * g_stateObj.bgmVolume / 100;
553
+ const FADE_INTERVAL_MS = 100;
554
+ const FADE_DELAY_MS = 500;
555
+
556
+ const currentIdx = g_headerObj.musicIdxList[g_settings.musicIdxNum];
557
+ const musicUrl = getMusicUrl(g_headerObj.viewLists[0]);
558
+ const url = getFullMusicUrl(musicUrl);
559
+ const encodeFlg = listMatching(musicUrl, [`.js`, `.txt`], { suffix: `$` });
560
+ const musicStart = g_headerObj.musicStarts?.[currentIdx] ?? 0;
561
+ const musicEnd = g_headerObj.musicEnds?.[currentIdx] ?? 0;
562
+ const isTitle = () => g_currentPage === `title` && _currentLoopNum === g_settings.musicLoopNum;
563
+
564
+ /**
565
+ * 汎用フェード処理
566
+ * @param {number} startVolume - 開始音量 (0〜1)
567
+ * @param {number} endVolume - 終了音量 (0〜1)
568
+ * @param {number} step - 1ステップの増減量
569
+ * @param {Function} onEnd - フェード完了時の処理
570
+ * @param {Function} isValid - フェード継続条件(true: 継続, false: 中断)
571
+ * @returns {number} timeoutId
572
+ */
573
+ const fadeVolume = (startVolume, endVolume, step, onEnd, isValid) => {
574
+
575
+ // 開始時点で終了音量とイコールの場合は終了
576
+ if (startVolume === endVolume || step === 0) {
577
+ g_audioForMS.volume = endVolume;
578
+ onEnd(true);
579
+ return null;
580
+ }
581
+
582
+ let volume = startVolume;
583
+ g_audioForMS.volume = startVolume;
584
+
585
+ const stepFunc = () => {
586
+ // 継続条件チェック
587
+ if (!isValid()) {
588
+ onEnd(false); // 中断
589
+ return;
590
+ }
591
+
592
+ // 終了判定
593
+ const reached =
594
+ (startVolume < endVolume && volume >= endVolume) ||
595
+ (startVolume > endVolume && volume <= endVolume);
596
+
597
+ if (reached) {
598
+ g_audioForMS.volume = endVolume;
599
+ onEnd(true); // 正常終了
600
+ return;
601
+ }
602
+
603
+ // 音量更新
604
+ volume += step;
605
+ g_audioForMS.volume = Math.min(Math.max(volume, 0), 1);
606
+
607
+ // 次のステップへ
608
+ g_timerHandler.setTimeout(stepFunc, FADE_INTERVAL_MS);
609
+ };
610
+
611
+ return g_timerHandler.setTimeout(stepFunc, FADE_INTERVAL_MS);
612
+ };
613
+
614
+ /**
615
+ * 汎用ポーリング(監視)処理
616
+ * @param {Function} check - 条件チェック関数(true なら終了)
617
+ * @param {Function} onEnd - 終了時の処理
618
+ * @param {Function} isValid - 継続条件(true: 継続, false: 中断)
619
+ * @returns {number} timeoutId
620
+ */
621
+ const poll = (check, onEnd, isValid) => {
622
+ const step = () => {
623
+ // 継続条件チェック
624
+ if (!isValid()) {
625
+ onEnd(false); // 中断
626
+ return;
627
+ }
628
+
629
+ // 条件成立
630
+ if (check()) {
631
+ onEnd(true); // 正常終了
632
+ return;
633
+ }
634
+
635
+ // 次のチェックへ
636
+ g_timerHandler.setTimeout(step, FADE_INTERVAL_MS);
637
+ };
638
+
639
+ return g_timerHandler.setTimeout(step, FADE_INTERVAL_MS);
640
+ };
641
+
642
+ /**
643
+ * BGMのフェードアウトとシーク
644
+ */
645
+ const fadeOutAndSeek = () => {
646
+ const start = g_audioForMS.volume;
647
+ const end = 0;
648
+
649
+ g_stateObj.bgmFadeOut = fadeVolume(
650
+ start,
651
+ end,
652
+ -FADE_STEP,
653
+ /* onEnd */
654
+ (finished) => {
655
+ g_stateObj.bgmFadeOut = null;
656
+
657
+ if (!finished) return; // 中断された
658
+
659
+ g_audioForMS.pause();
660
+ g_audioForMS.currentTime = musicStart;
661
+
662
+ if (isTitle()) {
663
+ g_stateObj.bgmRestart = g_timerHandler.setTimeout(() => {
664
+ g_stateObj.bgmRestart = null;
665
+ fadeIn();
666
+ if (encodeFlg) repeatBGM();
667
+ }, FADE_DELAY_MS);
668
+ } else {
669
+ pauseBGM();
670
+ }
671
+ },
672
+ /* isValid */
673
+ () =>
674
+ isTitle() &&
675
+ g_stateObj.bgmFadeOut !== null
676
+ );
677
+ };
678
+
679
+ /**
680
+ * BGMのフェードイン
681
+ */
682
+ const fadeIn = () => {
683
+ if (!(g_audioForMS instanceof AudioPlayer) && !g_audioForMS.src) return;
684
+
685
+ const start = 0;
686
+ const end = g_stateObj.bgmVolume / 100;
687
+
688
+ g_audioForMS.volume = 0;
689
+ g_audioForMS.play();
690
+
691
+ g_stateObj.bgmFadeIn = fadeVolume(
692
+ start,
693
+ end,
694
+ FADE_STEP,
695
+ /* onEnd */
696
+ () => {
697
+ g_stateObj.bgmFadeIn = null;
698
+ },
699
+ /* isValid */
700
+ () =>
701
+ isTitle() &&
702
+ g_stateObj.bgmFadeIn !== null &&
703
+ currentIdx === g_headerObj.musicIdxList[g_settings.musicIdxNum]
704
+ );
705
+ };
706
+
707
+ /**
708
+ * BGMのループ処理 (base64エンコード時用)
709
+ * - base64エンコード時はtimeupdateイベントが発火しないため、監視しながらループ処理を行う
710
+ */
711
+ const repeatBGM = () => {
712
+ const numAtStart = g_settings.musicIdxNum;
713
+
714
+ g_stateObj.bgmLooped = poll(
715
+ /* check */
716
+ () => {
717
+ try {
718
+ return (
719
+ g_audioForMS.elapsedTime >= musicEnd ||
720
+ numAtStart !== g_settings.musicIdxNum
721
+ );
722
+ } catch {
723
+ return true; // エラー時は終了扱い
724
+ }
725
+ },
726
+ /* onEnd */
727
+ (finished) => {
728
+ g_stateObj.bgmLooped = null;
729
+ if (finished) {
730
+ fadeOutAndSeek();
731
+ }
732
+ },
733
+ /* isValid */
734
+ () => g_stateObj.bgmLooped !== null
735
+ );
736
+ };
737
+
738
+ /**
739
+ * 既存のAudio/AudioPlayerをクローズ
740
+ */
741
+ const closeExistingAudio = () => {
742
+ if (g_stateObj.bgmTimeupdateEvtId !== null && g_stateObj.bgmTimeupdateEvtId !== undefined) {
743
+ g_handler.removeListener(g_stateObj.bgmTimeupdateEvtId);
744
+ g_stateObj.bgmTimeupdateEvtId = null;
745
+ }
746
+ if (g_audioForMS instanceof AudioPlayer) {
747
+ g_musicdata = ``;
748
+ g_audioForMS.close();
749
+ }
750
+ };
751
+
752
+ if (encodeFlg) {
753
+ try {
754
+ closeExistingAudio();
755
+ const cachedBuffer = getAudioBufferFromCache(url);
756
+ if (cachedBuffer !== undefined) {
757
+ const tmpAudio = new AudioPlayer();
758
+ tmpAudio.setBuffer(cachedBuffer);
759
+ g_audioForMS = tmpAudio;
760
+ } else {
761
+ await loadScript2(url);
762
+ musicInit();
763
+ if (!isTitle()) { g_musicdata = ``; return; }
764
+
765
+ const tmpAudio = new AudioPlayer();
766
+ const array = base64ToUint8Array(g_musicdata);
767
+ await tmpAudio.init(array.buffer);
768
+ if (!isTitle()) { g_musicdata = ``; tmpAudio.close(); return; }
769
+
770
+ cacheAudioBuffer(url, tmpAudio.getBuffer());
771
+ g_audioForMS = tmpAudio;
772
+ }
773
+ g_audioForMS.volume = g_stateObj.bgmVolume / 100;
774
+ if (g_currentPage === `title` && musicEnd > 0) {
775
+ g_audioForMS.currentTime = musicStart;
776
+ g_audioForMS.play();
777
+ repeatBGM();
778
+ }
779
+ } catch (e) {
780
+ // 音源の読み込みに失敗した場合、エラーを表示
781
+ console.warn(`BGM load error: ${e}`);
782
+ }
783
+
784
+ } else {
785
+ // 既存の監視を解除し、AudioPlayer を確実にクローズ
786
+ closeExistingAudio();
787
+ g_audioForMS = new Audio();
788
+ g_audioForMS.src = url;
789
+ g_audioForMS.autoplay = false;
790
+ g_audioForMS.volume = g_stateObj.bgmVolume / 100;
791
+ const loadedMeta = g_handler.addListener(g_audioForMS, `loadedmetadata`, () => {
792
+ g_handler.removeListener(loadedMeta);
793
+ if (!isTitle()) {
794
+ return;
795
+ }
796
+ g_audioForMS.currentTime = musicStart;
797
+ g_audioForMS.play();
798
+ }, { once: true });
799
+
800
+ if (musicEnd > 0) {
801
+ g_stateObj.bgmTimeupdateEvtId = g_handler.addListener(g_audioForMS, "timeupdate", () => {
802
+ if (g_audioForMS.currentTime >= musicEnd) {
803
+ fadeOutAndSeek();
804
+ }
805
+ });
806
+ }
807
+ }
808
+ };
809
+
810
+ /**
811
+ * 選曲ボタンを押したときの処理
812
+ * @param {number} _num
813
+ * @param {boolean} _initFlg
814
+ */
815
+ const changeMSelect = (_num, _initFlg = false) => {
816
+ if (document.getElementById(`lblComment`) !== null && lblComment.style.display === C_DIS_INHERIT) {
817
+ return;
818
+ }
819
+ const limitedMLength = 35;
820
+ pauseBGM();
821
+
822
+ // 選択方向に合わせて楽曲リスト情報を再取得
823
+ for (let j = -g_settings.mSelectableTerms; j <= g_settings.mSelectableTerms; j++) {
824
+ const idx = g_headerObj.musicIdxList[(j + _num + g_settings.musicIdxNum + g_headerObj.musicIdxList.length * 10) % g_headerObj.musicIdxList.length];
825
+ if (j === 0) {
826
+ } else {
827
+ document.getElementById(`btnMusicSelect${j}`).style.fontSize =
828
+ getFontSize2(g_headerObj.musicTitles[idx].slice(0, limitedMLength), g_btnWidth(1 / 2));
829
+ document.getElementById(`btnMusicSelect${j}`).innerHTML =
830
+ `${g_headerObj.musicTitles[idx].slice(0, limitedMLength)}${g_headerObj.musicTitles[idx].length > limitedMLength ? '...' : ''}<br>` +
831
+ `<span style="font-size:0.7em;line-height:9px"> / ${g_headerObj.artistNames[idx]}</span>`;
832
+ }
833
+ }
834
+ // 現在選択中の楽曲IDを再設定
835
+ g_settings.musicIdxNum = (g_settings.musicIdxNum + _num + g_headerObj.musicIdxList.length) % g_headerObj.musicIdxList.length;
836
+
837
+ // 楽曲の多重読込防止(この値が変化していれば読み込まない)
838
+ g_settings.musicLoopNum++;
839
+ const currentLoopNum = g_settings.musicLoopNum;
840
+
841
+ // 選択した楽曲に対応する譜面番号、製作者情報、曲長を取得
842
+ g_headerObj.viewLists = [];
843
+ const keyList = [], creatorList = [], playingFrameList = [], bpmList = [], difNameList = [], diffiList = [], notesList = [];
844
+ const targetIdx = g_headerObj.musicIdxList[(g_settings.musicIdxNum + g_headerObj.musicIdxList.length * 20) % g_headerObj.musicIdxList.length];
845
+ g_headerObj.musicNos.forEach((val, j) => {
846
+ if ((g_headerObj.musicGroups?.[val] ?? val) === targetIdx) {
847
+ g_headerObj.viewLists.push(j);
848
+ keyList.push(g_headerObj.keyLabels[j]);
849
+ creatorList.push(g_headerObj.creatorNames[j]);
850
+ playingFrameList.push(g_detailObj.playingFrameWithBlank[j]);
851
+ bpmList.push(g_headerObj.bpms[g_headerObj.musicNos[j]]);
852
+ difNameList.push(`${g_headerObj.keyLabels[j]} / ${g_headerObj.difLabels[j]}`);
853
+ diffiList.push(g_headerObj.difficulties[j]);
854
+ notesList.push(`Arrows: ${sumData(g_detailObj.arrowCnt[j])}+${sumData(g_detailObj.frzCnt[j])}`);
855
+ }
856
+ });
857
+ const playingFrames = makeDedupliArray(playingFrameList.map(val => transFrameToTimer(val))).join(`, `);
858
+ const bpm = makeDedupliArray(bpmList).join(`, `);
859
+ const [creatorName, creatorUrl, creatorIdx] = getCreatorInfo(creatorList);
860
+ const creatorLink = creatorIdx >= 0 ?
861
+ `<a href="${creatorUrl}" target="_blank">${creatorName}</a>` : creatorName;
862
+
863
+ // 選択した楽曲の情報表示
864
+ const idx = g_headerObj.musicIdxList[g_settings.musicIdxNum];
865
+ document.getElementById(`lblMusicSelect`).innerHTML =
866
+ `<span style="font-size:${getFontSize2(g_headerObj.musicTitlesForView[idx].join(`<br>`), g_btnWidth(1 / 2), { maxSiz: 18 })}px;` +
867
+ `font-weight:bold">${g_headerObj.musicTitlesForView[idx].join(`<br>`)}</span>`;
868
+ document.getElementById(`lblMusicSelectDetail`).innerHTML =
869
+ `Maker: ${creatorLink} / Artist: <a href="${g_headerObj.artistUrls[idx]}" target="_blank">` +
870
+ `${g_headerObj.artistNames[idx]}</a><br>Duration: ${playingFrames} / BPM: ${bpm}`;
871
+
872
+ // 選択した楽曲で使われているキー種の一覧を作成
873
+ deleteChildspriteAll(`keyTitleSprite`);
874
+ makeDedupliArray(keyList).sort((a, b) => parseInt(a) - parseInt(b))
875
+ .forEach((val, j) => keyTitleSprite.appendChild(
876
+ createDivCss2Label(`btnKeyTitle${val}`, val, { ...g_lblPosObj.btnKeyTitle, x: 10 + j * 40 })));
877
+
878
+ // 選択した楽曲の選択位置を表示
879
+ lblMusicCnt.innerHTML = `${g_settings.musicIdxNum + 1} / ${g_headerObj.musicIdxList.length}`;
880
+
881
+ // 楽曲別のローカルストレージを再取得
882
+ loadLocalStorage(g_settings.musicIdxNum);
883
+ viewKeyStorage.cache = new Map();
884
+
885
+ // 初期化もしくは楽曲変更時に速度を初期化
886
+ if (_initFlg || Math.abs(_num) % g_headerObj.musicIdxList.length !== 0) {
887
+ g_stateObj.speed = g_headerObj.initSpeeds[g_headerObj.viewLists[0]];
888
+ g_settings.speedNum = getCurrentNo(g_settings.speeds, g_stateObj.speed);
889
+ }
890
+
891
+ // 譜面情報、コメント文の加工
892
+ lblDifNameInfoM.innerHTML = ``;
893
+ lblDiffiInfoM.innerHTML = ``;
894
+ lblNotesInfoM.innerHTML = ``;
895
+ let notesInfo = ``;
896
+ for (let j = 0; j < difNameList.length; j++) {
897
+ let noteInfo = `${difNameList[j]}`;
898
+ if (makeDedupliArray(creatorList).length > 1) {
899
+ noteInfo += ` (${creatorList[j]})`;
900
+ }
901
+ lblDifNameInfoM.innerHTML += g_headerObj.difCustomLink[g_headerObj.viewLists[j]] !== undefined
902
+ ? `<a href="${g_headerObj.difCustomLink[g_headerObj.viewLists[j]]}" target="_blank" rel="noopener noreferrer">${noteInfo}</a>`
903
+ : noteInfo;
904
+ lblDifNameInfoM.innerHTML += `<br>`;
905
+ notesInfo += `${noteInfo}<br>`;
906
+
907
+ const difColorPart = g_headerObj.difColorList.find(val => diffiList[j] < val.threshold);
908
+ lblDiffiInfoM.innerHTML += `${diffiList[j] > 0
909
+ ? `<span style="color:${difColorPart?.color || ''}">${diffiList[j]}</span>`
910
+ : `-`}<br>`;
911
+ lblNotesInfoM.innerHTML += `/ ${notesList[j]}<br>`;
912
+ }
913
+ lblDifNameInfoM.style.fontSize = wUnit(getFontSize2(notesInfo,
914
+ g_lblPosObj.lblDifNameInfoM.w, { maxSiz: g_lblPosObj.lblDifNameInfoM.siz }));
915
+ lblDiffiInfoM.style.fontSize = lblDifNameInfoM.style.fontSize;
916
+ lblNotesInfoM.style.fontSize = lblDifNameInfoM.style.fontSize;
917
+ lblCommentInfoM.style.top = wUnit(getStrHeight(lblDifNameInfoM.innerHTML, parseFloat(lblDifNameInfoM.style.fontSize)));
918
+ lblCommentInfoM.innerHTML = convertStrToVal(g_headerObj[`commentVal${g_settings.musicIdxNum}`]);
919
+ lblCommentM.scrollTop = 0;
920
+
921
+ // BGM再生処理
922
+ if (!g_stateObj.bgmMuteFlg) {
923
+ if (_initFlg) {
924
+ playBGM(_num);
925
+ } else {
926
+ g_timerHandler.setTimeout(() => {
927
+ if (currentLoopNum === g_settings.musicLoopNum) {
928
+ playBGM(_num, currentLoopNum);
929
+ }
930
+ }, 500);
931
+ }
932
+ }
933
+
934
+ // 選曲変更時のカスタム関数実行
935
+ safeExecuteCustomHooks(`g_customJsObj.musicSelect`, g_customJsObj.musicSelect, g_settings.musicIdxNum);
936
+ };
937
+
938
+ /*-----------------------------------------------------------*/
939
+ /* Scene : DATA MANAGEMENT [pear] */
940
+ /*-----------------------------------------------------------*/
941
+
942
+ const dataMgtInit = () => {
943
+ clearWindow();
944
+ pauseBGM();
945
+ const prevPage = g_currentPage;
946
+ g_currentPage = `dataMgt`;
947
+ let selectedKey = g_keyObj.currentKey;
948
+
949
+ multiAppend(divRoot,
950
+
951
+ // 画面タイトル
952
+ getTitleDivLabel(`lblTitle`,
953
+ `<div class="settings_Title">DATA</div><div class="settings_Title2">MANAGEMENT</div>`
954
+ .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering),
955
+
956
+ createDescDiv(`dataDelMsg`, g_lblNameObj[`dataDelete${g_langStorage.safeMode}Desc`]),
957
+ );
958
+
959
+ // 各ボタン用のスプライトを作成
960
+ const optionsprite = createEmptySprite(divRoot, `optionsprite`, g_windowObj.dataSprite);
961
+
962
+ let reloadFlg = false;
963
+ const list = [C_FLG_OFF, C_FLG_ON];
964
+ const cssBarList = [C_FLG_OFF, C_FLG_ON];
965
+ const cssBgList = [g_settings.d_cssBgName, g_settings.d_cssBgName];
966
+
967
+ /**
968
+ * データ管理用ラベルの作成
969
+ * @param {string} _name
970
+ * @param {number} _heightPos
971
+ * @param {number} [x=0]
972
+ * @returns {HTMLDivElement}
973
+ */
974
+ const createMgtLabel = (_name, _heightPos, { x = 0 } = {}) =>
975
+ createDivCss2Label(`lbl${toCapitalize(_name)}`, getStgDetailName(toCapitalize(_name)), {
976
+ x, y: g_limitObj.setLblHeight * _heightPos + 40,
977
+ siz: g_limitObj.setLblSiz, align: C_ALIGN_LEFT,
978
+ });
979
+
980
+ /**
981
+ * データ管理用ボタンの作成
982
+ * @param {string} _name
983
+ * @param {number} _heightPos
984
+ * @param {number} _widthPos
985
+ * @param {number} [w=125]
986
+ * @param {Function} func
987
+ * @returns {HTMLDivElement}
988
+ */
989
+ const createMgtButton = (_name, _heightPos, _widthPos, { w = 125, func = () => true, ...rest } = {}) => {
990
+ const linkId = `btn${toCapitalize(_name)}`;
991
+ return createCss2Button(linkId, getStgDetailName(toCapitalize(_name)), () => {
992
+ const prevDisp = g_settings.dataMgtNum[_name];
993
+ const [prevBarColor, prevBgColor] = [cssBarList[prevDisp], cssBgList[prevDisp]];
994
+
995
+ g_settings.dataMgtNum[_name] = (g_settings.dataMgtNum[_name] + 1) % 2;
996
+ g_stateObj[`dm_${_name}`] = list[g_settings.dataMgtNum[_name]];
997
+
998
+ const nextDisp = g_settings.dataMgtNum[_name];
999
+ const [nextBarColor, nextBgColor] = [cssBarList[nextDisp], cssBgList[nextDisp]];
1000
+ document.getElementById(linkId).classList.replace(g_cssObj[`button_${prevBarColor}`], g_cssObj[`button_${nextBarColor}`]);
1001
+ document.getElementById(linkId).classList.replace(g_cssObj[`button_${prevBgColor}`], g_cssObj[`button_${nextBgColor}`]);
1002
+ func();
1003
+ }, {
1004
+ x: _widthPos * (w + 5) + 20, y: g_limitObj.setLblHeight * _heightPos + 40,
1005
+ w, h: 20, siz: g_limitObj.setLblSiz, borderStyle: `solid`, title: g_msgObj[_name], ...rest
1006
+ }, g_cssObj[`button_${cssBgList[g_settings.dataMgtNum[_name]]}`], g_cssObj[`button_${cssBarList[g_settings.dataMgtNum[_name]]}`]);
1007
+ };
1008
+
1009
+ multiAppend(optionsprite,
1010
+ createMgtLabel(`workData`, 0),
1011
+ createMgtButton(`environment`, 1.5, 0),
1012
+ createMgtButton(`highscores`, 2.5, 0),
1013
+ createMgtButton(`customKey`, 3.5, 0),
1014
+ createMgtButton(`others`, 4.5, 0),
1015
+ createMgtLabel(`keyData`, 6),
1016
+ createDivCss2Label(`lblTargetKey`, `(${getKeyName(selectedKey)})`, {
1017
+ x: 90, y: g_limitObj.setLblHeight * 6 + 40,
1018
+ siz: g_limitObj.setLblSiz, align: C_ALIGN_LEFT,
1019
+ })
1020
+ );
1021
+
1022
+ g_localStorageMgt = sortObjectByKeys(parseStorageData(g_localStorageUrl));
1023
+
1024
+ multiAppend(divRoot,
1025
+
1026
+ // 保存データの表示
1027
+ createDivCss2Label(`lblWorkDataView`,
1028
+ viewKeyStorage(`workStorage`), g_lblPosObj.lblWorkDataView),
1029
+ createDivCss2Label(`lblKeyDataView`, viewKeyStorage(`keyStorage`, selectedKey), g_lblPosObj.lblKeyDataView),
1030
+
1031
+ // 保存データの出力ボタン
1032
+ createCss2Button(`btnWorkStorage`, g_lblNameObj.b_copyStorage, () =>
1033
+ copyTextToClipboard(formatObject(g_storageFunc.get(`workStorage`)(), 0, { colorFmt: false }), g_msgInfoObj.I_0006),
1034
+ g_lblPosObj.btnWorkStorage, g_cssObj.button_Default, g_cssObj.button_ON),
1035
+ createCss2Button(`btnKeyStorage`, g_lblNameObj.b_copyStorage, () =>
1036
+ copyTextToClipboard(formatObject(g_storageFunc.get(`keyStorage`)(selectedKey), 0, { colorFmt: false }), g_msgInfoObj.I_0006),
1037
+ g_lblPosObj.btnKeyStorage, g_cssObj.button_Default, g_cssObj.button_ON),
1038
+ );
1039
+ setUserSelect($id(`lblWorkDataView`), `text`);
1040
+ setUserSelect($id(`lblKeyDataView`), `text`);
1041
+
1042
+ const keyList = makeDedupliArray(g_headerObj.keyLabels).sort((a, b) => parseInt(a) - parseInt(b));
1043
+ const keyListSprite = createEmptySprite(optionsprite, `keyListSprite`, g_windowObj.keyListSprite);
1044
+ keyList.forEach((key, j) => {
1045
+ g_stateObj[`dm_${key}`] = C_FLG_OFF;
1046
+ g_settings.dataMgtNum[key] = 0;
1047
+
1048
+ const keyWidth = Math.min(Math.max(50, getStrWidth(getKeyName(key), g_limitObj.setLblSiz, getBasicFont())), 80);
1049
+ keyListSprite.appendChild(createMgtButton(key, j - 2, 0, {
1050
+ w: keyWidth,
1051
+ siz: getFontSize2(getKeyName(key), keyWidth, { maxSiz: g_limitObj.setLblSiz, minSiz: 10 }),
1052
+ }));
1053
+ document.getElementById(`btn${toCapitalize(key)}`).innerHTML = getKeyName(key);
1054
+
1055
+ keyListSprite.appendChild(createCss2Button(`btnView${key}`, ``, evt => {
1056
+ keyList.forEach(keyx => {
1057
+ document.getElementById(`btnView${keyx}`).classList.replace(g_cssObj.button_Next, g_cssObj.button_Default);
1058
+ document.getElementById(`btnView${keyx}`).classList.replace(g_cssObj.button_ON, g_cssObj.button_OFF);
1059
+ document.getElementById(`btnView${keyx}`).textContent = ``;
1060
+ });
1061
+ document.getElementById(`btnView${key}`).classList.replace(g_cssObj.button_Default, g_cssObj.button_Next);
1062
+ document.getElementById(`btnView${key}`).classList.replace(g_cssObj.button_OFF, g_cssObj.button_ON);
1063
+ selectedKey = key;
1064
+ evt.target.textContent = `x`;
1065
+ lblKeyDataView.innerHTML = viewKeyStorage(`keyStorage`, key);
1066
+ lblKeyDataView.scrollTop = 0;
1067
+ lblTargetKey.innerHTML = `(${getKeyName(key)})`;
1068
+ }, {
1069
+ x: 0, y: g_limitObj.setLblHeight * (j - 2) + 40, w: 16, h: 20, siz: 12, borderStyle: `solid`
1070
+ }, g_cssObj.button_Default, g_cssObj.button_OFF));
1071
+ });
1072
+ document.getElementById(`btnView${selectedKey}`).click();
1073
+
1074
+ // ユーザカスタムイベント(初期)
1075
+ safeExecuteCustomHooks(`g_customJsObj.dataMgt`, g_customJsObj.dataMgt);
1076
+
1077
+ multiAppend(divRoot,
1078
+ createCss2Button(`btnBack`, g_lblNameObj.b_back, () => true, {
1079
+ ...g_lblPosObj.btnResetBack,
1080
+ resetFunc: () => [`title`, `precondition`].includes(prevPage) ? titleInit() : g_moveSettingWindow(false),
1081
+ }, g_cssObj.button_Back),
1082
+
1083
+ createCss2Button(`btnPrecond`, g_lblNameObj.b_precond, () => true, {
1084
+ ...g_lblPosObj.btnPrecond,
1085
+ resetFunc: () => preconditionInit(),
1086
+ }, g_cssObj.button_Setting),
1087
+
1088
+ createCss2Button(`btnSafeMode`, g_lblNameObj.b_safeMode +
1089
+ (g_langStorage.safeMode === C_FLG_ON ? C_FLG_OFF : C_FLG_ON), () => {
1090
+ if (window.confirm(g_msgObj[`safeMode${g_langStorage.safeMode}Confirm`])) {
1091
+ g_langStorage.safeMode = g_langStorage.safeMode === C_FLG_ON ? C_FLG_OFF : C_FLG_ON;
1092
+ localStorage.setItem(`danoni-locale`, JSON.stringify(g_langStorage));
1093
+ location.reload();
1094
+ }
1095
+ }, g_lblPosObj.btnSafeMode, g_cssObj.button_Setting),
1096
+
1097
+ createCss2Button(`btnReset`, g_lblNameObj.b_cReset, () => {
1098
+ reloadFlg = false;
1099
+ const backupData = new Map();
1100
+
1101
+ const selectedData = Object.keys(g_stateObj)
1102
+ .filter(key => key.startsWith('dm_') && g_stateObj[key] === C_FLG_ON)
1103
+ .map(key => key.slice(`dm_`.length));
1104
+
1105
+ if (selectedData.length === 0) {
1106
+ window.alert(g_msgObj.noDataSelected);
1107
+ return;
1108
+ }
1109
+
1110
+ if (window.confirm(g_msgObj.dataResetConfirm +
1111
+ `\n\n${selectedData.map(val => `- ${g_msgObj[val] || g_msgObj.keyTypes.split('{0}').join(val)}`).join(`\n`)}`)) {
1112
+ selectedData.forEach(key => {
1113
+ if (g_resetFunc.has(key)) {
1114
+ backupData.set(key, JSON.parse(JSON.stringify(g_localStorageMgt)));
1115
+ g_resetFunc.get(key)();
1116
+ localStorage.setItem(g_localStorageUrl, JSON.stringify(g_localStorageMgt));
1117
+
1118
+ } else if (keyList.includes(key)) {
1119
+ const storage = parseStorageData(`danonicw-${key}k`);
1120
+
1121
+ if (Object.keys(storage).length > 0) {
1122
+ backupData.set(key, JSON.parse(JSON.stringify(storage)));
1123
+ g_settings.keyStorages.forEach(val => delete storage[val]);
1124
+ localStorage.setItem(`danonicw-${key}k`, JSON.stringify(storage));
1125
+ } else {
1126
+ backupData.set(`XX` + key, JSON.parse(JSON.stringify(g_localStorageMgt)));
1127
+ g_settings.keyStorages.forEach(val => delete g_localStorageMgt[`${val}${key}`]);
1128
+ localStorage.setItem(g_localStorageUrl, JSON.stringify(g_localStorageMgt));
1129
+ }
1130
+ }
1131
+ });
1132
+ reloadFlg = true;
1133
+ sessionStorage.setItem(`resetBackup${g_settings.musicIdxNum}`, JSON.stringify(Array.from(backupData.entries())));
1134
+ }
1135
+ }, {
1136
+ ...g_lblPosObj.btnResetN,
1137
+ visibility: g_langStorage.safeMode === C_FLG_OFF ? C_DIS_INHERIT : `hidden`,
1138
+ resetFunc: () => {
1139
+ if (reloadFlg) {
1140
+ location.reload();
1141
+ }
1142
+ },
1143
+ }, g_cssObj.button_Reset),
1144
+
1145
+ // リカバリー用のボタン
1146
+ createCss2Button(`btnUndo`, g_lblNameObj.b_undo, () => {
1147
+ const backup = JSON.parse(sessionStorage.getItem(`resetBackup${g_settings.musicIdxNum}`));
1148
+ if (backup && window.confirm(g_msgObj.dataRestoreConfirm)) {
1149
+ backup.forEach(([key, data]) => {
1150
+ if (g_resetFunc.has(key) || keyList.includes(key.slice(`XX`.length))) {
1151
+ Object.assign(g_localStorageMgt, data);
1152
+ localStorage.setItem(g_localStorageUrl, JSON.stringify(g_localStorageMgt));
1153
+ } else if (keyList.includes(key)) {
1154
+ localStorage.setItem(`danonicw-${key}k`, JSON.stringify(data));
1155
+ }
1156
+ });
1157
+ sessionStorage.removeItem(`resetBackup${g_settings.musicIdxNum}`);
1158
+ location.reload();
1159
+ }
1160
+ }, g_lblPosObj.btnUndo, g_cssObj.button_Tweet)
1161
+ );
1162
+ if (sessionStorage.getItem(`resetBackup${g_settings.musicIdxNum}`) === null) {
1163
+ btnUndo.style.display = C_DIS_NONE;
1164
+ }
1165
+
1166
+ // キー操作イベント(デフォルト)
1167
+ setShortcutEvent(g_currentPage, () => true, { dfEvtFlg: true });
1168
+
1169
+ document.oncontextmenu = () => true;
1170
+ divRoot.oncontextmenu = () => false;
1171
+
1172
+ safeExecuteCustomHooks(`g_skinJsObj.dataMgt`, g_skinJsObj.dataMgt);
1173
+ };
1174
+
1175
+
1176
+ /*-----------------------------------------------------------*/
1177
+ /* Scene : PRECONDITION [mango] */
1178
+ /*-----------------------------------------------------------*/
1179
+
1180
+ const preconditionInit = () => {
1181
+ clearWindow();
1182
+ pauseBGM();
1183
+ const prevPage = g_currentPage;
1184
+ g_currentPage = `precondition`;
1185
+
1186
+ multiAppend(divRoot,
1187
+
1188
+ // 画面タイトル
1189
+ getTitleDivLabel(`lblTitle`,
1190
+ `<div class="settings_Title">PRECONDITION</div>`
1191
+ .replace(/[\t\n]/g, ``), 0, 15, g_cssObj.flex_centering),
1192
+
1193
+ createDivCss2Label(`lblPrecondView`, viewKeyStorage(`g_rootObj`), g_lblPosObj.lblPrecondView),
1194
+ createCss2Button(`btnPrecondView`, g_lblNameObj.b_copyStorage, () =>
1195
+ copyTextToClipboard((() => {
1196
+ const key = g_settings.preconditions[g_settings.preconditionNum * numOfPrecs + g_settings.preconditionNumSub];
1197
+ if (key === `g_editorTmp2`) {
1198
+ return g_editorTmp2.replaceAll(`<br>`, `\r\n`).replaceAll(`&nbsp;`, ` `);
1199
+ } else {
1200
+ return viewKeyStorage(key, ``, false);
1201
+ }
1202
+ })(), g_msgInfoObj.I_0007),
1203
+ g_lblPosObj.btnPrecondView, g_cssObj.button_Default, g_cssObj.button_ON),
1204
+ );
1205
+ setUserSelect($id(`lblPrecondView`), `text`);
1206
+
1207
+ // 1ページあたりに表示するオブジェクト数
1208
+ const numOfPrecs = Math.round((g_btnWidth(1) / 500) / 2 * 10) * 2;
1209
+
1210
+ // ボタン名切り替え
1211
+ const switchPreconditions = () => {
1212
+ g_settings.preconditionNum = nextPos(g_settings.preconditionNum, 1, Math.round(g_settings.preconditions.length / numOfPrecs) + 1);
1213
+ for (let j = 0; j < Math.min(g_settings.preconditions.length, numOfPrecs); j++) {
1214
+ if (g_settings.preconditionNum * numOfPrecs + j < g_settings.preconditions.length) {
1215
+ document.getElementById(`btnPrecond${j}`).innerHTML =
1216
+ g_settings.preconditions[g_settings.preconditionNum * numOfPrecs + j];
1217
+ document.getElementById(`btnPrecond${j}`).style.visibility = `visible`;
1218
+ } else {
1219
+ document.getElementById(`btnPrecond${j}`).style.visibility = `hidden`;
1220
+ }
1221
+ }
1222
+ btnPrecond0.click();
1223
+ };
1224
+
1225
+ // オブジェクト表示ボタンの作成
1226
+ for (let j = 0; j < Math.min(g_settings.preconditions.length, numOfPrecs); j++) {
1227
+ divRoot.appendChild(createCss2Button(`btnPrecond${j}`, g_settings.preconditions[j], evt => {
1228
+ for (let k = 0; k < Math.min(g_settings.preconditions.length, numOfPrecs); k++) {
1229
+ document.getElementById(`btnPrecond${k}`).classList.replace(g_cssObj.button_Reset, g_cssObj.button_Default);
1230
+ }
1231
+ lblPrecondView.innerHTML = viewKeyStorage(g_settings.preconditions[g_settings.preconditionNum * numOfPrecs + j]);
1232
+ lblPrecondView.scrollTop = 0;
1233
+ g_settings.preconditionNumSub = j;
1234
+ evt.target.classList.replace(g_cssObj.button_Default, g_cssObj.button_Reset);
1235
+ }, {
1236
+ x: g_btnX() + g_btnWidth((j % (numOfPrecs / 2)) / (numOfPrecs / 2 + 1)),
1237
+ y: 70 + Number(j >= numOfPrecs / 2) * 20, w: g_btnWidth(1 / (numOfPrecs / 2 + 1)), h: 20, siz: 12,
1238
+ }, g_cssObj.button_Default));
1239
+ }
1240
+ btnPrecond0.classList.replace(g_cssObj.button_Default, g_cssObj.button_Reset);
1241
+
1242
+ // 次のオブジェクト表示群の表示
1243
+ divRoot.appendChild(createCss2Button(`btnPrecondNext`, `>`, () => switchPreconditions(), {
1244
+ x: g_btnX() + g_btnWidth(numOfPrecs / 2 / (numOfPrecs / 2 + 1)),
1245
+ y: 70, w: g_btnWidth(1 / Math.max((numOfPrecs / 2 + 1), 12)), h: 40, siz: 12,
1246
+ visibility: (g_settings.preconditions.length > numOfPrecs ? `visible` : `hidden`),
1247
+ }, g_cssObj.button_Setting));
1248
+
1249
+ // ユーザカスタムイベント(初期)
1250
+ safeExecuteCustomHooks(`g_customJsObj.precondition`, g_customJsObj.precondition);
1251
+
1252
+ multiAppend(divRoot,
1253
+
1254
+ // データ管理画面へ移動
1255
+ createCss2Button(`btnReset`, g_lblNameObj.dataReset, () => {
1256
+ dataMgtInit();
1257
+ }, g_lblPosObj.btnReset, g_cssObj.button_Reset),
1258
+
1259
+ createCss2Button(`btnBack`, g_lblNameObj.b_back, () => true, {
1260
+ ...g_lblPosObj.btnPrecond,
1261
+ resetFunc: () => {
1262
+ viewKeyStorage.cache = new Map();
1263
+ prevPage === `dataMgt` ? dataMgtInit() : g_moveSettingWindow(false);
1264
+ },
1265
+ }, g_cssObj.button_Back),
1266
+ );
1267
+ // キー操作イベント(デフォルト)
1268
+ setShortcutEvent(g_currentPage, () => true, { dfEvtFlg: true });
1269
+
1270
+ document.oncontextmenu = () => true;
1271
+ divRoot.oncontextmenu = () => true;
1272
+
1273
+ safeExecuteCustomHooks(`g_skinJsObj.precondition`, g_skinJsObj.precondition);
1274
+ };