danoniplus 50.4.0 → 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,2837 @@
1
+ /**
2
+ * Dancing☆Onigiri (CW Edition)
3
+ * 譜面及び楽曲データの読込・変換処理
4
+ * - ページ: loading, loadingIos
5
+ *
6
+ * Source by tickle
7
+ * Created : 2026/09/13
8
+ * Revised :
9
+ *
10
+ * https://github.com/cwtickle/danoniplus
11
+ */
12
+
13
+ /*-----------------------------------------------------------*/
14
+ /* Scene : LOADING [strawberry] */
15
+ /*-----------------------------------------------------------*/
16
+
17
+ const loadMusic = async () => {
18
+
19
+ clearWindow();
20
+ pauseBGM();
21
+ g_currentPage = `loading`;
22
+
23
+ const musicUrl = getMusicUrl(g_stateObj.scoreId);
24
+ const url = getFullMusicUrl(musicUrl);
25
+ g_headerObj.musicUrl = musicUrl;
26
+ g_musicEncodedFlg = listMatching(musicUrl, [`.js`, `.txt`], { suffix: `$` });
27
+
28
+ // Now Loadingを表示
29
+ const lblLoading = getLoadingLabel();
30
+ divRoot.appendChild(lblLoading);
31
+
32
+ // 音源準備Promise(ローカル/オンライン双方を吸収)
33
+ const audioReadyPromise = loadAndSetupAudio(url, lblLoading);
34
+
35
+ // 譜面データ読込・変換処理Promise(g_audioに依存しない部分)
36
+ const chartReadyPromise = loadChartFile().then(() => prepareScoreData());
37
+
38
+ // 両方の完了を待つ
39
+ let loadSucceeded = true;
40
+ try {
41
+ await Promise.all([audioReadyPromise, chartReadyPromise]);
42
+ } catch (e) {
43
+ console.warn(`Loading error: ${e}`);
44
+ loadSucceeded = false;
45
+ } finally {
46
+ deleteDiv(divRoot, `lblLoading`);
47
+ }
48
+
49
+ if (loadSucceeded) {
50
+ // 初回プレイ時に出力デバイスの起動待ちでずれるのを防ぐため、
51
+ // メイン画面へ移行する前にAudioContextを起動させておく
52
+ await warmUpAudioContext();
53
+ mainInit(); // 音源・譜面変換双方の完了後にまとめて呼ぶ
54
+ }
55
+ };
56
+
57
+ /**
58
+ * 音源の取得とセットアップ
59
+ * - エンコード形式(base64)か通常の音声ファイルかを判定し、それぞれの準備処理に振り分ける
60
+ * - iOSの場合はユーザー操作(ジェスチャー)を待ってから再生準備を行う(readyToStart経由)
61
+ * - キャッシュヒット時はダウンロード自体を行わないよう、取得処理はsetupWebAudioへ
62
+ * コールバックとして渡し、キャッシュミス時にのみ評価されるようにしている
63
+ * @param {string} _url 音源の取得元URL
64
+ * @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
65
+ * @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
66
+ */
67
+ const loadAndSetupAudio = async (_url, _lblLoading) => {
68
+
69
+ /**
70
+ * iOSの場合はユーザー操作(ジェスチャー)を待ってから_funcを実行する
71
+ * - AudioContextの制約上、iOSはジェスチャーを起点にしないと音声再生が許可されないため
72
+ * @param {() => Promise<void>} _func 実行する音源準備処理
73
+ * @returns {Promise<void>}
74
+ */
75
+ const readyToStart = _func => {
76
+ if (!g_isIos) {
77
+ return _func(); // 通常環境はそのまま実行するだけ
78
+ }
79
+ return new Promise((resolve, reject) => {
80
+ g_currentPage = `loadingIos`;
81
+ _lblLoading.textContent = `Click to Start!`;
82
+ divRoot.appendChild(makePlayButton(evt => {
83
+ getSharedAudioContext().resume();
84
+ g_currentPage = `loading`;
85
+ resetKeyControl();
86
+ divRoot.removeChild(evt.target);
87
+ _func().then(resolve).catch(reject);
88
+ }));
89
+ setShortcutEvent(g_currentPage);
90
+ });
91
+ };
92
+
93
+ // 音源準備処理そのものを、条件に応じて組み立てる
94
+ const setupAudioFunc = (() => {
95
+
96
+ // エンコードなし & ローカル実行: Audio要素で直接再生
97
+ if (!g_musicEncodedFlg && g_isFile) {
98
+ return () => {
99
+ g_audio = new Audio();
100
+ g_audio.src = _url;
101
+ return musicAfterLoaded();
102
+ };
103
+ }
104
+
105
+ // エンコードなし & オンライン: WebAudioAPI経由(URLからfetch)
106
+ if (!g_musicEncodedFlg) {
107
+ return () => setupWebAudio(async () => {
108
+ const blobUrl = await fetchMusicBlobUrl(_url, _lblLoading);
109
+ try {
110
+ const response = await fetch(blobUrl);
111
+ return await response.arrayBuffer();
112
+ } finally {
113
+ URL.revokeObjectURL(blobUrl);
114
+ }
115
+ }, _url);
116
+ }
117
+
118
+ // エンコードあり: スクリプト読込・musicInit実行を経てWebAudioAPI準備
119
+ return () => setupWebAudio(async () => {
120
+ const scriptSrc = g_isFile ? _url : await fetchMusicBlobUrl(_url, _lblLoading);
121
+ try {
122
+ await loadScript2(scriptSrc);
123
+ } finally {
124
+ if (!g_isFile) {
125
+ URL.revokeObjectURL(scriptSrc);
126
+ }
127
+ }
128
+ if (typeof musicInit !== C_TYP_FUNCTION) {
129
+ makeWarningWindow(g_msgInfoObj.E_0031, { backBtnUse: true });
130
+ throw new Error(`musicInit is not defined`);
131
+ }
132
+ musicInit();
133
+ return base64ToUint8Array(g_musicdata).buffer;
134
+ }, _url);
135
+ })();
136
+
137
+ return readyToStart(setupAudioFunc);
138
+ };
139
+
140
+ /**
141
+ * XHRによる音源ファイルのダウンロード(Promise化)
142
+ * - ダウンロードして Blob URL を返す
143
+ * @param {string} _url 音源ファイルのURL
144
+ * @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
145
+ * @returns {Promise<string>} Blob URL
146
+ */
147
+ const fetchMusicBlobUrl = (_url, _lblLoading) => new Promise((resolve, reject) => {
148
+ const request = new XMLHttpRequest();
149
+ request.open(`GET`, _url, true);
150
+ request.responseType = `blob`;
151
+
152
+ const STALL_TIMEOUT_MS = 30000; // 30秒間、進捗がなければ停滞とみなす
153
+ let stallTimer = null;
154
+
155
+ // 停滞タイマーをリセット
156
+ const resetStallTimer = () => {
157
+ g_timerHandler.clearTimeout(stallTimer);
158
+ stallTimer = g_timerHandler.setTimeout(() => {
159
+ request.abort();
160
+ makeWarningWindow(g_msgInfoObj.E_0033, { backBtnUse: true });
161
+ reject(new Error(`stalled`));
162
+ }, STALL_TIMEOUT_MS);
163
+ };
164
+
165
+ // 読み込み完了時
166
+ request.addEventListener(`load`, () => {
167
+ g_timerHandler.clearTimeout(stallTimer);
168
+ if (request.status >= 200 && request.status < 300) {
169
+ const blobUrl = URL.createObjectURL(request.response);
170
+ createEmptySprite(divRoot, `loader`, g_windowObj.loader);
171
+ _lblLoading.textContent = g_lblNameObj.pleaseWait;
172
+ resolve(blobUrl);
173
+ } else {
174
+ makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(_url))}<br>(${request.status} ${request.statusText})`, { backBtnUse: true });
175
+ reject(new Error(`HTTP ${request.status}`));
176
+ }
177
+ });
178
+
179
+ // 進捗時
180
+ request.addEventListener(`progress`, _event => {
181
+ resetStallTimer(); // 進捗があるたびにタイマーをリセット
182
+ const lblLoadingElem = document.getElementById(`lblLoading`);
183
+ if (lblLoadingElem === null) return; // 並列処理で先に削除されている場合の防御
184
+
185
+ if (_event.lengthComputable) {
186
+ const rate = _event.loaded / _event.total;
187
+ createEmptySprite(divRoot, `loader`, { y: g_sHeight - 10, h: 10, w: g_sWidth * rate, backgroundColor: `#eeeeee` });
188
+ lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${Math.floor(rate * 100)}%`;
189
+ } else {
190
+ lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${_event.loaded}Bytes`;
191
+ }
192
+ // ユーザカスタムイベント
193
+ safeExecuteCustomHooks(`g_customJsObj.progress`, g_customJsObj.progress, _event);
194
+ });
195
+
196
+ request.addEventListener(`error`, () => {
197
+ g_timerHandler.clearTimeout(stallTimer);
198
+ makeWarningWindow(g_msgInfoObj.E_0034, { backBtnUse: true });
199
+ reject(new Error(`network error`));
200
+ });
201
+
202
+ resetStallTimer(); // 初回(最初のprogressが来るまで)のタイマーも開始
203
+ request.send();
204
+ });
205
+
206
+ /**
207
+ * WebAudioAPIによる音源再生の準備(共通処理)
208
+ * - AudioPlayerを生成し、canplaythrough/errorの発火待ちを開始した上で、
209
+ * ArrayBufferを取得してデコードする(キャッシュヒット時はデコードをスキップ)
210
+ * @param {() => Promise<ArrayBuffer>} _fetchArrayBuffer 未キャッシュの場合にArrayBufferを取得する関数
211
+ * @param {string} [_cacheKey] AudioBufferキャッシュ照合用のキー(省略時はキャッシュを使わない)
212
+ * @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
213
+ */
214
+ const setupWebAudio = async (_fetchArrayBuffer, _cacheKey) => {
215
+ g_audio = new AudioPlayer();
216
+ const loadedPromise = musicAfterLoaded(); // canplaythrough/errorの発火をここで待つ
217
+
218
+ const cachedBuffer = _cacheKey ? getAudioBufferFromCache(_cacheKey) : undefined;
219
+ if (cachedBuffer !== undefined) {
220
+ g_audio.setBuffer(cachedBuffer);
221
+ } else {
222
+ const arrayBuffer = await _fetchArrayBuffer();
223
+ await g_audio.init(arrayBuffer);
224
+ if (_cacheKey) {
225
+ cacheAudioBuffer(_cacheKey, g_audio.getBuffer());
226
+ }
227
+ }
228
+ return loadedPromise;
229
+ };
230
+
231
+ const g_audioBufferCache = new Map();
232
+ const AUDIO_CACHE_MAX = 5;
233
+
234
+ /**
235
+ * デコード済みAudioBufferのキャッシュからの取得
236
+ * - Mapは挿入順を保持するのみでアクセス順を保持しないため、
237
+ * ヒット時にエントリを一度削除して再挿入し、最新として扱う(LRU方式)
238
+ * @param {string} _key キャッシュキー(楽曲の実URL)
239
+ * @returns {AudioBuffer|undefined} キャッシュされたAudioBuffer。存在しない場合はundefined
240
+ */
241
+ const getAudioBufferFromCache = (_key) => {
242
+ if (!g_audioBufferCache.has(_key)) {
243
+ return undefined;
244
+ }
245
+ const buffer = g_audioBufferCache.get(_key);
246
+ g_audioBufferCache.delete(_key);
247
+ g_audioBufferCache.set(_key, buffer); // 末尾(最新)に再挿入
248
+ return buffer;
249
+ };
250
+
251
+ /**
252
+ * デコード済みAudioBufferのキャッシュへの登録
253
+ * - 直近 AUDIO_CACHE_MAX 件を保持するLRU方式。上限を超えた場合は最も古いエントリから破棄する
254
+ * @param {string} _key キャッシュキー(楽曲の実URL)
255
+ * @param {AudioBuffer} _buffer デコード済みのAudioBuffer
256
+ */
257
+ const cacheAudioBuffer = (_key, _buffer) => {
258
+ g_audioBufferCache.set(_key, _buffer);
259
+ if (g_audioBufferCache.size > AUDIO_CACHE_MAX) {
260
+ g_audioBufferCache.delete(g_audioBufferCache.keys().next().value); // 古い順に破棄
261
+ }
262
+ };
263
+
264
+ /**
265
+ * base64文字列をUint8Arrayに変換
266
+ * - Uint8Array.from(atob(str), callback)によるコールバック呼び出し形式は
267
+ * 大容量データで変換オーバーヘッドが大きいため、forループによる直接代入で高速化している
268
+ * @param {string} _base64Str base64エンコードされた文字列
269
+ * @returns {Uint8Array} 変換後のバイト配列
270
+ */
271
+ const base64ToUint8Array = (_base64Str) => {
272
+ const binaryStr = atob(_base64Str);
273
+ const len = binaryStr.length;
274
+ const array = new Uint8Array(len);
275
+ for (let i = 0; i < len; i++) {
276
+ array[i] = binaryStr.charCodeAt(i);
277
+ }
278
+ return array;
279
+ };
280
+
281
+ /**
282
+ * 音源の再生準備完了を待つ
283
+ * - g_audio が Audio要素の場合は canplaythrough/error イベントの発火を待つ
284
+ * - g_audio が AudioPlayer の場合、readyState が既に4(デコード済み、キャッシュヒット時など)であれば即時解決する
285
+ * - canplaythrough/error のどちらが発火しても、もう一方のリスナーも確実に削除する(cleanup)
286
+ * @returns {Promise<void>} 再生準備が完了したら解決し、読込エラー時は例外を投げて拒否するPromise
287
+ */
288
+ const musicAfterLoaded = () => new Promise((resolve, reject) => {
289
+ g_audio.load();
290
+
291
+ if (g_audio.readyState === 4) {
292
+ resolve();
293
+ } else {
294
+ const onCanPlay = () => { cleanup(); resolve(); };
295
+ const onError = () => {
296
+ cleanup();
297
+ makeWarningWindow(g_msgInfoObj.E_0041.split(`{0}`).join(g_audio.src), { backBtnUse: true });
298
+ reject(new Error(`audio load error`));
299
+ };
300
+ const cleanup = () => {
301
+ g_audio.removeEventListener(`canplaythrough`, onCanPlay, false);
302
+ g_audio.removeEventListener(`error`, onError, false);
303
+ };
304
+ g_audio.addEventListener(`canplaythrough`, onCanPlay, false);
305
+ g_audio.addEventListener(`error`, onError, false);
306
+ }
307
+ });
308
+
309
+ /**
310
+ * 譜面データの変換処理
311
+ * - 音源データの状態に依存しない部分のみを担う(g_audio非参照)
312
+ * - loadMusic経由(並行フロー)、executeRetry経由(曲中リトライ)の両方から呼ばれる
313
+ */
314
+ const prepareScoreData = () => {
315
+
316
+ const tkObj = getKeyInfo();
317
+ const [keyCtrlPtn, keyNum] = [tkObj.keyCtrlPtn, tkObj.keyNum];
318
+ g_headerObj.blankFrameDef = setVal(g_headerObj.blankFrameDefs[g_stateObj.scoreId], g_headerObj.blankFrameDefs[0]);
319
+ g_headerObj.blankFrame = g_headerObj.blankFrameDef;
320
+
321
+ // ユーザカスタムイベント
322
+ safeExecuteCustomHooks(`g_customJsObj.preloading`, g_customJsObj.preloading);
323
+ safeExecuteCustomHooks(`g_skinJsObj.preloading`, g_skinJsObj.preloading);
324
+
325
+ let dummyIdHeader = ``;
326
+ if (g_stateObj.dummyId !== ``) {
327
+ if (g_stateObj.dummyId === 0 || g_stateObj.dummyId === 1) {
328
+ dummyIdHeader = ``;
329
+ } else {
330
+ dummyIdHeader = g_stateObj.dummyId;
331
+ }
332
+ }
333
+ g_scoreObj = scoreConvert(g_rootObj, g_stateObj.scoreId, 0, dummyIdHeader);
334
+
335
+ // Motionオプション適用時の矢印別の速度を取得(配列形式)
336
+ g_workObj.motionOnFrames = setMotionOnFrame();
337
+ g_workObj.motionReverseFlg = g_workObj.motionOnFrames.filter(val => g_stateObj.speed + val < 0).length > 0;
338
+
339
+ // 矢印描画時間の引き伸ばし設定(個別加速がある場合かつ逆走を伴うモーションは初期倍速によりさらに引き伸ばしを行う)
340
+ // ただし、速度による引き伸ばし(boostFactor)の上限は描画の関係で2倍までとする
341
+ let maxBoost = 1;
342
+ for (let j = 0; j < g_scoreObj.boostData.length; j += 2) {
343
+ maxBoost = Math.max(maxBoost, g_scoreObj.boostData[j + 1]);
344
+ }
345
+ const boostFactor = 1 + (
346
+ (g_settings.motionDistRates[g_settings.motionNum] === 1 || !g_workObj.motionReverseFlg)
347
+ ? 0
348
+ : (
349
+ g_stateObj.speed * g_headerObj.baseSpeed - g_settings.motionBoostFactorMinSpd > 0 && maxBoost > 1
350
+ ? Math.min(maxBoost / 4, 1) : 0
351
+ )
352
+ );
353
+ g_scoreObj.distY = Math.max(
354
+ g_headerObj.stretchYRate[g_stateObj.scoreId], g_settings.motionDistRates[g_settings.motionNum] * boostFactor
355
+ ) * g_posObj.distY;
356
+
357
+ // 最終フレーム数の取得
358
+ let lastFrame = getLastFrame(g_scoreObj) + g_headerObj.blankFrame;
359
+
360
+ // 最初の矢印データがあるフレーム数を取得
361
+ let firstArrowFrame = getFirstArrowFrame(g_scoreObj);
362
+
363
+ // 開始フレーム数の取得(フェードイン加味)
364
+ g_scoreObj.frameNum = getStartFrame(lastFrame, g_stateObj.fadein);
365
+
366
+ // フレームごとの速度を取得(配列形式)
367
+ let speedOnFrame = setSpeedOnFrame(g_scoreObj.speedData, lastFrame);
368
+
369
+ // 最初のフレームで出現する矢印が、ステップゾーンに到達するまでのフレーム数を取得
370
+ const firstFrame = (g_scoreObj.frameNum === 0 ? 0 : g_scoreObj.frameNum + g_headerObj.blankFrame);
371
+ let arrivalFrame = getFirstArrivalFrame(firstFrame, speedOnFrame);
372
+
373
+ // キーパターン(デフォルト)に対応する矢印番号を格納
374
+ convertReplaceNums();
375
+
376
+ const setData = (_data, _minLength = 1) => hasArrayList(_data, _minLength) ? _data.concat() : [];
377
+
378
+ // フレーム・曲開始位置調整
379
+ let preblankFrame = 0;
380
+ if (g_scoreObj.frameNum === 0) {
381
+ if (firstArrowFrame - g_limitObj.adjustment < arrivalFrame) {
382
+ preblankFrame = arrivalFrame - firstArrowFrame + g_limitObj.adjustment;
383
+
384
+ // 譜面データの再読み込み
385
+ const noteExistObj = {
386
+ arrow: true,
387
+ frz: true,
388
+ dummyArrow: g_stateObj.shuffle === C_FLG_OFF,
389
+ dummyFrz: g_stateObj.shuffle === C_FLG_OFF,
390
+ };
391
+ const tmpObj = scoreConvert(g_rootObj, g_stateObj.scoreId, preblankFrame, dummyIdHeader);
392
+ for (let j = 0; j < keyNum; j++) {
393
+ Object.keys(noteExistObj).filter(name => tmpObj[`${name}Data`][j] !== undefined && noteExistObj[name])
394
+ .forEach(name => g_scoreObj[`${name}Data`][j] = structuredClone(tmpObj[`${name}Data`][j]));
395
+ }
396
+
397
+ Object.keys(g_dataMinObj).forEach(dataType =>
398
+ g_scoreObj[`${dataType}Data`] = setData(tmpObj[`${dataType}Data`], g_dataMinObj[dataType]));
399
+
400
+ lastFrame += preblankFrame;
401
+ firstArrowFrame += preblankFrame;
402
+ speedOnFrame = setSpeedOnFrame(g_scoreObj.speedData, lastFrame);
403
+ arrivalFrame = getFirstArrivalFrame(firstFrame, speedOnFrame);
404
+ g_headerObj.blankFrame += preblankFrame;
405
+ }
406
+ }
407
+ g_scoreObj.baseFrame = g_scoreObj.frameNum - g_stateObj.intAdjustment;
408
+
409
+ // シャッフルグループ未定義の場合
410
+ if (g_keyObj[`shuffle${keyCtrlPtn}`] === undefined) {
411
+ g_keyObj[`shuffle${keyCtrlPtn}`] = fillArray(keyNum);
412
+ }
413
+
414
+ // シャッフルグループを扱いやすくする
415
+ // [0, 0, 0, 1, 0, 0, 0] -> [[0, 1, 2, 4, 5, 6], [3]]
416
+ g_workObj.shuffleGroupMap = {};
417
+ g_keyObj[`shuffle${keyCtrlPtn}`].forEach((_val, _i) =>
418
+ g_workObj.shuffleGroupMap[_val]?.push(_i) || (g_workObj.shuffleGroupMap[_val] = [_i]));
419
+
420
+ // Mirror,Random,S-Randomの適用
421
+ g_shuffleFunc.get(g_stateObj.shuffle)(keyNum, Object.values(g_workObj.shuffleGroupMap));
422
+
423
+ // アシスト用の配列があれば、ダミーデータで上書き
424
+ if (typeof g_keyObj[`assistPos${keyCtrlPtn}`] === C_TYP_OBJECT &&
425
+ !g_autoPlaysBase.includes(g_stateObj.autoPlay)) {
426
+ const assistArray = g_keyObj[`assistPos${keyCtrlPtn}`][g_stateObj.autoPlay];
427
+ for (let j = 0; j < keyNum; j++) {
428
+ if (assistArray[j] === 1) {
429
+ g_scoreObj.dummyArrowData[j] = g_scoreObj.arrowData[j].concat();
430
+ g_scoreObj.arrowData[j] = [];
431
+ g_scoreObj.dummyFrzData[j] = g_scoreObj.frzData[j].concat();
432
+ g_scoreObj.frzData[j] = [];
433
+ } else {
434
+ g_scoreObj.dummyArrowData[j] = [];
435
+ g_scoreObj.dummyFrzData[j] = [];
436
+ }
437
+ }
438
+ }
439
+
440
+ // 矢印・フリーズアロー数をカウント
441
+ g_allArrow = 0;
442
+ g_allFrz = 0;
443
+ g_scoreObj.arrowData.forEach(data => g_allArrow += data.length);
444
+ g_scoreObj.frzData.forEach(data => g_allFrz += Math.floor(data.length / 2) * 2);
445
+
446
+ // ライフ回復・ダメージ量の計算
447
+ // フリーズ始点でも通常判定させる場合は総矢印数を水増しする
448
+ if (g_headerObj.frzStartjdgUse) {
449
+ g_allArrow += g_allFrz / 2;
450
+ }
451
+ g_fullArrows = g_allArrow + g_allFrz / 2;
452
+
453
+ calcLifeVals(g_fullArrows);
454
+
455
+ // 矢印・フリーズアロー・速度/色変化格納処理
456
+ pushArrows(g_scoreObj, speedOnFrame, arrivalFrame);
457
+
458
+ // メインに入る前の最終初期化処理
459
+ getArrowSettings();
460
+
461
+ // ユーザカスタムイベント
462
+ safeExecuteCustomHooks(`g_customJsObj.loading`, g_customJsObj.loading);
463
+ };
464
+
465
+ /**
466
+ * 譜面番号の取得
467
+ * @param {number} _scoreId
468
+ * @param {boolean} _scoreLockFlg
469
+ * @param {boolean} _useOne 1譜面目指定有無フラグ (初期色に関する箇所のみ指定)
470
+ * @returns {number|string}
471
+ */
472
+ const setScoreIdHeader = (_scoreId = 0, _scoreLockFlg = false, _useOne = false) => {
473
+ if (!_scoreLockFlg && _scoreId > 0) {
474
+ return Number(_scoreId) + 1;
475
+ } else if (_scoreLockFlg && g_headerObj.scoreNos?.[_scoreId] > 1) {
476
+ return g_headerObj.scoreNos[_scoreId];
477
+ }
478
+ return _useOne ? 1 : ``;
479
+ };
480
+
481
+ /**
482
+ * 譜面ファイル番号の取得
483
+ * @param {number} [_scoreId=0]
484
+ * @param {boolean} [_scoreLockFlg=false]
485
+ * @returns {number|string}
486
+ */
487
+ const setDosIdHeader = (_scoreId = 0, _scoreLockFlg = false) => {
488
+ if (_scoreLockFlg && g_headerObj.dosNos?.[_scoreId] > 0) {
489
+ return g_headerObj.dosNos?.[_scoreId] > 1 ? g_headerObj.dosNos[_scoreId] : ``;
490
+ } else if (_scoreId > 0) {
491
+ return Number(_scoreId) + 1;
492
+ }
493
+ return ``;
494
+ };
495
+
496
+ /**
497
+ * Mirror,Randomの適用
498
+ * @param {number} _keyNum
499
+ * @param {number[][]} _shuffleGroup
500
+ * @param {number[][]} _style
501
+ */
502
+ const applyShuffle = (_keyNum, _shuffleGroup, _style) => {
503
+ // 並べ替え用の配列を作成
504
+ // index[i]番目のキーの譜面がi番目のキーに流れるようになります
505
+ const index = [...Array(_keyNum).keys()];
506
+ for (let i = 0; i < _shuffleGroup.length; i++) {
507
+ for (let j = 0; j < _shuffleGroup[i].length; j++) {
508
+ index[_shuffleGroup[i][j]] = _style[i][j];
509
+ }
510
+ }
511
+
512
+ // indexに従って並べ替え
513
+ g_typeLists.arrow.forEach(type => {
514
+ const tmpData = structuredClone(g_scoreObj[`${type}Data`]);
515
+ for (let i = 0; i < _keyNum; i++) {
516
+ g_scoreObj[`${type}Data`][i] = tmpData[index[i]] || [];
517
+ }
518
+ });
519
+ };
520
+
521
+ /**
522
+ * X-Mirror作成用の入れ替え関数
523
+ * グループが4の倍数のとき、4n+1, 4n+2のみ入れ替える
524
+ * @param {number[][]} _style
525
+ * @param {number[]} _group
526
+ * @param {number} _i
527
+ * @param {number} _divideNum
528
+ * @returns {number[][]}
529
+ */
530
+ const swapGroupNums = (_style, _group, _i, _divideNum) => {
531
+ if (_group.length % _divideNum === 0) {
532
+ for (let k = 0; k < _group.length / _divideNum; k++) {
533
+ const swap1 = Math.floor(_divideNum * (k + 1 / 2) - 1);
534
+ const swap2 = Math.ceil(_divideNum * (k + 1 / 2));
535
+ [_style[_i][swap1], _style[_i][swap2]] = [_style[_i][swap2], _style[_i][swap1]];
536
+ }
537
+ }
538
+ return _style;
539
+ };
540
+
541
+ /**
542
+ * Mirrorの適用
543
+ * @param {number} _keyNum
544
+ * @param {number[][]} _shuffleGroup
545
+ * @param {boolean} [_swapFlg=false]
546
+ */
547
+ const applyMirror = (_keyNum, _shuffleGroup, _swapFlg = false) => {
548
+
549
+ // シャッフルグループごとにミラー
550
+ const style = structuredClone(_shuffleGroup).map(_group => _group.reverse());
551
+ const mirStyle = structuredClone(style);
552
+
553
+ if (_swapFlg) {
554
+ style.forEach((group, i) => {
555
+ g_settings.swapPattern.forEach(val => {
556
+ swapGroupNums(style, group, i, val);
557
+ });
558
+ });
559
+ let swapUseFlg = false;
560
+ style.forEach((_group, j) => {
561
+ _group.forEach((val, k) => {
562
+ if (style[j][k] !== mirStyle[j][k]) {
563
+ swapUseFlg = true;
564
+ return;
565
+ }
566
+ });
567
+ });
568
+ if (!swapUseFlg) {
569
+ g_stateObj.shuffle = `Mirror`;
570
+ }
571
+ }
572
+ applyShuffle(_keyNum, _shuffleGroup, style);
573
+ };
574
+
575
+ /**
576
+ * Turningの適用
577
+ * @param {number} _keyNum
578
+ * @param {number[][]} _shuffleGroup
579
+ */
580
+ const applyTurning = (_keyNum, _shuffleGroup) => {
581
+ const mirrorOrNot = _array => Math.random() >= 0.5 ? _array.reverse() : _array;
582
+ const style = structuredClone(_shuffleGroup).map(_group => {
583
+ const startNum = Math.floor(Math.random() * (_group.length - 1)) + 1;
584
+ return mirrorOrNot(makeDedupliArray(_group.slice(startNum), _group.slice(0, startNum)));
585
+ });
586
+ applyShuffle(_keyNum, _shuffleGroup, style);
587
+ };
588
+
589
+ /**
590
+ * Randomの適用
591
+ * @param {number} _keyNum
592
+ * @param {number[][]} _shuffleGroup
593
+ */
594
+ const applyRandom = (_keyNum, _shuffleGroup) => {
595
+ // シャッフルグループごとにシャッフル(Fisher-Yates)
596
+ const style = structuredClone(_shuffleGroup).map(_group => {
597
+ for (let i = _group.length - 1; i > 0; i--) {
598
+ const random = Math.floor(Math.random() * (i + 1));
599
+ [_group[i], _group[random]] = [_group[random], _group[i]];
600
+ }
601
+ return _group;
602
+ });
603
+ applyShuffle(_keyNum, _shuffleGroup, style);
604
+ };
605
+
606
+ /**
607
+ * S-Randomの適用
608
+ * @param {number} _keyNum
609
+ * @param {number[][]} _shuffleGroup
610
+ * @param {string} _arrowHeader
611
+ * @param {string} _frzHeader
612
+ */
613
+ const applySRandom = (_keyNum, _shuffleGroup, _arrowHeader, _frzHeader) => {
614
+
615
+ const tmpArrowData = [...Array(_keyNum)].map(() => []);
616
+ const tmpFrzData = [...Array(_keyNum)].map(() => []);
617
+ const scatterFrame = 10;
618
+
619
+ // シャッフルグループごとに処理
620
+ _shuffleGroup.forEach(_group => {
621
+ // 全フリーズを開始フレーム順に並べる
622
+ const allFreezeArrows = [];
623
+ _group.forEach(_key => {
624
+ const frzData = g_scoreObj[`${_frzHeader}Data`][_key] || [];
625
+ for (let i = 0; i < frzData.length; i += 2) {
626
+ allFreezeArrows.push({ begin: frzData[i], end: frzData[i + 1] });
627
+ }
628
+ });
629
+ allFreezeArrows.sort((_a, _b) => _a.begin - _b.begin);
630
+
631
+ // 重ならないようにフリーズを配置
632
+ allFreezeArrows.forEach(_freeze => {
633
+ // 置ける場所を検索
634
+ const freeSpacesFlat = _group.filter(
635
+ _key => tmpFrzData[_key].find(_other => _freeze.begin - scatterFrame <= _other.end + scatterFrame) === undefined
636
+ );
637
+ const freeSpaces = _group.filter(
638
+ _key => tmpFrzData[_key].find(_other => _freeze.begin <= _other.end) === undefined
639
+ );
640
+ let currentFreeSpaces = freeSpaces;
641
+ if (g_stateObj.shuffle.startsWith(`Scatter`)) {
642
+ currentFreeSpaces = freeSpacesFlat.length > 0 ? freeSpacesFlat : freeSpaces;
643
+ }
644
+ // ランダムに配置
645
+ const random = Math.floor(Math.random() * currentFreeSpaces.length);
646
+ tmpFrzData[currentFreeSpaces[random]].push(_freeze);
647
+ });
648
+
649
+ // 通常矢印の配置
650
+ const allArrows = _group.map(_key => g_scoreObj[`${_arrowHeader}Data`][_key]).flat();
651
+ allArrows.sort((_a, _b) => _a - _b);
652
+ let prev2Num = 0, prevNum = 0, sameFlg = true;
653
+ allArrows.forEach(_arrow => {
654
+
655
+ // 直前の矢印のフレーム数を取得
656
+ sameFlg = true;
657
+ if (prev2Num !== _arrow && prevNum !== _arrow) {
658
+ prev2Num = prevNum;
659
+ prevNum = _arrow;
660
+ sameFlg = false;
661
+ }
662
+
663
+ const getFreeSpaces = ({ scatterFrame = 0, frzFlg = false, prevFlg = false } = {}) =>
664
+ _group.filter(_key =>
665
+ // 通常矢印と重ならない
666
+ tmpArrowData[_key].find(_other => _arrow >= _other - scatterFrame && _arrow <= _other + scatterFrame) === undefined
667
+ // フリーズと重ならない
668
+ && (!frzFlg || tmpFrzData[_key].find(_freeze => _arrow >= _freeze.begin - scatterFrame && _arrow <= _freeze.end + scatterFrame) === undefined)
669
+ // 直前の矢印と重ならない
670
+ && (!prevFlg || tmpArrowData[_key].find(_other => prev2Num === _other) === undefined)
671
+ );
672
+
673
+ // 置ける場所を検索
674
+ const freeSpacesFlat = getFreeSpaces({ scatterFrame, frzFlg: true, prevFlg: true });
675
+ const freeSpaces = getFreeSpaces({ frzFlg: true });
676
+ const freeSpacesAlt = getFreeSpaces();
677
+
678
+ // ランダムに配置
679
+ let currentFreeSpaces = freeSpaces.length > 0 ? freeSpaces : freeSpacesAlt;
680
+ if (g_stateObj.shuffle.startsWith(`Scatter`)) {
681
+ currentFreeSpaces = freeSpacesFlat.length > 0 && !sameFlg ? freeSpacesFlat : currentFreeSpaces;
682
+ }
683
+ const random = Math.floor(Math.random() * currentFreeSpaces.length);
684
+ tmpArrowData[currentFreeSpaces[random]].push(_arrow);
685
+ })
686
+ });
687
+
688
+ g_scoreObj[`${_arrowHeader}Data`] = tmpArrowData;
689
+ g_scoreObj[`${_frzHeader}Data`] = tmpFrzData.map(_freezes =>
690
+ _freezes.map(_freeze => [_freeze.begin, _freeze.end]).flat()
691
+ );
692
+ };
693
+
694
+ /**
695
+ * 譜面データの分解
696
+ * @param {object} _dosObj
697
+ * @param {number} _scoreId 譜面番号
698
+ * @param {number} _preblankFrame 補完フレーム数
699
+ * @param {string} [_dummyNo] ダミー用譜面番号添え字
700
+ * @param {string} [_keyCtrlPtn] 選択キー及びパターン
701
+ * @param {boolean} [_scoreAnalyzeFlg=false] 譜面詳細データのために必要分で読込を中断
702
+ * @returns {object} 矢印・フリーズアロー・速度/色変化データの格納オブジェクト
703
+ */
704
+ const scoreConvert = (_dosObj, _scoreId, _preblankFrame, _dummyNo = ``,
705
+ _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`, _scoreAnalyzeFlg = false) => {
706
+
707
+ // 矢印群の格納先
708
+ const obj = {};
709
+
710
+ const scoreIdHeader = setScoreIdHeader(_scoreId, g_stateObj.scoreLockFlg, false);
711
+ const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
712
+ obj.arrowData = [];
713
+ obj.frzData = [];
714
+ obj.dummyArrowData = [];
715
+ obj.dummyFrzData = [];
716
+
717
+ // realAdjustment: 全体, intAdjustment: 整数値のみ(切り捨て、ファイル時のみ四捨五入), decimalAdjustment: 小数値のみ
718
+ const headerAdjustment = parseFloat(g_headerObj.adjustment[g_stateObj.scoreId] || g_headerObj.adjustment[0]);
719
+ g_stateObj.realAdjustment = (parseFloat(g_stateObj.adjustment) + headerAdjustment) / g_headerObj.playbackRate + _preblankFrame;
720
+ g_stateObj.intAdjustment = isLocalMusicFile(g_stateObj.scoreId) ?
721
+ Math.round(g_stateObj.realAdjustment) : Math.floor(g_stateObj.realAdjustment);
722
+ g_stateObj.decimalAdjustment = g_stateObj.realAdjustment - g_stateObj.intAdjustment;
723
+
724
+ const blankFrame = g_headerObj.blankFrame;
725
+ const calcFrame = _frame => Math.round((parseFloat(_frame) - blankFrame) / g_headerObj.playbackRate + blankFrame + g_stateObj.intAdjustment);
726
+
727
+ /**
728
+ * 矢印データの格納
729
+ * @param {string} _data
730
+ * @returns {number[]}
731
+ */
732
+ const storeArrowData = _data => hasVal(_data) ?
733
+ splitLF(_data)?.join(``).split(`,`).filter(data => !isNaN(parseFloat(data))).map(data => calcFrame(data)).sort((_a, _b) => _a - _b) : [];
734
+
735
+ for (let j = 0; j < keyNum; j++) {
736
+
737
+ // 矢印データの分解
738
+ const arrowName = g_keyObj[`chara${_keyCtrlPtn}`][j];
739
+ obj.arrowData[j] = storeArrowData(_dosObj[`${arrowName}${scoreIdHeader}_data`]);
740
+
741
+ if (g_stateObj.dummyId !== ``) {
742
+ obj.dummyArrowData[j] = storeArrowData(_dosObj[`${arrowName}${_dummyNo}_data`]);
743
+ }
744
+
745
+ // 矢印名からフリーズアロー名への変換
746
+ let frzName = replaceStr(g_keyObj[`chara${_keyCtrlPtn}`][j], g_escapeStr.frzName);
747
+ if (frzName.indexOf(`frz`) === -1 && frzName.indexOf(`foni`) === -1) {
748
+ frzName = frzName.replaceAll(frzName, `frz${toCapitalize(frzName)}`);
749
+ }
750
+
751
+ // フリーズアローデータの分解 (2つで1セット)
752
+ obj.frzData[j] = storeArrowData(_dosObj[`${frzName}${scoreIdHeader}_data`]);
753
+
754
+ if (g_stateObj.dummyId !== ``) {
755
+ obj.dummyFrzData[j] = storeArrowData(_dosObj[`${frzName}${_dummyNo}_data`]);
756
+ }
757
+ }
758
+
759
+ /**
760
+ * 速度変化データの分解・格納(フレーム数, 矢印番号)
761
+ * @param {string} _header
762
+ * @param {number} _scoreNo
763
+ * @param {string} _footer
764
+ * @returns {number[]}
765
+ */
766
+ const setSpeedData = (_header, _scoreNo, _footer = `_data`) => {
767
+ const dosSpeedData = getRefData(_header, `${_scoreNo}${_footer}`);
768
+ const speedData = [];
769
+
770
+ if (hasVal(dosSpeedData) && g_stateObj.d_velocity !== C_FLG_OFF) {
771
+ const tmpArrayData = splitLF(dosSpeedData);
772
+
773
+ tmpArrayData.filter(data => hasVal(data)).forEach(tmpData => {
774
+ const tmpSpeedData = tmpData.split(`,`);
775
+ for (let k = 0; k < tmpSpeedData.length; k += 2) {
776
+ if (!hasVal(tmpSpeedData[k])) {
777
+ continue;
778
+ } else if (tmpSpeedData[k + 1] === `-`) {
779
+ break;
780
+ }
781
+ const frame = calcFrame(setVal(tmpSpeedData[k], ``, C_TYP_CALC));
782
+ const speed = setVal(tmpSpeedData[k + 1], 1, C_TYP_CALC);
783
+
784
+ speedData.push([frame, speed]);
785
+ }
786
+ });
787
+ return speedData.sort((_a, _b) => _a[0] - _b[0]).flat();
788
+ }
789
+ return [];
790
+ };
791
+
792
+ /**
793
+ * 個別・全体色変化データをマージして整列し、単純配列として返却
794
+ * @param {string} _header
795
+ * @returns {any[]}
796
+ */
797
+ const mergeColorData = (_header = ``) => {
798
+ if (obj[`color${_header}Data`] === undefined) return [];
799
+ const tmpArr = obj[`color${_header}Data`].concat(obj[`acolor${_header}Data`]);
800
+ delete obj[`acolor${_header}Data`];
801
+ return tmpArr.sort((_a, _b) => _a[0] - _b[0]).flat();
802
+ };
803
+
804
+ /**
805
+ * 色変化データの分解・格納(フレーム数, 矢印番号)
806
+ * 後で個別・全体色変化をマージするため、二次元配列として返却
807
+ * @param {string} _header
808
+ * @param {number} _scoreNo
809
+ * @returns {any[][]}
810
+ */
811
+ const setColorData = (_header, _scoreNo) => {
812
+ const dosColorData = getRefData(_header, `${_scoreNo}_data`);
813
+ const colorData = [];
814
+ const allFlg = (_header.charAt(0) === `a`);
815
+
816
+ if (hasVal(dosColorData) && g_stateObj.d_color === C_FLG_ON) {
817
+ const tmpArrayData = splitLF(dosColorData);
818
+
819
+ tmpArrayData.filter(data => hasVal(data)).forEach(tmpData => {
820
+ const tmpColorData = tmpData.split(`,`);
821
+ for (let k = 0; k < tmpColorData.length; k += 3) {
822
+ if (!hasVal(tmpColorData[k])) {
823
+ continue;
824
+ } else if (tmpColorData[k + 1] === `-`) {
825
+ break;
826
+ }
827
+ const frame = calcFrame(setVal(tmpColorData[k], ``, C_TYP_CALC));
828
+ const colorNum = setVal(tmpColorData[k + 1], 0, C_TYP_CALC);
829
+ const colorCd = tmpColorData[k + 2];
830
+
831
+ // フレーム数、色番号、カラーコード、全体色変化フラグをセットとして配列化
832
+ colorData.push([frame, colorNum, colorCd, allFlg]);
833
+ }
834
+ });
835
+ return colorData.sort((_a, _b) => _a[0] - _b[0]);
836
+ }
837
+ return [];
838
+ };
839
+
840
+ /**
841
+ * 色変化データの分解・格納(新形式)
842
+ * - 個別・全体色変化を一体で管理するため通常の配列で返却
843
+ * @param {string} _header
844
+ * @param {number} _scoreNo
845
+ * @returns {any[][]}
846
+ */
847
+ const setColor2Data = (_header, _scoreNo) => {
848
+ const dosColorData = getRefData(_header, `${_scoreNo}_data`);
849
+ const colorData = {
850
+ Arrow: [], ArrowShadow: [], Normal: [], NormalBar: [], NormalShadow: [],
851
+ Hit: [], HitBar: [], HitShadow: [],
852
+ };
853
+
854
+ if (hasVal(dosColorData) && g_stateObj.d_color === C_FLG_ON) {
855
+
856
+ splitLF(dosColorData).filter(data => hasVal(data)).forEach(tmpData => {
857
+ const tmpColorData = tmpData.split(`,`).map(val => trimStr(val));
858
+ if (!hasVal(tmpColorData[0]) || tmpColorData[1] === `-`) {
859
+ return;
860
+ }
861
+ const frame = calcFrame(setVal(tmpColorData[0], ``, C_TYP_CALC));
862
+ const colorCd = tmpColorData[2];
863
+
864
+ // 色変化対象の取得
865
+ const pos = tmpColorData[1]?.indexOf(`:`);
866
+ const patternStr = pos > 0 ? [trimStr(tmpColorData[1].substring(0, pos)), trimStr(tmpColorData[1].substring(pos + 1))]
867
+ : [tmpColorData[1]];
868
+ const patterns = replaceStr(trimStr(patternStr[1] || `Arrow`), g_escapeStr.colorPatternName).split(`/`);
869
+
870
+ // 矢印番号の組み立て
871
+ const colorVals = [];
872
+ replaceStr(patternStr[0], g_escapeStr.targetPatternName)?.split(`/`)?.forEach(val => {
873
+ if (val.indexOf(`...`) > 0) {
874
+ // 範囲指定表記の補完 例. 0...3 -> 0/1/2/3
875
+ const [valMin, valMax] = [val.split(`...`)[0], val.split(`...`)[1]].map(val => setIntVal(val));
876
+ for (let k = valMin; k <= valMax; k++) {
877
+ colorVals.push(String(k));
878
+ }
879
+ } else {
880
+ colorVals.push(val);
881
+ }
882
+ });
883
+
884
+ // フレーム数、色番号、カラーコード、全体色変化フラグ、変更対象をセットとして配列化し、色変化対象ごとのプロパティへ追加
885
+ patterns.forEach(pattern => {
886
+ try {
887
+ colorVals.forEach(val => colorData[pattern].push([frame, val, colorCd, hasVal(tmpColorData[3]), pattern]));
888
+ } catch (error) {
889
+ makeWarningWindow(g_msgInfoObj.E_0201.split(`{0}`).join(pattern));
890
+ }
891
+ });
892
+ });
893
+ // 色変化対象ごとにフレーム数をキーにソートしてフラット化
894
+ Object.keys(colorData).forEach(pattern =>
895
+ colorData[pattern] = colorData[pattern].sort((_a, _b) => _a[0] - _b[0]).flat());
896
+ }
897
+ return colorData;
898
+ };
899
+
900
+ /**
901
+ * 矢印モーションデータの分解・格納(フレーム数, 矢印番号)
902
+ * @param {string} _header
903
+ * @param {number} _scoreNo
904
+ * @returns {any[]}
905
+ */
906
+ const setCssMotionData = (_header, _scoreNo) => {
907
+ const dosCssMotionData = getRefData(`${_header}Motion`, `${_scoreNo}_data`) || _dosObj[`${_header}Motion_data`];
908
+ const cssMotionData = [];
909
+
910
+ if (hasVal(dosCssMotionData) && g_stateObj.d_arroweffect === C_FLG_ON) {
911
+ splitLF(dosCssMotionData).filter(data => hasVal(data)).forEach(tmpData => {
912
+ const tmpcssMotionData = tmpData.split(`,`);
913
+ if (isNaN(parseInt(tmpcssMotionData[0]))) {
914
+ return;
915
+ }
916
+ const frame = calcFrame(tmpcssMotionData[0]);
917
+ const arrowNum = parseFloat(tmpcssMotionData[1]);
918
+ const styleUp = (tmpcssMotionData[2] === `none` ? `` : tmpcssMotionData[2]);
919
+ const styleDown = (tmpcssMotionData[3] === `none` ? `` : setVal(tmpcssMotionData[3], styleUp));
920
+ const movLock = setBoolVal(tmpcssMotionData[4] === `movLock`, false);
921
+ const initManual = setBoolVal(tmpcssMotionData[5] === `initManual`, false);
922
+
923
+ cssMotionData.push([frame, arrowNum, styleUp, styleDown, movLock, initManual]);
924
+ });
925
+
926
+ // 個別のモーションデータが存在する場合、Effect設定を自動リセット
927
+ if (cssMotionData.length > 0) {
928
+ g_stateObj.effect = C_FLG_OFF;
929
+ g_settings.effectNum = 0;
930
+ }
931
+ return cssMotionData.sort((_a, _b) => _a[0] - _b[0]).flat();
932
+ }
933
+ return [];
934
+ };
935
+
936
+ /**
937
+ * スクロール変化データの分解
938
+ * @param {number} _scoreNo
939
+ * @returns {number[]}
940
+ */
941
+ const setScrollchData = (_scoreNo) => {
942
+ const dosScrollchData = getRefData(`scrollch`, `${_scoreNo}_data`) || _dosObj.scrollch_data;
943
+ const scrollchData = [];
944
+ let maxLayerGroup = 0;
945
+
946
+ if (hasVal(dosScrollchData)) {
947
+ splitLF(dosScrollchData).filter(data => hasVal(data)).forEach(tmpData => {
948
+ const tmpScrollchData = tmpData.split(`,`);
949
+ if (isNaN(parseInt(tmpScrollchData[0]))) {
950
+ return;
951
+ }
952
+ const frame = calcFrame(tmpScrollchData[0]);
953
+ const arrowNum = parseFloat(tmpScrollchData[1]);
954
+ const scrollDir = parseFloat(tmpScrollchData[2] ?? `1`);
955
+ const layerGroup = parseFloat(tmpScrollchData[3] ?? `-1`);
956
+ let layerTrans = tmpScrollchData[4] ?? ``;
957
+ maxLayerGroup = Math.max(maxLayerGroup, layerGroup);
958
+ if (g_stateObj.reverse === C_FLG_ON) {
959
+ layerTrans = invertSpecificTransforms(layerTrans, [`translateX`]);
960
+ }
961
+ if (g_stateObj.swapping === `Mirror+`) {
962
+ layerTrans = invertSpecificTransforms(layerTrans, [`translateY`]);
963
+ }
964
+
965
+ scrollchData.push([frame, arrowNum, frame, scrollDir, layerGroup, layerTrans]);
966
+ });
967
+ g_stateObj.layerNumDf = Math.max((maxLayerGroup + 1) * 2, 2);
968
+ return scrollchData.sort((_a, _b) => _a[0] - _b[0]).flat();
969
+ }
970
+ return [];
971
+ };
972
+
973
+ /**
974
+ * 譜面データに別の関連名が含まれていた場合、関連名の変数を返す
975
+ * 例) |backA2_data=back_data| -> back_dataで定義された値を使用
976
+ * @param {string} _header
977
+ * @param {string} _dataName
978
+ * @returns {string|undefined}
979
+ */
980
+ const getRefData = (_header, _dataName) => {
981
+ const data = _dosObj[`${_header}${_dataName}`];
982
+ let dataStr = data;
983
+ splitLF(data)?.filter(val => val?.startsWith(_header) && _dosObj[val] !== undefined)
984
+ .forEach(val => dataStr = dataStr.replace(val, _dosObj[val]));
985
+ return dataStr;
986
+ };
987
+
988
+ /**
989
+ * 譜面データの優先順配列パターンの取得
990
+ * @param {string} _type
991
+ * @param {number} _scoreNo
992
+ * @returns {string[]}
993
+ */
994
+ const getPriorityVal = (_type, _scoreNo) => [
995
+ `${_type}${g_localeObj.val}${_scoreNo}_data`,
996
+ `${_type}${g_localeObj.val}_data`,
997
+ `${_type}${_scoreNo}_data`,
998
+ `${_type}_data`
999
+ ];
1000
+
1001
+ /**
1002
+ * 歌詞表示、背景・マスクデータの優先順取得
1003
+ * @param {string[]} _defaultHeaders
1004
+ * @returns {string[]}
1005
+ */
1006
+ const getPriorityHeader = (_defaultHeaders = []) => {
1007
+ if (_defaultHeaders.length > 0) {
1008
+ return makeDedupliArray(_defaultHeaders);
1009
+ }
1010
+
1011
+ const list = [];
1012
+ const ptnName = `${(g_keyObj.currentPtn === -1 ? g_keyObj.storagePtn : g_keyObj.currentPtn) + 1}`;
1013
+ const keyName = setVal(g_keyObj[`transKey${_keyCtrlPtn}`], g_keyObj.currentKey);
1014
+ let type = ``;
1015
+ if (g_stateObj.scroll !== C_FLG_HYPHEN) {
1016
+ type = `Alt`;
1017
+ } else if (g_stateObj.reverse === C_FLG_ON) {
1018
+ type = `Rev`;
1019
+ }
1020
+
1021
+ [g_stateObj.scroll, type, ``].forEach(header => {
1022
+ list.push(`${header}[${ptnName}]`, `${header}<${keyName}>`);
1023
+ if (hasVal(g_keyObj[`transKey${_keyCtrlPtn}`])) {
1024
+ list.push(`${header}A`);
1025
+ }
1026
+ });
1027
+ list.push(g_stateObj.scroll, type, ``);
1028
+
1029
+ return makeDedupliArray(list);
1030
+ };
1031
+
1032
+ /**
1033
+ * 歌詞データの分解
1034
+ * @param {string} _scoreNo
1035
+ * @returns {string[][]}
1036
+ */
1037
+ const makeWordData = _scoreNo => {
1038
+ const wordDataList = [];
1039
+ const wordTargets = [];
1040
+ let wordReverseFlg = false;
1041
+ const divideCnt = getKeyInfo().divideCnt;
1042
+ const addDataList = (_type = ``) => wordTargets.push(...getPriorityVal(_type, _scoreNo));
1043
+ getPriorityHeader().forEach(val => addDataList(val));
1044
+ makeDedupliArray(wordTargets).forEach(val => wordDataList.push(getRefData(`word`, val)));
1045
+
1046
+ if (g_stateObj.reverse === C_FLG_ON) {
1047
+ let wordTarget = ``;
1048
+ for (let val of makeDedupliArray(wordTargets)) {
1049
+ if (getRefData(`word`, val) !== undefined) {
1050
+ wordTarget = val;
1051
+ break;
1052
+ }
1053
+ }
1054
+
1055
+ // wordRev_dataが指定されている場合はそのままの位置を採用
1056
+ // word_dataのみ指定されている場合、下記ルールに従って設定
1057
+ if (!wordTarget.includes(`Rev`) && g_stateObj.scroll === C_FLG_HYPHEN) {
1058
+ // Reverse時の歌詞の自動反転制御設定
1059
+ if (g_headerObj.wordAutoReverse !== C_DIS_AUTO) {
1060
+ wordReverseFlg = g_headerObj.wordAutoReverse === C_FLG_ON;
1061
+ } else if (keyNum === divideCnt + 1) {
1062
+ wordReverseFlg = true;
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ const inputWordData = wordDataList.find((v) => v !== undefined);
1068
+ return (inputWordData !== undefined ? makeSpriteWordData(inputWordData, wordReverseFlg) : [[], -1]);
1069
+ };
1070
+
1071
+ /**
1072
+ * 多層歌詞データの格納処理
1073
+ * @param {object} _data
1074
+ * @param {boolean} _reverseFlg
1075
+ * @returns
1076
+ */
1077
+ const makeSpriteWordData = (_data, _reverseFlg = false) => {
1078
+ const wordData = [];
1079
+ let wordMaxDepth = -1;
1080
+ let wordReverseFlg = _reverseFlg;
1081
+ const tmpArrayData = splitLF(_data);
1082
+
1083
+ if (g_headerObj.wordAutoReverse === C_DIS_AUTO) {
1084
+ tmpArrayData.filter(data => hasVal(data) && data?.indexOf(`<br>`) !== -1).forEach(() => wordReverseFlg = false);
1085
+ }
1086
+
1087
+ tmpArrayData.filter(data => hasVal(data)).forEach(tmpData => {
1088
+ const tmpWordData = tmpData.split(`,`).map(val => trimStr(val));
1089
+ for (let k = 0; k < tmpWordData.length; k += 3) {
1090
+ if (!hasVal(tmpWordData[k])) {
1091
+ continue;
1092
+ } else if (tmpWordData[k + 1] === `-`) {
1093
+ break;
1094
+ }
1095
+ tmpWordData[k] = calcFrame(setVal(tmpWordData[k], ``, C_TYP_CALC));
1096
+ tmpWordData[k + 1] = setVal(tmpWordData[k + 1], 0, C_TYP_CALC);
1097
+ tmpWordData[k + 1] = Math.floor(tmpWordData[k + 1] / 2) * 2 +
1098
+ (tmpWordData[k + 1] + Number(wordReverseFlg)) % 2;
1099
+
1100
+ if (tmpWordData[k + 1] > wordMaxDepth) {
1101
+ wordMaxDepth = tmpWordData[k + 1];
1102
+ }
1103
+
1104
+ // 同一行数で数字が取得できるまでは歌詞表示と見做して結合
1105
+ let m = 3;
1106
+ while (hasVal(tmpWordData[m]) && isNaN(parseInt(tmpWordData[m])) && m < tmpWordData.length) {
1107
+ tmpWordData[k + 2] += `,${tmpWordData[k + m]}`;
1108
+ tmpWordData.splice(k + m, 1);
1109
+ }
1110
+
1111
+ // 歌詞表示データの格納
1112
+ let dataCnts = 0;
1113
+ [wordData[tmpWordData[k]], dataCnts] =
1114
+ checkDuplicatedObjects(wordData[tmpWordData[k]]);
1115
+
1116
+ if (tmpWordData.length > 3 && tmpWordData.length < 6) {
1117
+ tmpWordData[3] = setIntVal(tmpWordData[3], C_WOD_FRAME);
1118
+ wordData[tmpWordData[0]][dataCnts].push(tmpWordData[1],
1119
+ escapeHtmlForEnabledTag(tmpWordData[2]), tmpWordData[3]);
1120
+ break;
1121
+ } else {
1122
+ wordData[tmpWordData[k]][dataCnts].push(tmpWordData[k + 1],
1123
+ escapeHtmlForEnabledTag(tmpWordData[k + 2] ?? ``));
1124
+ }
1125
+ }
1126
+ });
1127
+
1128
+ return [wordData, wordMaxDepth];
1129
+ };
1130
+
1131
+ /**
1132
+ * 背景・マスク、スキン変更データの分解
1133
+ * @param {string} _header
1134
+ * @param {string} _scoreNo 譜面番号
1135
+ * @param {string[]} [object.resultTypes] リザルトモーションの種類 (result, failedB, failedS)
1136
+ * @returns {any[][]}
1137
+ */
1138
+ const makeBackgroundData = (_header, _scoreNo, { resultTypes = [] } = {}) => {
1139
+ const dataList = [];
1140
+ const animationTargets = [];
1141
+ const calcFrameFunc = resultTypes.length > 0 ? undefined : calcFrame;
1142
+ const addDataList = (_type = ``) => animationTargets.push(...getPriorityVal(_type, _scoreNo));
1143
+ getPriorityHeader(resultTypes).forEach(val => addDataList(val));
1144
+ makeDedupliArray(animationTargets).forEach(val => dataList.push(getRefData(_header, val)));
1145
+
1146
+ const data = dataList.find((v) => v !== undefined);
1147
+ return (data !== undefined ? g_animationFunc.make[_header](data, calcFrameFunc) : [[], -1]);
1148
+ };
1149
+
1150
+ // 速度変化データの分解 (2つで1セット)
1151
+ let speedFooter = ``;
1152
+ if (hasVal(_dosObj[`speed${scoreIdHeader}_data`])) {
1153
+ speedFooter = `_data`;
1154
+ }
1155
+ if (hasVal(_dosObj[`speed${scoreIdHeader}_change`])) {
1156
+ speedFooter = `_change`;
1157
+ }
1158
+
1159
+ // 速度変化(個別・全体)の分解 (2つで1セット, セット毎の改行区切り可)
1160
+ obj.boostData = setSpeedData(`boost`, scoreIdHeader);
1161
+ obj.speedData = setSpeedData(`speed`, scoreIdHeader, speedFooter);
1162
+
1163
+ // 色変化(個別・全体)の分解 (3つで1セット, セット毎の改行区切り可)
1164
+ g_typeLists.color.forEach(sprite => {
1165
+ obj[`${sprite}Data`] = setColorData(sprite, scoreIdHeader);
1166
+ if (g_stateObj.dummyId !== ``) {
1167
+ obj[`${sprite}DummyData`] = setColorData(sprite, _dummyNo);
1168
+ }
1169
+ });
1170
+ // 色変化(新形式)の分解(3~4つで1セット, セット毎の改行区切り)
1171
+ obj.ncolorData = setColor2Data(`ncolor`, scoreIdHeader);
1172
+ if (g_stateObj.dummyId !== ``) {
1173
+ obj.ncolorDummyData = setColor2Data(`ncolor`, _dummyNo);
1174
+ }
1175
+
1176
+ if (_scoreAnalyzeFlg) {
1177
+ return obj;
1178
+ }
1179
+
1180
+ obj.colorData = mergeColorData();
1181
+ obj.dummyColorData = mergeColorData(`Dummy`);
1182
+
1183
+ // レイヤー数の初期値設定(譜面ごとに設定のため)
1184
+ g_stateObj.layerNumDf = 2;
1185
+
1186
+ // 矢印モーション(個別)データの分解(3~4つで1セット, セット毎の改行区切り)
1187
+ obj.arrowCssMotionData = setCssMotionData(`arrow`, scoreIdHeader);
1188
+ obj.frzCssMotionData = setCssMotionData(`frz`, scoreIdHeader);
1189
+ if (g_stateObj.dummyId !== ``) {
1190
+ obj.dummyArrowCssMotionData = setCssMotionData(`arrow`, _dummyNo);
1191
+ obj.dummyFrzCssMotionData = setCssMotionData(`frz`, _dummyNo);
1192
+ }
1193
+
1194
+ // スクロール変化データの分解
1195
+ obj.scrollchData = setScrollchData(scoreIdHeader);
1196
+
1197
+ // 歌詞データの分解 (3つで1セット, セット毎の改行区切り可)
1198
+ obj.wordData = [];
1199
+ obj.wordMaxDepth = -1;
1200
+ if (g_stateObj.d_background === C_FLG_OFF) {
1201
+ } else {
1202
+ [obj.wordData, obj.wordMaxDepth] = makeWordData(scoreIdHeader);
1203
+ }
1204
+
1205
+ // 背景・マスク・スキン変更データの分解 (下記すべてで1セット、改行区切り)
1206
+ // - 背景・マスク: [フレーム数, 階層, 背景パス, class(CSSで別定義), X, Y, width, height, opacity, animationName, animationDuration, animationFillMode]
1207
+ // - スキン変更 : [フレーム数, CSSカスタムプロパティ名, 設定内容]
1208
+ g_animationData.forEach(sprite => {
1209
+ obj[`${sprite}Data`] = [];
1210
+ obj[`${sprite}MaxDepth`] = -1;
1211
+
1212
+ if (g_stateObj.d_background === C_FLG_OFF) {
1213
+ } else {
1214
+ [obj[`${sprite}Data`], obj[`${sprite}MaxDepth`]] = makeBackgroundData(sprite, scoreIdHeader);
1215
+ }
1216
+
1217
+ if (g_stateObj.d_background === C_FLG_OFF && g_headerObj.resultMotionSet) {
1218
+ [`Result`, `Failed`].forEach(backName => {
1219
+ g_headerObj[`${backName}Data`] = [];
1220
+ g_headerObj[`${backName}MaxDepth`] = -1;
1221
+ });
1222
+ } else {
1223
+ [g_headerObj[`${sprite}ResultData`], g_headerObj[`${sprite}ResultMaxDepth`]] =
1224
+ makeBackgroundData(sprite, scoreIdHeader, { resultTypes: [`result`] });
1225
+ [g_headerObj[`${sprite}FailedData`], g_headerObj[`${sprite}FailedMaxDepth`]] =
1226
+ makeBackgroundData(sprite, scoreIdHeader, { resultTypes: [`failed${g_stateObj.lifeMode.slice(0, 1)}`, `result`] });
1227
+ }
1228
+ });
1229
+
1230
+ // キー変化定義
1231
+ obj.keychFrames = [];
1232
+ obj.keychTarget = [];
1233
+ obj.keychTargetAlpha = [];
1234
+ if (hasVal(getRefData(`keych`, `${scoreIdHeader}_data`))) {
1235
+ const keychdata = splitLF2(getRefData(`keych`, `${scoreIdHeader}_data`), `,`);
1236
+ obj.keychFrames.push(...(keychdata.filter((val, j) => j % 2 === 0)).map(val => val === `0` ? 0 : calcFrame(val)));
1237
+
1238
+ keychdata.filter((val, j) => j % 2 === 1)?.forEach(targets => {
1239
+ const targetKeyList = [], targetKeyAlpha = [];
1240
+ targets?.split(`/`).forEach(target => {
1241
+ targetKeyList.push(trimStr(target?.split(`:`)[0]));
1242
+ targetKeyAlpha.push(trimStr(target?.split(`:`)[1]) || 1);
1243
+ })
1244
+ obj.keychTarget.push(targetKeyList);
1245
+ obj.keychTargetAlpha.push(targetKeyAlpha);
1246
+ });
1247
+ }
1248
+ obj.keychFrames.unshift(0);
1249
+ obj.keychTarget.unshift([`0`]);
1250
+ obj.keychTargetAlpha.unshift([1]);
1251
+
1252
+ return obj;
1253
+ };
1254
+
1255
+ /**
1256
+ * ライフ回復量・ダメージ量の算出
1257
+ * @param {number} _allArrows
1258
+ */
1259
+ const calcLifeVals = _allArrows => {
1260
+
1261
+ if (g_stateObj.lifeVariable === C_FLG_ON) {
1262
+ g_workObj.lifeRcv = calcLifeVal(g_stateObj.lifeRcv, _allArrows);
1263
+ g_workObj.lifeDmg = calcLifeVal(g_stateObj.lifeDmg, _allArrows);
1264
+ } else {
1265
+ g_workObj.lifeRcv = g_stateObj.lifeRcv;
1266
+ g_workObj.lifeDmg = g_stateObj.lifeDmg;
1267
+ }
1268
+ g_workObj.lifeBorder = g_headerObj.maxLifeVal * g_stateObj.lifeBorder / 100;
1269
+ g_workObj.lifeInit = g_headerObj.maxLifeVal * g_stateObj.lifeInit / 100;
1270
+ };
1271
+
1272
+ /**
1273
+ * ライフ回復量・ダメージ量の算出
1274
+ * @param {number} _val
1275
+ * @param {number} _allArrows
1276
+ * @returns {number}
1277
+ */
1278
+ const calcLifeVal = (_val, _allArrows) => _val * g_headerObj.maxLifeVal / _allArrows;
1279
+
1280
+ /**
1281
+ * 最終フレーム数の取得
1282
+ * @param {object} _dataObj
1283
+ * @param {string} [_keyCtrlPtn]
1284
+ * @returns {number}
1285
+ */
1286
+ const getLastFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`) => {
1287
+
1288
+ let tmpLastNum = 0;
1289
+ const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
1290
+
1291
+ for (let j = 0; j < keyNum; j++) {
1292
+ const data = [
1293
+ _dataObj.arrowData[j],
1294
+ _dataObj.frzData[j],
1295
+ _dataObj.dummyArrowData[j],
1296
+ _dataObj.dummyFrzData[j]
1297
+ ];
1298
+
1299
+ data.filter(data => hasVal(data)).forEach(_objData => {
1300
+ if (_objData.at(-1) > tmpLastNum) {
1301
+ tmpLastNum = _objData.at(-1);
1302
+ }
1303
+ });
1304
+ }
1305
+ return tmpLastNum;
1306
+ };
1307
+
1308
+ /**
1309
+ * 最初の矢印フレームの取得
1310
+ * @param {object} _dataObj
1311
+ * @param {string} [_keyCtrlPtn]
1312
+ * @returns {number}
1313
+ */
1314
+ const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`) => {
1315
+
1316
+ let tmpFirstNum = Infinity;
1317
+ const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
1318
+
1319
+ for (let j = 0; j < keyNum; j++) {
1320
+ const data = [
1321
+ _dataObj.arrowData[j],
1322
+ _dataObj.frzData[j],
1323
+ _dataObj.dummyArrowData[j],
1324
+ _dataObj.dummyFrzData[j]
1325
+ ];
1326
+
1327
+ data.filter(data => hasVal(data)).forEach(_objData => {
1328
+ if (_objData[0] !== `` && _objData[0] < tmpFirstNum && _objData[0] + g_limitObj.adjustment > 0) {
1329
+ tmpFirstNum = _objData[0];
1330
+ }
1331
+ });
1332
+ }
1333
+ return (tmpFirstNum === Infinity ? 0 : tmpFirstNum);
1334
+ };
1335
+
1336
+ /**
1337
+ * 開始フレームの取得
1338
+ * @param {number} _lastFrame
1339
+ * @param {number} [_fadein=0]
1340
+ * @param {number} [_scoreId=g_scoreObj.scoreId]
1341
+ * @returns {number}
1342
+ */
1343
+ const getStartFrame = (_lastFrame, _fadein = 0, _scoreId = g_stateObj.scoreId) => {
1344
+ let frameNum = setIntVal(g_headerObj.startFrame?.[_scoreId], setIntVal(g_headerObj.startFrame?.[0], 0));
1345
+ if (_lastFrame >= frameNum) {
1346
+ frameNum = Math.round(_fadein / 100 * (_lastFrame - frameNum)) + frameNum;
1347
+ }
1348
+ return frameNum;
1349
+ };
1350
+
1351
+ /**
1352
+ * 速度補正値の算出
1353
+ * @param {number} _speed
1354
+ * @returns {number}
1355
+ */
1356
+ const getSpeedFactor = _speed => {
1357
+ if (Math.abs(_speed) === 1) {
1358
+ // ±1 はそのまま返して符号を保持
1359
+ return _speed;
1360
+ }
1361
+ if (g_stateObj.d_velocity === `Extreme`) {
1362
+ // |speed|>1 を強めに、<1 を弱めに
1363
+ return _speed * (Math.abs(_speed) > 1 ? 1.5 : 0.75);
1364
+ }
1365
+ if (g_stateObj.d_velocity === `Soft`) {
1366
+ // 変化幅を緩和(符号は維持)
1367
+ return (1 + Math.abs(_speed)) / 2 * Math.sign(_speed);
1368
+ }
1369
+ return _speed;
1370
+ };
1371
+
1372
+ /**
1373
+ * 各フレームごとの速度を格納
1374
+ * @param {object} _speedData
1375
+ * @param {number} _lastFrame
1376
+ * @returns {number[]}
1377
+ */
1378
+ const setSpeedOnFrame = (_speedData, _lastFrame) => {
1379
+
1380
+ const speedOnFrame = [];
1381
+ let currentSpeed = g_stateObj.speed * g_headerObj.baseSpeed * 2;
1382
+
1383
+ for (let frm = 0, s = 0; frm <= _lastFrame; frm++) {
1384
+ while (frm >= _speedData?.[s]) {
1385
+ currentSpeed = getSpeedFactor(_speedData[s + 1]) * g_stateObj.speed * g_headerObj.baseSpeed * 2;
1386
+ s += 2;
1387
+ }
1388
+ speedOnFrame[frm] = currentSpeed;
1389
+ }
1390
+ return speedOnFrame;
1391
+ };
1392
+
1393
+ /**
1394
+ * Motionオプション適用時の矢印別の速度設定
1395
+ * - 矢印が表示される最大フレーム数を 101フレーム と定義。
1396
+ * @returns {number[]}
1397
+ */
1398
+ const setMotionOnFrame = () => g_motionFunc.get(g_stateObj.motion)(fillArray(101));
1399
+
1400
+ /**
1401
+ * Boost用の適用関数
1402
+ * - ステップゾーンに近づくにつれて加速量を大きく/小さくする (16 → 85)
1403
+ * @param {number[]} _frms
1404
+ * @param {number} _spd
1405
+ * @param {number} [_pnFlg=1] 正負(1 もしくは -1)
1406
+ * @returns {number[]}
1407
+ */
1408
+ const getBoostTrace = (_frms, _spd, _pnFlg = 1) => {
1409
+ for (let j = C_MOTION_STD_POS + 1; j < C_MOTION_STD_POS + 70; j++) {
1410
+ _frms[j] = (C_MOTION_STD_POS + 70 - j) * _pnFlg * _spd / 50;
1411
+ }
1412
+ return _frms;
1413
+ };
1414
+
1415
+ /**
1416
+ * Brake用の適用関数
1417
+ * - 初期は+2x、ステップゾーンに近づくにつれて加速量を下げる (20 → 34)
1418
+ * @param {number[]} _frms
1419
+ * @returns {number[]}
1420
+ */
1421
+ const getBrakeTrace = _frms => {
1422
+ for (let j = C_MOTION_STD_POS + 5; j < C_MOTION_STD_POS + 19; j++) {
1423
+ _frms[j] = (j - 15) * 4 / 14;
1424
+ }
1425
+ for (let j = C_MOTION_STD_POS + 19; j <= g_headerObj.playingHeight / 2; j++) {
1426
+ _frms[j] = 4;
1427
+ }
1428
+ return _frms;
1429
+ };
1430
+
1431
+ /**
1432
+ * Fountain用の適用関数
1433
+ * - 反対側から出現し、画面中央付近で折り返す。タイミングは初期速度により変化。
1434
+ * @param {number[]} _frms
1435
+ * @param {number} _spd
1436
+ * @returns {number[]}
1437
+ */
1438
+ const getFountainTrace = (_frms, _spd) => {
1439
+ const minj = C_MOTION_STD_POS + 1;
1440
+ const maxj = C_MOTION_STD_POS + Math.ceil(400 / _spd) + 1;
1441
+ const maxMotionFrm = Math.max(maxj, C_MOTION_STD_POS + g_sHeight / 2);
1442
+ const diff = 50 / (maxj - minj);
1443
+ const factor = 0.5 + _spd / 40;
1444
+
1445
+ for (let j = minj; j < maxMotionFrm; j++) {
1446
+ _frms[j] = Math.floor((10 - (j - C_MOTION_STD_POS - 1) * diff) * factor);
1447
+ }
1448
+ return _frms;
1449
+ };
1450
+
1451
+ /**
1452
+ * 最初のフレームで出現する矢印が、ステップゾーンに到達するまでのフレーム数を取得
1453
+ * @param {number} _startFrame
1454
+ * @param {object} _speedOnFrame
1455
+ * @returns {number}
1456
+ */
1457
+ const getFirstArrivalFrame = (_startFrame, _speedOnFrame) => {
1458
+ let startY = 0;
1459
+ let frm = _startFrame;
1460
+
1461
+ while (g_scoreObj.distY - startY > 0) {
1462
+ startY += _speedOnFrame[frm];
1463
+ frm++;
1464
+ }
1465
+ return frm;
1466
+ };
1467
+
1468
+ /**
1469
+ * 矢印・フリーズアロー・速度/色変化格納処理
1470
+ * @param {object} _dataObj
1471
+ * @param {object} _speedOnFrame
1472
+ * @param {number} _firstArrivalFrame
1473
+ */
1474
+ const pushArrows = (_dataObj, _speedOnFrame, _firstArrivalFrame) => {
1475
+
1476
+ // 矢印・フリーズアロー・速度/色変化用 フレーム別処理配列
1477
+ [``, `Dummy`].forEach(header =>
1478
+ g_typeLists.dataList.forEach(name => g_workObj[`mk${header}${name}`] = []));
1479
+
1480
+ const boostData = [];
1481
+ if (hasArrayList(_dataObj.boostData, 2)) {
1482
+ const _data = _dataObj.boostData.concat();
1483
+ for (let k = 0; k < _data.length; k += 2) {
1484
+ boostData.push({ frame: _data[k], spd: getSpeedFactor(_data[k + 1]) });
1485
+ }
1486
+ if (boostData.length > 0 && boostData[0].frame > 0) {
1487
+ boostData.unshift({ frame: 0, spd: 1 });
1488
+ }
1489
+ }
1490
+ const getSpdByFrame = _targetFrame => {
1491
+ // targetFrame 以下の frame を持つ要素の中で、最後(最新)のものを見つける
1492
+ const result = boostData.findLast((item) => _targetFrame >= item.frame);
1493
+
1494
+ // 該当するものがない場合(frame: 0 未満など)のフォールバック
1495
+ return result ? result.spd : 1;
1496
+ };
1497
+
1498
+ /**
1499
+ * 矢印・フリーズアローのデータ格納処理
1500
+ * @param {number} _j
1501
+ * @param {number} _k
1502
+ * @param {number[]} _data
1503
+ * @param {number} _startPoint
1504
+ * @param {string} _header
1505
+ * @param {boolean} _frzFlg
1506
+ * @param {number} object.initY
1507
+ * @param {number} object.initBoostY
1508
+ * @param {number} object.arrivalFrame
1509
+ * @param {number} object.motionFrame
1510
+ */
1511
+ const setNotes = (_j, _k, _data, _startPoint, _header, _frzFlg = false, { initY, initBoostY, arrivalFrame, motionFrame, boostSpd } = {}) => {
1512
+ if (_startPoint >= 0) {
1513
+ const arrowAttrs = {
1514
+ pos: _j, initY, initBoostY, arrivalFrame, motionFrame, boostSpd,
1515
+ get boostDir() {
1516
+ return Math.sign(this.boostSpd);
1517
+ }
1518
+ };
1519
+ if (g_workObj[`mk${_header}Arrow`][_startPoint] === undefined) {
1520
+ g_workObj[`mk${_header}Arrow`][_startPoint] = [arrowAttrs];
1521
+ } else {
1522
+ let insertFlg = false;
1523
+ for (let m = 0; m < g_workObj[`mk${_header}Arrow`][_startPoint].length; m++) {
1524
+ if (arrowAttrs.arrivalFrame < g_workObj[`mk${_header}Arrow`][_startPoint][m].arrivalFrame) {
1525
+ g_workObj[`mk${_header}Arrow`][_startPoint].splice(m, 0, arrowAttrs);
1526
+ insertFlg = true;
1527
+ break;
1528
+ }
1529
+ }
1530
+ if (!insertFlg) {
1531
+ g_workObj[`mk${_header}Arrow`][_startPoint].push(arrowAttrs);
1532
+ }
1533
+ }
1534
+ if (_frzFlg) {
1535
+ g_workObj[`mk${_header}Length`][_j][_k] = getFrzLength(_speedOnFrame, _data[_k], _data[_k + 1]);
1536
+ }
1537
+ } else if (_frzFlg && g_workObj[`mk${_header}Length`][_j] !== undefined) {
1538
+ g_workObj[`mk${_header}Length`][_j] = structuredClone(g_workObj[`mk${_header}Length`][_j].slice(_k + 2));
1539
+ }
1540
+ };
1541
+
1542
+ /**
1543
+ * 矢印・フリーズアローの出現位置計算
1544
+ * @param {number} _j
1545
+ * @param {number[]} _data
1546
+ * @param {string} _header
1547
+ * @param {boolean} _frzFlg
1548
+ */
1549
+ const calcNotes = (_j, _data, _header = ``, _frzFlg = false) => {
1550
+ if (_data === undefined) {
1551
+ return;
1552
+ }
1553
+
1554
+ const calcInitBoostY = _arrivalFrame => sumData(g_workObj.motionOnFrames.filter((val, j) => j <= _arrivalFrame));
1555
+ const camelHeader = toCapitalize(_header);
1556
+ const setcnt = (_frzFlg ? 2 : 1);
1557
+ if (_frzFlg && _data.length % 2 !== 0) {
1558
+ _data.pop();
1559
+ }
1560
+
1561
+ let spdNext = Infinity;
1562
+ let spdk = (_dataObj.speedData?.length ?? 0) - 2;
1563
+ let spdPrev = _dataObj.speedData?.[spdk] ?? 0;
1564
+
1565
+ // 最後尾のデータから計算して格納
1566
+ const lastk = _data.length - setcnt;
1567
+ let arrowArrivalFrm = _data[lastk];
1568
+ let tmpObj = getArrowStartFrame(arrowArrivalFrm, _speedOnFrame);
1569
+
1570
+ let startPoint = tmpObj.frm;
1571
+ let arrivalFrm = tmpObj.arrivalFrm;
1572
+ let minNotesFrame = startPoint;
1573
+
1574
+ if (_frzFlg) {
1575
+ g_workObj[`mk${camelHeader}Length`][_j] = [];
1576
+ }
1577
+ setNotes(_j, lastk, _data, startPoint, camelHeader, _frzFlg, {
1578
+ initY: tmpObj.startY, initBoostY: calcInitBoostY(tmpObj.motionFrm),
1579
+ arrivalFrame: tmpObj.arrivalFrm, motionFrame: tmpObj.motionFrm, boostSpd: getSpdByFrame(arrowArrivalFrm)
1580
+ });
1581
+
1582
+ // 矢印は1つずつ、フリーズアローは2つで1セット
1583
+ for (let k = lastk - setcnt; k >= 0; k -= setcnt) {
1584
+ arrowArrivalFrm = _data[k];
1585
+
1586
+ if (arrowArrivalFrm < _firstArrivalFrame) {
1587
+
1588
+ // 出現位置が開始前の場合は除外
1589
+ if (_frzFlg && g_workObj[`mk${camelHeader}Length`][_j] !== undefined) {
1590
+ g_workObj[`mk${camelHeader}Length`][_j] = structuredClone(g_workObj[`mk${camelHeader}Length`][_j].slice(k + 2));
1591
+ }
1592
+ break;
1593
+
1594
+ } else if ((arrowArrivalFrm - arrivalFrm > spdPrev)
1595
+ && arrowArrivalFrm < spdNext) {
1596
+
1597
+ // 最初から最後まで同じスピードのときは前回のデータを流用(ステップゾーン到達フレーム - 移動フレーム)
1598
+ startPoint = arrowArrivalFrm - arrivalFrm;
1599
+
1600
+ } else {
1601
+
1602
+ // 速度変化が間に入るときは再計算
1603
+ while (arrowArrivalFrm < spdPrev) {
1604
+ spdk -= 2;
1605
+ spdNext = spdPrev;
1606
+ spdPrev = _dataObj.speedData[spdk];
1607
+ }
1608
+ tmpObj = getArrowStartFrame(arrowArrivalFrm, _speedOnFrame);
1609
+ startPoint = tmpObj.frm;
1610
+ arrivalFrm = tmpObj.arrivalFrm;
1611
+
1612
+ // --- 逆転検知ロジック ---
1613
+ // 後ろからループしているため、minNotesFrameには「自分より譜面上後ろにあるノーツ」の
1614
+ // 最小生成フレーム(最も早く出現するもの)が入っている。
1615
+
1616
+ // 「自分より後ろのノーツ」の方が、「自分」よりも早く出現する場合、
1617
+ // 配列の順序と出現時間の順序が入れ替わっている(逆転)とみなす。
1618
+ // 逆転している場合は、最小生成フレームまでさらに遡って、出現フレームを再計算する。
1619
+ if (minNotesFrame < startPoint) {
1620
+ tmpObj = getAdjArrowStartFrame({ ...tmpObj, frm: startPoint }, _speedOnFrame, minNotesFrame);
1621
+ startPoint = tmpObj.frm;
1622
+ arrivalFrm = tmpObj.arrivalFrm;
1623
+ }
1624
+ }
1625
+ // 最小値を更新
1626
+ minNotesFrame = Math.min(minNotesFrame, startPoint);
1627
+
1628
+ // 出現タイミングを保存
1629
+ setNotes(_j, k, _data, startPoint, camelHeader, _frzFlg, {
1630
+ initY: tmpObj.startY, initBoostY: calcInitBoostY(tmpObj.motionFrm),
1631
+ arrivalFrame: tmpObj.arrivalFrm, motionFrame: tmpObj.motionFrm, boostSpd: getSpdByFrame(arrowArrivalFrm)
1632
+ });
1633
+ }
1634
+ };
1635
+
1636
+ for (let j = 0; j < getKeyInfo().keyNum; j++) {
1637
+
1638
+ // 矢印の出現フレーム数計算
1639
+ calcNotes(j, _dataObj.arrowData[j]);
1640
+ calcNotes(j, _dataObj.dummyArrowData[j], `dummy`);
1641
+
1642
+ // フリーズアローの出現フレーム数計算
1643
+ calcNotes(j, _dataObj.frzData[j], `frz`, true);
1644
+ calcNotes(j, _dataObj.dummyFrzData[j], `dummyFrz`, true);
1645
+ }
1646
+
1647
+ /**
1648
+ * 色変化・モーションデータ・スクロール反転データのタイミング更新
1649
+ * - この関数を使用する場合、配列グループの先頭2つが「フレーム数、矢印番号」となっていないと動作しない
1650
+ * @param {string} _type
1651
+ * @param {string} _header
1652
+ * @param {Function} _setFunc 後続実行関数
1653
+ * @param {number} object._term 1セット当たりのデータ数(デフォルトは後続実行関数の引数の数-1, デフォルト引数・オブジェクト引数除く)
1654
+ * @param {boolean} object._colorFlg 個別色変化フラグ
1655
+ * @param {boolean} object._calcFrameFlg 逆算を無条件で行うかどうかの可否
1656
+ * @param {string} object._propName 色変化種類 (Arrow, ArrowShadow, FrzNormal, FrzNormalBar, ...)
1657
+ */
1658
+ const calcDataTiming = (_type, _header, _setFunc = () => true,
1659
+ { _term = _setFunc.length - 1, _colorFlg = false, _calcFrameFlg = false, _propName = `` } = {}) => {
1660
+
1661
+ const camelHeader = _header === `` ? _type : `${_header}${toCapitalize(_type)}`;
1662
+ const baseData = hasVal(_propName) ? _dataObj[`${camelHeader}Data`][_propName] : _dataObj[`${camelHeader}Data`];
1663
+
1664
+ if (!hasArrayList(baseData, _term)) {
1665
+ return;
1666
+ }
1667
+ const frontData = [];
1668
+ let minDataFrame = Infinity;
1669
+ for (let k = baseData.length - _term; k >= 0; k -= _term) {
1670
+ const calcFrameFlg = (_colorFlg && !baseData[k + 3]) || _calcFrameFlg;
1671
+
1672
+ if (baseData[k] < g_scoreObj.frameNum) {
1673
+ // フェードイン直前にある色変化・モーションデータ・スクロール反転データを取得して格納
1674
+ if (!hasValInArray(baseData[k + 1], frontData)) {
1675
+ frontData.unshift(baseData.slice(k + 1, k + _term));
1676
+ }
1677
+ } else {
1678
+ if (calcFrameFlg) {
1679
+ let tmpObj = getArrowStartFrame(baseData[k], _speedOnFrame);
1680
+
1681
+ // 到達フレームがより早い(=baseData[k]がより小さい)要素なのに、
1682
+ // より遅い要素より後ろで出現してしまう場合は矢印と同様に補正する
1683
+ if (minDataFrame < tmpObj.frm) {
1684
+ tmpObj = getAdjArrowStartFrame(tmpObj, _speedOnFrame, minDataFrame);
1685
+ }
1686
+ if (tmpObj.frm < g_scoreObj.frameNum) {
1687
+ const diff = g_scoreObj.frameNum - tmpObj.frm;
1688
+ tmpObj.frm = g_scoreObj.frameNum;
1689
+ tmpObj.arrivalFrm -= diff;
1690
+ }
1691
+ minDataFrame = Math.min(minDataFrame, tmpObj.frm);
1692
+ baseData[k] = tmpObj.frm;
1693
+ }
1694
+ _setFunc(toCapitalize(_header), ...baseData.slice(k, k + _term));
1695
+ }
1696
+ }
1697
+ frontData.forEach(data => _setFunc(toCapitalize(_header), g_scoreObj.frameNum, ...data));
1698
+ };
1699
+
1700
+ /**
1701
+ * 歌詞表示、背景・マスク表示のフェードイン時調整処理
1702
+ * @param {string} _type
1703
+ * @param {object} _data
1704
+ * @returns {object}
1705
+ */
1706
+ const calcAnimationData = (_type, _data) => {
1707
+
1708
+ const startNum = g_scoreObj.frameNum;
1709
+ const cgArrays = [`word`];
1710
+
1711
+ const isSameDepth = (_j, _k) =>
1712
+ _data[startNum][_j] !== undefined &&
1713
+ _data[startNum][_k] !== undefined &&
1714
+ (cgArrays.includes(_type) ? _data[startNum][_j][0] === _data[startNum][_k][0] :
1715
+ _data[startNum][_j].depth === _data[startNum][_k].depth);
1716
+
1717
+ const isExceptData = {
1718
+ word: (_exceptList, _j) => listMatching(_data[startNum][_j][1] || ``, _exceptList.word),
1719
+ back: (_exceptList, _j) => listMatching(_data[startNum][_j].animationName || ``, _exceptList.back),
1720
+ mask: (_exceptList, _j) => listMatching(_data[startNum][_j].animationName || ``, _exceptList.mask),
1721
+ style: (_exceptList, _j) => listMatching(_data[startNum][_j].depth || ``, _exceptList.style),
1722
+ };
1723
+
1724
+ const getLength = _list =>
1725
+ _list === undefined ? 0 :
1726
+ (cgArrays.includes(_type) ? _list.length : Object.keys(_list).length);
1727
+
1728
+ // フェードイン位置にそれ以前のデータを前追加
1729
+ if (startNum > 0 && _data[startNum] === undefined) {
1730
+ _data[startNum] = [];
1731
+ }
1732
+ for (let j = _data.length - 1; j >= 0; j--) {
1733
+ if (_data[j] !== undefined && j < g_scoreObj.frameNum) {
1734
+ _data[startNum].unshift(..._data[j]);
1735
+ _data[j] = undefined;
1736
+ }
1737
+ }
1738
+
1739
+ // 重複する深度をカット(後方優先)
1740
+ // ただし、除外リストにあるデータは残す
1741
+ for (let j = getLength(_data[startNum]) - 1; j >= 0; j--) {
1742
+ if (_data[startNum][j] !== undefined) {
1743
+ for (let k = j - 1; k >= 0; k--) {
1744
+ if (isSameDepth(j, k) && !isExceptData[_type](g_preloadExceptList, k)) {
1745
+ _data[startNum][k] = undefined;
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+ // g_stockForceDelList に合致する消去対象データを検索し、削除
1751
+ for (let j = getLength(_data[startNum]) - 1; j >= 0; j--) {
1752
+ if (_data[startNum][j] !== undefined && isExceptData[_type](g_stockForceDelList, j)) {
1753
+ _data[startNum][j] = undefined;
1754
+ }
1755
+ }
1756
+
1757
+ // カットした箇所をリストから削除
1758
+ if (getLength(_data[startNum]) > 0) {
1759
+ _data[startNum] = _data[startNum].filter(list => getLength(list) > 0);
1760
+ }
1761
+
1762
+ return _data;
1763
+ };
1764
+
1765
+ // 個別・全体色変化、モーションデータ・スクロール反転データのタイミング更新
1766
+ [``, `dummy`].forEach(type => {
1767
+ calcDataTiming(`color`, type, pushColors, { _term: 4, _colorFlg: true });
1768
+ if (_dataObj[`ncolor${type}Data`] !== undefined) {
1769
+ Object.keys(_dataObj[`ncolor${type}Data`]).forEach(pattern =>
1770
+ calcDataTiming(`ncolor`, type, pushColors, { _term: 5, _colorFlg: true, _propName: pattern }));
1771
+ }
1772
+ });
1773
+
1774
+ g_typeLists.arrow.forEach(header =>
1775
+ calcDataTiming(`cssMotion`, header, pushCssMotions, { _calcFrameFlg: true }));
1776
+
1777
+ calcDataTiming(`scrollch`, ``, pushScrollchs, { _calcFrameFlg: true });
1778
+
1779
+ g_fadeinStockList.forEach(type =>
1780
+ _dataObj[`${type}Data`] = calcAnimationData(type, _dataObj[`${type}Data`]));
1781
+
1782
+
1783
+ // 実際に処理させる途中変速配列を作成
1784
+ g_workObj.speedData = [];
1785
+ g_workObj.speedData.push(g_scoreObj.frameNum);
1786
+ g_workObj.speedData.push(_speedOnFrame[g_scoreObj.frameNum]);
1787
+
1788
+ for (let k = 0; k < _dataObj?.speedData.length; k += 2) {
1789
+ if (_dataObj.speedData[k] >= g_scoreObj.frameNum) {
1790
+ g_workObj.speedData.push(_dataObj.speedData[k]);
1791
+ g_workObj.speedData.push(_speedOnFrame[_dataObj.speedData[k]]);
1792
+ }
1793
+ }
1794
+ };
1795
+
1796
+ /**
1797
+ * 出現フレームを指定フレームまで強制的に遡って再計算する(逆転補正用)
1798
+ * @param {object} _obj getArrowStartFrameの戻り値
1799
+ * @param {object} _speedOnFrame
1800
+ * @param {number} _targetFrame
1801
+ * @returns {{ frm: number, startY: number, arrivalFrm: number, motionFrm: number }}
1802
+ */
1803
+ const getAdjArrowStartFrame = (_obj, _speedOnFrame, _targetFrame) => {
1804
+ while (_obj.frm > _targetFrame) {
1805
+ _obj.startY += _speedOnFrame[_obj.frm - 1];
1806
+
1807
+ if (_speedOnFrame[_obj.frm - 1] !== 0) {
1808
+ _obj.motionFrm++;
1809
+ }
1810
+ _obj.frm--;
1811
+ _obj.arrivalFrm++;
1812
+ }
1813
+ return _obj;
1814
+ };
1815
+
1816
+ /**
1817
+ * ステップゾーン到達地点から逆算して開始フレームを取得
1818
+ * @param {number} _frame
1819
+ * @param {object} _speedOnFrame
1820
+ * @returns {{ frm: number, startY: number, arrivalFrm: number, motionFrm: number }}
1821
+ */
1822
+ const getArrowStartFrame = (_frame, _speedOnFrame) => {
1823
+
1824
+ const obj = {
1825
+ frm: _frame,
1826
+ startY: 0,
1827
+ arrivalFrm: 0,
1828
+ motionFrm: C_MOTION_STD_POS
1829
+ };
1830
+
1831
+ while (g_scoreObj.distY - obj.startY > 0) {
1832
+ obj.startY += _speedOnFrame[obj.frm - 1];
1833
+
1834
+ if (_speedOnFrame[obj.frm - 1] !== 0) {
1835
+ obj.motionFrm++;
1836
+ }
1837
+ obj.frm--;
1838
+ obj.arrivalFrm++;
1839
+ }
1840
+
1841
+ return obj;
1842
+ };
1843
+
1844
+ /**
1845
+ * 速度を加味したフリーズアローの長さを取得
1846
+ * @param {object} _speedOnFrame
1847
+ * @param {number} _startFrame
1848
+ * @param {number} _endFrame
1849
+ * @returns {number}
1850
+ */
1851
+ const getFrzLength = (_speedOnFrame, _startFrame, _endFrame) => {
1852
+ let frzLength = 0;
1853
+
1854
+ for (let frm = _startFrame; frm < _endFrame; frm++) {
1855
+ frzLength += _speedOnFrame[frm];
1856
+ }
1857
+ return frzLength;
1858
+ };
1859
+
1860
+ /**
1861
+ * キーパターン(デフォルト)に対応する矢印番号を格納
1862
+ * - 色変化、矢印・フリーズアローモーション、スクロール変化で
1863
+ * 矢印レーンの番号を実際のキーパターンに対応する番号に置き換える際に使用
1864
+ * - 例: [0, 1, 2, 3, 4] -> [4, 0, 1, 2, 3]
1865
+ */
1866
+ const convertReplaceNums = () => {
1867
+ const tkObj = getKeyInfo();
1868
+ const baseCharas = g_keyObj[`chara${g_keyObj.currentKey}_0`];
1869
+ const convCharas = g_keyObj[`chara${tkObj.keyCtrlPtn}`];
1870
+
1871
+ g_workObj.replaceNums = [];
1872
+
1873
+ for (let j = 0; j < tkObj.keyNum; j++) {
1874
+ for (let k = 0; k < tkObj.keyNum; k++) {
1875
+ if (baseCharas[j] === convCharas[k]) {
1876
+ g_workObj.replaceNums[j] = k;
1877
+ continue;
1878
+ }
1879
+ }
1880
+ }
1881
+ };
1882
+
1883
+ /**
1884
+ * 色情報の格納
1885
+ * @param {string} _header
1886
+ * @param {number} _frame
1887
+ * @param {number} _val 色番号
1888
+ * @param {string} _colorCd カラーコード
1889
+ * @param {string} _allFlg 全体色変化有無
1890
+ * @param {string} _pattern 色変化種類 (Arrow, ArrowShadow, FrzNormal, FrzNormalBar, ...)
1891
+ */
1892
+ const pushColors = (_header, _frame, _val, _colorCd, _allFlg, _pattern = ``) => {
1893
+
1894
+ const tkObj = getKeyInfo();
1895
+ const grdFlg = (g_colorType === `Type0` ? !g_headerObj.defaultColorgrd[0] : g_headerObj.defaultColorgrd[0]);
1896
+ const colorCd = makeColorGradation(_colorCd, { _defaultColorgrd: [grdFlg, g_headerObj.defaultColorgrd[1]] });
1897
+ const addAll = Number(_allFlg) * 1000;
1898
+ const allUseTypes = [];
1899
+
1900
+ /**
1901
+ * 全体色変化の有効化(フレーム別)
1902
+ * @param {...any} _types
1903
+ */
1904
+ const enabledAll = (..._types) => {
1905
+ if (_allFlg) {
1906
+ _types.forEach(type => g_workObj[`mk${type}ColorChangeAll`][_frame] = true);
1907
+ }
1908
+ };
1909
+
1910
+ /**
1911
+ * 色変化用配列(フレーム別)へのデータ追加
1912
+ * @param {string} _baseStr
1913
+ * @param {number} _cVal
1914
+ */
1915
+ const pushColor = (_baseStr, _cVal) => {
1916
+ g_workObj[_baseStr][_frame]?.push(_cVal) || (g_workObj[_baseStr][_frame] = [_cVal]);
1917
+ g_workObj[`${_baseStr}Cd`][_frame]?.push(colorCd) || (g_workObj[`${_baseStr}Cd`][_frame] = [colorCd]);
1918
+ };
1919
+
1920
+ /**
1921
+ * 色変化データ(ncolor_data)の格納処理
1922
+ */
1923
+ const executePushColors = () => {
1924
+ const baseHeaders = [];
1925
+ if (_pattern.startsWith(`Arrow`)) {
1926
+ baseHeaders.push(`mk${_header}Color${_pattern.slice('Arrow'.length)}`);
1927
+ allUseTypes.push(`Arrow`);
1928
+
1929
+ // フリーズアロー色の追随設定がある場合、対象を追加
1930
+ if (_pattern === `Arrow`) {
1931
+ g_headerObj.frzScopeFromArrowColors.forEach(type =>
1932
+ baseHeaders.push(`mk${_header}FColor${type}`, `mk${_header}FColor${type}Bar`));
1933
+ if (g_headerObj.frzScopeFromArrowColors.length > 0) {
1934
+ allUseTypes.push(`Frz`);
1935
+ }
1936
+ }
1937
+ } else {
1938
+ baseHeaders.push(`mk${_header}FColor${_pattern}`);
1939
+ allUseTypes.push(`Frz`);
1940
+ }
1941
+ // 色変化情報の格納
1942
+ if (_val.startsWith('g')) {
1943
+ // g付きの場合は矢印グループから対象の矢印番号を検索
1944
+ const groupVal = setIntVal(_val.slice(1));
1945
+ for (let j = 0; j < tkObj.keyNum; j++) {
1946
+ if (g_keyObj[`color${tkObj.keyCtrlPtn}`][j] === groupVal) {
1947
+ baseHeaders.forEach(baseHeader => pushColor(baseHeader, j + addAll));
1948
+ }
1949
+ }
1950
+ } else {
1951
+ baseHeaders.forEach(baseHeader => pushColor(baseHeader, g_workObj.replaceNums[setIntVal(_val)] + addAll));
1952
+ }
1953
+ };
1954
+
1955
+ /**
1956
+ * 従来の色変化データ派生(color_data, acolor_data)の格納処理
1957
+ */
1958
+ const executePushColorsOld = () => {
1959
+
1960
+ if (_val < 30 || _val >= 1000) {
1961
+ const baseHeaders = [`mk${_header}Color`];
1962
+ allUseTypes.push(`Arrow`);
1963
+
1964
+ // フリーズアロー色の追随設定がある場合、対象を追加
1965
+ g_headerObj.frzScopeFromArrowColors.forEach(type =>
1966
+ baseHeaders.push(`mk${_header}FColor${type}`, `mk${_header}FColor${type}Bar`));
1967
+ if (g_headerObj.frzScopeFromArrowColors.length > 0) {
1968
+ allUseTypes.push(`Frz`);
1969
+ }
1970
+
1971
+ // 矢印の色変化 (追随指定時はフリーズアローも色変化)
1972
+ baseHeaders.forEach(baseHeader => {
1973
+ if (_val < 20 || _val >= 1000) {
1974
+ pushColor(baseHeader, g_workObj.replaceNums[_val % 1000] + addAll);
1975
+ } else if (_val >= 20) {
1976
+ const colorNum = _val - 20;
1977
+ for (let j = 0; j < tkObj.keyNum; j++) {
1978
+ if (g_keyObj[`color${tkObj.keyCtrlPtn}`][j] === colorNum) {
1979
+ pushColor(baseHeader, j + addAll);
1980
+ }
1981
+ }
1982
+ }
1983
+ });
1984
+
1985
+ } else {
1986
+ const baseHeader = `mk${_header}FColor`;
1987
+ allUseTypes.push(`Frz`);
1988
+
1989
+ // フリーズアローの色変化
1990
+ const tmpVals = [];
1991
+ if (_val < 50) {
1992
+ tmpVals.push(_val % 30);
1993
+ } else if (_val < 60) {
1994
+ tmpVals.push((_val % 50) * 2, (_val % 50) * 2 + 1);
1995
+ } else {
1996
+ if (_val === 60) {
1997
+ tmpVals.push(...Array(8).keys());
1998
+ } else {
1999
+ tmpVals.push(...[...Array(8).keys()].map(j => j + 10));
2000
+ }
2001
+ }
2002
+ tmpVals.forEach(targetj => {
2003
+
2004
+ // targetj=0,2,4,6,8 ⇒ Arrow, 1,3,5,7,9 ⇒ Bar
2005
+ const ctype = (targetj >= 10 ? `Hit` : `Normal`) + (targetj % 2 === 0 ? `` : `Bar`);
2006
+ const colorPos = Math.ceil((targetj % 10 - 1) / 2);
2007
+
2008
+ g_keyObj[`color${tkObj.keyCtrlPtn}`].forEach((cpattern, k) => {
2009
+ if (colorPos === cpattern) {
2010
+ pushColor(baseHeader + ctype, k + addAll);
2011
+ }
2012
+ });
2013
+ });
2014
+ }
2015
+ };
2016
+ hasVal(_pattern) ? executePushColors() : executePushColorsOld();
2017
+ enabledAll(...allUseTypes);
2018
+ };
2019
+
2020
+ /**
2021
+ * CSSモーション情報の格納
2022
+ * @param {string} _header
2023
+ * @param {number} _frame
2024
+ * @param {number} _val
2025
+ * @param {string} _styleName
2026
+ * @param {string} _styleNameRev
2027
+ * @param {boolean|undefined} _movLock
2028
+ * @param {boolean|undefined} _initManual
2029
+ */
2030
+ const pushCssMotions = (_header, _frame, _val, _styleName, _styleNameRev, _movLock, _initManual) => {
2031
+
2032
+ const camelHeader = toCapitalize(_header);
2033
+ const tkObj = getKeyInfo();
2034
+
2035
+ // 矢印のモーション
2036
+ if (g_workObj[`mk${camelHeader}CssMotion`][_frame] === undefined) {
2037
+ g_workObj[`mk${camelHeader}CssMotion`][_frame] = [];
2038
+ g_workObj[`mk${camelHeader}CssMotionName`][_frame] = [];
2039
+ g_workObj[`mk${camelHeader}MovLock`][_frame] = [];
2040
+ g_workObj[`mk${camelHeader}InitManual`][_frame] = [];
2041
+ }
2042
+ if (_val < 20 || _val >= 1000) {
2043
+ const realVal = g_workObj.replaceNums[_val % 1000];
2044
+ g_workObj[`mk${camelHeader}CssMotion`][_frame].push(realVal);
2045
+ g_workObj[`mk${camelHeader}CssMotionName`][_frame].push(_styleName, _styleNameRev);
2046
+ g_workObj[`mk${camelHeader}MovLock`][_frame].push(_movLock);
2047
+ g_workObj[`mk${camelHeader}InitManual`][_frame].push(_initManual);
2048
+
2049
+ } else {
2050
+ const colorNum = _val - 20;
2051
+ for (let j = 0; j < tkObj.keyNum; j++) {
2052
+ if (g_keyObj[`color${tkObj.keyCtrlPtn}`][j] === colorNum) {
2053
+ g_workObj[`mk${camelHeader}CssMotion`][_frame].push(j);
2054
+ g_workObj[`mk${camelHeader}CssMotionName`][_frame].push(_styleName, _styleNameRev);
2055
+ g_workObj[`mk${camelHeader}MovLock`][_frame].push(_movLock);
2056
+ g_workObj[`mk${camelHeader}InitManual`][_frame].push(_initManual);
2057
+ }
2058
+ }
2059
+ }
2060
+ };
2061
+
2062
+ /**
2063
+ * スクロール変化情報の格納
2064
+ * @param {string} _header
2065
+ * @param {number} _frameArrow
2066
+ * @param {number} _val
2067
+ * @param {number} _frameStep
2068
+ * @param {number} _scrollDir
2069
+ * @param {number} _layerGroup
2070
+ * @param {number} _layerTrans
2071
+ */
2072
+ const pushScrollchs = (_header, _frameArrow, _val, _frameStep, _scrollDir, _layerGroup, _layerTrans) => {
2073
+ const tkObj = getKeyInfo();
2074
+ g_stateObj.layerNum = Math.max(g_stateObj.layerNum, (_layerGroup + 1) * 2);
2075
+
2076
+ const frameArrow = Math.max(_frameArrow, g_scoreObj.frameNum);
2077
+ const frameStep = Math.max(_frameStep, g_scoreObj.frameNum);
2078
+ const pushData = (_pattern, _frame, _val) =>
2079
+ g_workObj[`mkScrollch${_pattern}`][_frame]?.push(_val) || (g_workObj[`mkScrollch${_pattern}`][_frame] = [_val]);
2080
+ const pushScrollData = _j => {
2081
+ pushData(`Arrow`, frameArrow, _j);
2082
+ pushData(`ArrowDir`, frameArrow, _scrollDir);
2083
+ pushData(`ArrowLayerGroup`, frameArrow, _layerGroup);
2084
+ pushData(`ArrowLayerTrans`, frameArrow, _layerTrans);
2085
+ pushData(`Step`, frameStep, _j);
2086
+ pushData(`StepDir`, frameStep, _scrollDir);
2087
+ pushData(`StepLayerGroup`, frameStep, _layerGroup);
2088
+ pushData(`StepLayerTrans`, frameStep, _layerTrans);
2089
+ };
2090
+
2091
+ if (_val < 20 || _val >= 1000) {
2092
+ const realVal = g_workObj.replaceNums[_val % 1000];
2093
+ pushScrollData(realVal);
2094
+
2095
+ } else {
2096
+ const colorNum = _val - 20;
2097
+ for (let j = 0; j < tkObj.keyNum; j++) {
2098
+ if (g_keyObj[`color${tkObj.keyCtrlPtn}`][j] === colorNum) {
2099
+ pushScrollData(j);
2100
+ }
2101
+ }
2102
+ }
2103
+ };
2104
+
2105
+ /**
2106
+ * ホワイトリストに登録されたCSS関数内の数値符号のみを反転する関数
2107
+ * @param {string} cssString
2108
+ * @param {string[]} exceptList 反転対象から除外するCSS関数名のリスト
2109
+ * @returns {string}
2110
+ */
2111
+ const invertSpecificTransforms = (cssString, exceptList = []) => {
2112
+ // 反転対象にしたいCSS関数名を指定(ホワイトリスト)
2113
+ const baseTargetFunctions = [
2114
+ `rotate`, `rotateX`, `rotateY`, `rotateZ`, `rotate3d`,
2115
+ `translate`, `translateX`, `translateY`, `translateZ`, `translate3d`,
2116
+ `skew`, `skewX`, `skewY`
2117
+ ];
2118
+ const targetFunctions = baseTargetFunctions.filter(pattern => !exceptList.includes(pattern));
2119
+ if (targetFunctions.length === 0) {
2120
+ return cssString; // 反転対象がない場合は元の文字列を返す
2121
+ }
2122
+
2123
+ // RegExパターンを作成: /(rotate|translate...)\(([^)]+)\)/g
2124
+ const pattern = new RegExp(`\\b(${targetFunctions.join(`|`)})\\(([^)]+)\\)`, `g`);
2125
+
2126
+ return cssString.replace(pattern, (match, funcName, args) => {
2127
+ // 関数の中身(引数)に含まれる数値だけを符号反転
2128
+ const invertedArgs = args.replace(/(-?\d+(?:\.\d+)?)/g, (numStr) => {
2129
+ return numStr.startsWith(`-`) ? numStr.slice(1) : `-` + numStr;
2130
+ });
2131
+
2132
+ return `${funcName}(${invertedArgs})`;
2133
+ });
2134
+ }
2135
+
2136
+ /**
2137
+ * メイン画面前の初期化処理
2138
+ */
2139
+ const getArrowSettings = () => {
2140
+
2141
+ g_attrObj = {};
2142
+ const tkObj = getKeyInfo();
2143
+ const [keyCtrlPtn, keyNum, posMax, divideCnt] =
2144
+ [tkObj.keyCtrlPtn, tkObj.keyNum, tkObj.posMax, tkObj.divideCnt];
2145
+
2146
+ g_keyCopyLists.simpleDef.forEach(header => updateKeyInfo(header, keyCtrlPtn));
2147
+ g_headerObj.tuning = g_headerObj.creatorNames[g_stateObj.scoreId];
2148
+
2149
+ // 各種初期化
2150
+ // g_workObj.frzArrowInitRtnはフリーズアロー(初期表示)としての利用に限定
2151
+ g_workObj.stepX = [];
2152
+ g_workObj.stepX_df = [];
2153
+ g_workObj.scrollDir = [];
2154
+ g_workObj.scrollDirDefault = [];
2155
+ g_workObj.dividePos = [];
2156
+ g_workObj.scale = g_keyObj.scale;
2157
+ g_workObj.stepRtn = structuredClone(g_keyObj[`stepRtn${keyCtrlPtn}`]);
2158
+ g_workObj.stepHitRtn = structuredClone(g_keyObj[`stepRtn${keyCtrlPtn}`]);
2159
+ g_workObj.arrowRtn = structuredClone(g_keyObj[`stepRtn${keyCtrlPtn}`]);
2160
+ g_workObj.frzArrowInitRtn = structuredClone(g_keyObj[`stepRtn${keyCtrlPtn}`]);
2161
+ g_workObj.keyCtrl = structuredClone(g_keyObj[`keyCtrl${keyCtrlPtn}`]);
2162
+ g_workObj.diffList = [];
2163
+ g_workObj.mainEndTime = 0;
2164
+ g_workObj.currentLifeState = ``;
2165
+ g_workObj.layerTrans = [];
2166
+ g_errorCache['g_customJsObj.mainEnterFrame'] = [];
2167
+
2168
+ const rotateBy = (val, delta) => {
2169
+ // numeric
2170
+ const n = Number(val);
2171
+ if (Number.isFinite(n)) return n + delta;
2172
+
2173
+ // "type:deg"
2174
+ const [type, degStr = `0`] = String(val).split(`:`);
2175
+ const deg = Number(degStr);
2176
+ return Number.isFinite(deg) ? `${type}:${deg + delta}` : val;
2177
+ };
2178
+ const changeStepRtn = (_name, _angle) =>
2179
+ g_workObj[_name] = g_workObj[_name].map(v => rotateBy(v, _angle));
2180
+
2181
+ if (g_stateObj.playWindow === `SideScroll`) {
2182
+ if (g_stateObj.rotateEnabled) {
2183
+ const sign = g_stateObj.playWindowType === C_FLG_REVERSE2 ? -1 : 1;
2184
+ changeStepRtn(`stepRtn`, 90 * sign);
2185
+ changeStepRtn(`stepHitRtn`, 90 * sign);
2186
+ changeStepRtn(`arrowRtn`, 90 * sign);
2187
+ changeStepRtn(`frzArrowInitRtn`, 90 * sign);
2188
+ }
2189
+ const div = g_keyObj[`div${keyCtrlPtn}`];
2190
+ const divMax = g_keyObj[`divMax${keyCtrlPtn}`] ?? posMax;
2191
+ const denom = (Math.max(div, divMax - div) + 1) * g_keyObj.blank;
2192
+ if (Number.isFinite(denom) && denom > 0) {
2193
+ g_workObj.scale *= Math.min(g_sHeight / denom, 1);
2194
+ }
2195
+ }
2196
+
2197
+ // CamoufrageType: FrzArrowの場合のみ、フリーズアロー(初期)の矢印を180度回転
2198
+ if (g_stateObj.rotateEnabled && g_stateObj.camoufrageType === `FrzArrow`) {
2199
+ changeStepRtn(`frzArrowInitRtn`, 180);
2200
+ }
2201
+
2202
+ g_workObj.keyGroupMaps = tkObj.keyGroupMaps;
2203
+ g_workObj.keyGroupList = tkObj.keyGroupList;
2204
+
2205
+ if (g_keyObj[`layerTrans${keyCtrlPtn}`]?.[0]) {
2206
+ g_workObj.layerTrans = structuredClone(g_keyObj[`layerTrans${keyCtrlPtn}`][0]);
2207
+ if (g_stateObj.reverse === C_FLG_ON) {
2208
+ // Reverse時はX軸反転のため、X軸の座標変換では符号を変換しない
2209
+ g_workObj.layerTrans = g_workObj.layerTrans.map(val => invertSpecificTransforms(val, [`translateX`]));
2210
+ }
2211
+ if (g_stateObj.swapping === `Mirror+`) {
2212
+ // Swapping: Mirror+ はY軸反転のため、Y軸の座標変換では符号を変換しない
2213
+ g_workObj.layerTrans = g_workObj.layerTrans.map(val => invertSpecificTransforms(val, [`translateY`]));
2214
+ }
2215
+ }
2216
+
2217
+ const keyCtrlLen = g_workObj.keyCtrl.length;
2218
+ g_workObj.keyCtrlN = [...Array(keyCtrlLen)].map(() => []);
2219
+ g_workObj.keyHitFlg = [...Array(keyCtrlLen)].map(() => []);
2220
+ for (let j = 0; j < g_workObj.keyCtrl.length; j++) {
2221
+ for (let k = 0; k < g_workObj.keyCtrl[j].length; k++) {
2222
+ g_workObj.keyCtrlN[j][k] = g_kCdN[g_workObj.keyCtrl[j][k]];
2223
+ g_workObj.keyHitFlg[j][k] = false;
2224
+ }
2225
+ }
2226
+
2227
+ g_typeLists.arrow.forEach(type => g_workObj[`judg${toCapitalize(type)}Cnt`] = fillArray(keyNum, 1));
2228
+ g_workObj.judgFrzHitCnt = fillArray(keyNum, 1);
2229
+ g_judgObj.lockFlgs = fillArray(keyNum, false);
2230
+
2231
+ // 矢印色管理 (個別・全体)
2232
+ const eachOrAll = [``, `All`];
2233
+ eachOrAll.forEach(type => {
2234
+ [`arrow`, `dummyArrow`].forEach(arrowType =>
2235
+ g_typeLists.arrowColor.forEach(objType => g_workObj[`${arrowType}${objType}Colors${type}`] = []));
2236
+
2237
+ [`frz`, `dummyFrz`].forEach(arrowType =>
2238
+ g_typeLists.frzColor.forEach(frzType => g_workObj[`${arrowType}${frzType}Colors${type}`] = []));
2239
+ });
2240
+
2241
+ // モーション管理
2242
+ g_typeLists.arrow.forEach(type => {
2243
+ g_workObj[`${type}CssMotions`] = fillArray(keyNum, ``);
2244
+ g_workObj[`${type}MovLock`] = fillArray(keyNum, ``);
2245
+ g_workObj[`${type}InitManual`] = fillArray(keyNum, ``);
2246
+ });
2247
+ g_workObj.frzArrowCssMotions = fillArray(keyNum, ``);
2248
+ g_workObj.dummyFrzArrowCssMotions = fillArray(keyNum, ``);
2249
+
2250
+ const scrollDirOptions = g_keyObj[`scrollDir${keyCtrlPtn}`]?.[g_stateObj.scroll] ?? fillArray(keyNum, 1);
2251
+
2252
+ g_stateObj.autoAll = boolToSwitch(g_stateObj.autoPlay === C_FLG_ALL);
2253
+ g_workObj.hitPosition = (g_stateObj.autoAll === C_FLG_ON ? 0 : g_stateObj.hitPosition);
2254
+ changeSetColor();
2255
+
2256
+ for (let j = 0; j < keyNum; j++) {
2257
+
2258
+ const posj = g_keyObj[`pos${keyCtrlPtn}`][j];
2259
+ const colorj = g_keyObj[`color${keyCtrlPtn}`][j];
2260
+ let stdPos = posj - ((posj > divideCnt ? posMax : 0) + divideCnt) / 2;
2261
+
2262
+ if (g_stateObj.swapping === `Mirror+`) {
2263
+ g_workObj.stepX_df[j] = g_keyObj.blank * stdPos + (g_headerObj.playingWidth - C_ARW_WIDTH) / 2;
2264
+ stdPos = -stdPos;
2265
+ }
2266
+
2267
+ g_workObj.stepX[j] = g_keyObj.blank * stdPos + (g_headerObj.playingWidth - C_ARW_WIDTH) / 2;
2268
+ const baseLayer = g_keyObj[`layerGroup${keyCtrlPtn}`]?.[j] || 0;
2269
+ g_workObj.dividePos[j] = baseLayer * 2 + ((posj <= divideCnt ? 0 : 1) + (scrollDirOptions[j] === 1 ? 0 : 1) + (g_stateObj.reverse === C_FLG_OFF ? 0 : 1)) % 2;
2270
+ g_workObj.scrollDir[j] = (posj <= divideCnt ? 1 : -1) * scrollDirOptions[j] * (g_stateObj.reverse === C_FLG_OFF ? 1 : -1);
2271
+
2272
+ // 個別色設定
2273
+ g_workObj.arrowColors[j] = g_headerObj.setColor[colorj];
2274
+ g_workObj.dummyArrowColors[j] = g_headerObj.setDummyColor[colorj];
2275
+ g_workObj.arrowShadowColors[j] = g_headerObj.setShadowColor[colorj] || ``;
2276
+ g_workObj.dummyArrowShadowColors[j] = g_headerObj.setDummyColor[colorj] || ``;
2277
+
2278
+ g_typeLists.frzColor.forEach((frzType, k) => {
2279
+ g_workObj[`frz${frzType}Colors`][j] = g_headerObj.frzColor[colorj][k] || ``;
2280
+ g_workObj[`dummyFrz${frzType}Colors`][j] =
2281
+ frzType.includes(`Shadow`) ? `` : g_headerObj.setDummyColor[colorj] || ``;
2282
+ });
2283
+ g_workObj.frzNormalShadowColors[j] = g_headerObj.frzShadowColor[colorj][0] || ``;
2284
+ g_workObj.frzHitShadowColors[j] = g_headerObj.frzShadowColor[colorj][1] || ``;
2285
+
2286
+ // 全体色設定
2287
+ g_workObj.arrowColorsAll[j] = ``;
2288
+ g_workObj.dummyArrowColorsAll[j] = ``;
2289
+ g_workObj.arrowShadowColorsAll[j] = ``;
2290
+ g_workObj.dummyArrowShadowColorsAll[j] = ``;
2291
+
2292
+ g_typeLists.frzColor.forEach((frzType, k) => {
2293
+ g_workObj[`frz${frzType}ColorsAll`][j] = ``;
2294
+ g_workObj[`dummyFrz${frzType}ColorsAll`][j] = ``;
2295
+ });
2296
+ g_workObj.frzNormalShadowColorsAll[j] = ``;
2297
+ g_workObj.frzHitShadowColorsAll[j] = ``;
2298
+
2299
+ }
2300
+ g_workObj.orgFlatFlg = g_workObj.dividePos.every(v => v % 2 === g_workObj.dividePos[0] % 2);
2301
+ g_stateObj.layerNumDf = Math.max(g_stateObj.layerNumDf, Math.ceil((Math.max(...g_workObj.dividePos) + 1) / 2) * 2);
2302
+
2303
+ // Swapping設定に応じたステップゾーンの入れ替え
2304
+ // Mirror+のみ事前にオリジナルの位置を設定済みのためスキップする
2305
+ if (g_workObj.stepX_df.length === 0) {
2306
+ g_workObj.stepX_df = structuredClone(g_workObj.stepX);
2307
+ }
2308
+ if (g_settings.swappingSubs.includes(g_stateObj.swapping)) {
2309
+
2310
+ // Swappingにおけるグループ単位での入れ替えでは、上下でステップゾーンが分かれている場合は分離してシャッフルする
2311
+ let _style = structuredClone(Object.values(g_workObj.shuffleGroupMap));
2312
+ const _styleTrans = _style.flatMap(arr => {
2313
+ // g_workObj.devidePosの値ごとにグループ化する
2314
+ const groups = {};
2315
+
2316
+ arr.forEach(n => {
2317
+ const div = g_workObj.dividePos[n];
2318
+ if (!groups[div]) groups[div] = [];
2319
+ groups[div].push(n);
2320
+ });
2321
+
2322
+ // groups は {0:[...], 1:[...], 2:[...]} のような形になるので
2323
+ // これを配列に変換して返す
2324
+ return Object.values(groups);
2325
+ });
2326
+ const _styleTransDf = structuredClone(Object.values(_styleTrans));
2327
+
2328
+ if (g_stateObj.swapping === `Mirror` || g_stateObj.swapping === `OuterSwap`) {
2329
+ _styleTrans.map(_group => _group.reverse());
2330
+ }
2331
+ if (g_stateObj.swapping.endsWith(`Swap`)) {
2332
+ // グループの内側だけ入れ替える
2333
+ _styleTrans.forEach((group, i) => {
2334
+ g_settings.swapPattern.forEach(val => {
2335
+ swapGroupNums(_styleTrans, group, i, val);
2336
+ });
2337
+ });
2338
+ }
2339
+ // オリジナルと同一の場合、設定をOFFに戻す
2340
+ if (JSON.stringify(_styleTransDf) === JSON.stringify(_styleTrans)) {
2341
+ g_stateObj.swapping = C_FLG_OFF;
2342
+ g_settings.swappingNum = 0;
2343
+ }
2344
+
2345
+ // 入れ替えた結果に合わせてX座標位置を入れ替える
2346
+ _styleTrans.forEach((_group, _i) => {
2347
+ _group.forEach((_val, _j) => {
2348
+ g_workObj.stepX[_group[_j]] = g_workObj.stepX_df[_styleTransDf[_i][_j]];
2349
+ });
2350
+ });
2351
+ }
2352
+
2353
+ // StepArea(Default, Halfway以外)によるレイヤー移動
2354
+ // ずらした位置に表示するため、レイヤーを倍化して倍化した先に割り当てる
2355
+ const assignLayer = _func => {
2356
+ for (let j = 0; j < keyNum; j++) {
2357
+ g_workObj.dividePos[j] = (_func(j) ? 0 : 1) * g_stateObj.layerNumDf + g_workObj.dividePos[j];
2358
+ }
2359
+ }
2360
+ if (g_stateObj.stepArea === `X-Flower` || (g_stateObj.stepArea.includes(`Mismatched`) && g_workObj.orgFlatFlg)) {
2361
+ assignLayer(j => g_workObj.stepX[j] < (g_headerObj.playingWidth - C_ARW_WIDTH) / 2);
2362
+ } else if (g_stateObj.stepArea === `Alt-Crossing`) {
2363
+ assignLayer(j => Math.round(g_keyObj[`pos${keyCtrlPtn}`][j]) % 2 === 0);
2364
+ }
2365
+ if (g_stateObj.stepArea === `2Step`) {
2366
+ for (let j = 0; j < keyNum; j++) {
2367
+ if (g_workObj.orgFlatFlg && g_workObj.stepX[j] >= (g_headerObj.playingWidth - C_ARW_WIDTH) / 2) {
2368
+ g_workObj.dividePos[j] = Math.floor(g_workObj.dividePos[j] / 2) * 2 + (g_workObj.dividePos[j] + 1) % 2;
2369
+ g_workObj.scrollDir[j] *= -1;
2370
+ }
2371
+ if (g_workObj.dividePos[j] % 2 === (Number(g_stateObj.reverse === C_FLG_ON) + 1) % 2) {
2372
+ g_workObj.dividePos[j] = g_stateObj.layerNumDf + g_workObj.dividePos[j] + Number(g_stateObj.reverse === C_FLG_ON ? 1 : -1);
2373
+ g_workObj.scrollDir[j] *= -1;
2374
+ }
2375
+ }
2376
+ }
2377
+ g_workObj.scrollDirDefault = g_workObj.scrollDir.concat();
2378
+ g_workObj.dividePosDefault = g_workObj.dividePos.concat();
2379
+ g_stateObj.layerNum = g_stateObj.layerNumDf * (g_settings.stepAreaLayers.includes(g_stateObj.stepArea) ? 2 : 1);
2380
+
2381
+ // g_workObjの不要なプロパティを削除
2382
+ if (g_stateObj.dummyId === `` && g_autoPlaysBase.includes(g_stateObj.autoPlay)) {
2383
+ Object.keys(g_workObj).filter(key => key.startsWith(`dummy`) || key.startsWith(`mkDummy`))
2384
+ .forEach(key => delete g_workObj[key]);
2385
+ }
2386
+ const targetColorKeys = [`mkColor`, `mkColorShadow`];
2387
+ const usedColorKeys = [];
2388
+ targetColorKeys.push(...g_typeLists.frzColor.map(type => `mkFColor${type}`));
2389
+ targetColorKeys.forEach(key => {
2390
+ if (g_workObj[key].length === 0) {
2391
+ delete g_workObj[key];
2392
+ delete g_workObj[`${key}Cd`];
2393
+ } else {
2394
+ usedColorKeys.push(key);
2395
+ }
2396
+ });
2397
+ [`Arrow`, `Step`].forEach(type => {
2398
+ [``, `Dir`, `LayerGroup`, `LayerTrans`].forEach(type2 => {
2399
+ if (g_workObj[`mkScrollch${type}${type2}`].length === 0) {
2400
+ delete g_workObj[`mkScrollch${type}${type2}`];
2401
+ }
2402
+ });
2403
+ });
2404
+ [`Arrow`, `Frz`].forEach(type => {
2405
+ if (g_workObj[`mk${type}ColorChangeAll`].length === 0) {
2406
+ delete g_workObj[`mk${type}ColorChangeAll`];
2407
+ }
2408
+ [``, `Name`].forEach(type2 => {
2409
+ if (g_workObj[`mk${type}CssMotion${type2}`].length === 0) {
2410
+ delete g_workObj[`mk${type}CssMotion${type2}`];
2411
+ }
2412
+ });
2413
+ });
2414
+
2415
+ // 初期位置、ライフ設定の初期化
2416
+ Object.keys(g_resultObj).forEach(judgeCnt => g_resultObj[judgeCnt] = 0);
2417
+ g_resultObj.spState = ``;
2418
+ g_resultObj.gaugeTransition = [];
2419
+
2420
+ g_displays.forEach(_disp => {
2421
+ const lowerDisp = _disp.toLowerCase();
2422
+ g_workObj[`${lowerDisp}Disp`] = (g_stateObj[`d_${lowerDisp}`] === C_FLG_OFF ? C_DIS_NONE : C_DIS_INHERIT);
2423
+ });
2424
+ g_workObj.judgmentDisp = g_stateObj.d_judgment !== C_FLG_ON ? C_DIS_NONE : C_DIS_INHERIT;
2425
+ g_workObj.fastslowDisp = g_stateObj.d_judgment === C_FLG_OFF ? C_DIS_NONE : C_DIS_INHERIT;
2426
+
2427
+ g_workObj.lifeVal = Math.floor(g_workObj.lifeInit * 100) / 100;
2428
+ g_workObj.arrowReturnVal = 0;
2429
+ g_gameOverFlg = false;
2430
+ g_finishFlg = true;
2431
+ g_workObj.nonDefaultSc = g_headerObj.keyRetry !== C_KEY_RETRY || g_headerObj.keyTitleBack !== C_KEY_TITLEBACK || g_headerObj.keyPause !== C_KEY_PAUSE;
2432
+ if (g_headerObj.scAreaWidth === 0 && (
2433
+ g_headerObj.keyRetry !== g_headerObj.keyRetryDef2 ||
2434
+ g_headerObj.keyTitleBack !== g_headerObj.keyTitleBackDef2 ||
2435
+ g_headerObj.keyPause !== g_headerObj.keyPauseDef2
2436
+ )) {
2437
+ g_workObj.nonDefaultSc = false;
2438
+ }
2439
+ if (g_diffObj.shortcutX !== 0 || g_diffObj.shortcutY !== 0) {
2440
+ g_workObj.nonDefaultSc = true;
2441
+ }
2442
+
2443
+ g_workObj.backX = (g_workObj.nonDefaultSc && g_headerObj.playingLayout ? g_headerObj.scAreaWidth : 0);
2444
+ g_workObj.playingX = g_headerObj.playingX + g_workObj.backX;
2445
+
2446
+ // FrzReturnの初期化
2447
+ g_workObj.frzReturnFlg = false;
2448
+ g_workObj.frzReturnSeq = g_frzReturnSeqFunc.get(g_stateObj.frzReturnType)();
2449
+ if (g_workObj.frzReturnTimerId) {
2450
+ g_timerHandler.clearTimeout(g_workObj.frzReturnTimerId);
2451
+ g_workObj.frzReturnTimerId = null;
2452
+ }
2453
+
2454
+ // Camoufrageの設定
2455
+ if (g_stateObj.camoufrage !== C_FLG_OFF) {
2456
+
2457
+ const eachOrAll = [``, `All`];
2458
+ const keyNum = g_keyObj[`chara${g_keyObj.currentKey}_${g_keyObj.currentPtn}`].length;
2459
+
2460
+ // 位置変更用の配列を作成
2461
+ const randArray = [...Array(keyNum).keys()].map(_i => _i);
2462
+ let _i = randArray.length;
2463
+ while (_i) {
2464
+ const _j = Math.floor(Math.random() * _i--);
2465
+ [randArray[_i], randArray[_j]] = [randArray[_j], randArray[_i]];
2466
+ }
2467
+
2468
+ // 位置変更用の配列に従い、初期矢印・初期色の位置変更
2469
+ const getSwapArray = (_array) => {
2470
+ const _copiedArray = structuredClone(_array);
2471
+ return _array.map((_val, _i) => _array[_i] = _copiedArray[randArray[_i]]);
2472
+ };
2473
+
2474
+ // 矢印ヒット時に元の矢印がわかるようにするため、あえて g_workObj.stepHitRtn はそのままにする
2475
+ if (g_stateObj.camoufrage.includes(`Step`)) {
2476
+ g_workObj.stepRtn = getSwapArray(g_workObj.stepRtn);
2477
+ }
2478
+ if (g_stateObj.camoufrage.includes(`Arrow`)) {
2479
+ g_workObj.arrowRtn = getSwapArray(g_workObj.arrowRtn);
2480
+ g_workObj.frzArrowInitRtn = getSwapArray(g_workObj.frzArrowInitRtn);
2481
+ }
2482
+ if (g_stateObj.camoufrage.includes(`Color`)) {
2483
+ eachOrAll.forEach(type => {
2484
+ // ダミー矢印は対象外
2485
+ g_workObj[`arrowColors${type}`] = getSwapArray(g_workObj[`arrowColors${type}`]);
2486
+ g_workObj[`arrowShadowColors${type}`] = getSwapArray(g_workObj[`arrowShadowColors${type}`]);
2487
+
2488
+ g_typeLists.frzColor.forEach(frzType => {
2489
+ g_workObj[`frz${frzType}Colors${type}`] = getSwapArray(g_workObj[`frz${frzType}Colors${type}`]);
2490
+ });
2491
+ g_workObj[`frzNormalShadowColors${type}`] = getSwapArray(g_workObj[`frzNormalShadowColors${type}`]);
2492
+ g_workObj[`frzHitShadowColors${type}`] = getSwapArray(g_workObj[`frzHitShadowColors${type}`]);
2493
+ });
2494
+
2495
+ // 位置変更用の配列に従い、個別・全体色変化の位置変更
2496
+ const getSwapList = (_array) => {
2497
+ const _copiedArray = structuredClone(_array);
2498
+ return _array.map((_val, _i) => _array[_i] = randArray[_copiedArray[_i]]);
2499
+ };
2500
+ usedColorKeys.filter(type => g_workObj[type] !== undefined).forEach(type => {
2501
+ for (let j = 0; j < g_workObj[type].length; j++) {
2502
+ if (g_workObj[type][j] === undefined) {
2503
+ continue;
2504
+ }
2505
+ g_workObj[type][j] = getSwapList(g_workObj[type][j]);
2506
+ }
2507
+ });
2508
+ }
2509
+ }
2510
+
2511
+ // Shaking: Drunkでの画面揺れ設定 (X方向、Y方向、移動軸条件、回転軸条件)
2512
+ g_workObj.drunkXFlg = false;
2513
+ g_workObj.drunkYFlg = false;
2514
+ g_workObj.drunkAxisFlg = false;
2515
+ g_workObj.drunkRotateFlg = false;
2516
+
2517
+ // AppearanceFilterの可視範囲設定
2518
+ g_workObj.aprFilterCnt = 0;
2519
+
2520
+ // キー別の移動ロック制御、初期位置のマニュアル可否設定の初期値
2521
+ // 矢印・フリーズアローモーションよりも優先される
2522
+ g_workObj.movLockEnabled = g_keyObj[`movLock${g_keyObj.currentKey}`] === true;
2523
+ g_workObj.initManualEnabled = g_keyObj[`initManual${g_keyObj.currentKey}`] === true;
2524
+
2525
+ if (g_stateObj.dataSaveFlg) {
2526
+ // ローカルストレージへAdjustment, HitPosition, Volume設定を保存
2527
+ // 変更が確定した時点で表示用のキャッシュを解放
2528
+ g_storeSettings.forEach(setting => g_localStorage[setting] = g_stateObj[setting]);
2529
+ viewKeyStorage.cache = new Map();
2530
+ localStorage.setItem(g_localStorageUrl, JSON.stringify(g_localStorage));
2531
+ }
2532
+
2533
+ // リバース、キーコンフィグなどをローカルストレージへ保存(Data Save: ON かつ別キーモードで無い場合)
2534
+ if (g_stateObj.dataSaveFlg && !hasVal(g_keyObj[`transKey${keyCtrlPtn}`])) {
2535
+
2536
+ // 次回キーコンフィグ画面へ戻ったとき、保存済みキーコンフィグ設定が表示されるようにする
2537
+ g_keyObj.prevKey = `Dummy`;
2538
+
2539
+ // ローカルストレージへcolorType設定を保存
2540
+ g_localStorage.colorType = g_colorType;
2541
+
2542
+ let storageObj = g_localKeyStorage;
2543
+ let addKey = ``;
2544
+
2545
+ // リバース、キーコンフィグの保存(キー別)
2546
+ if (g_stateObj.extraKeyFlg) {
2547
+ storageObj = g_localStorage;
2548
+ addKey = g_keyObj.currentKey;
2549
+ }
2550
+ if (g_headerObj.reverseUse) {
2551
+ storageObj[`reverse${addKey}`] = g_stateObj.reverse;
2552
+ }
2553
+ storageObj[`keyCtrl${addKey}`] = setKeyCtrl(g_localKeyStorage, keyNum, keyCtrlPtn);
2554
+ if (g_keyObj.currentPtn !== -1) {
2555
+ storageObj[`keyCtrlPtn${addKey}`] = g_keyObj.currentPtn;
2556
+ g_keyObj[`keyCtrl${keyCtrlPtn}`] = structuredClone(g_keyObj[`keyCtrl${keyCtrlPtn}d`]);
2557
+ }
2558
+
2559
+ // カラーセットの保存(キー別)
2560
+ if (!g_keycons.colorDefTypes.includes(g_colorType)) {
2561
+
2562
+ resetColorType({ _toObj: storageObj, _to: addKey });
2563
+ resetColorType({ _from: g_colorType, _to: g_colorType, _fromObj: g_dfColorObj });
2564
+
2565
+ g_colorType = g_keycons.colorSelf;
2566
+ g_localStorage.colorType = g_keycons.colorSelf;
2567
+ g_keycons.colorTypes = addValtoArray(g_keycons.colorTypes, g_keycons.colorSelf);
2568
+ resetColorType({ _to: g_keycons.colorSelf });
2569
+ }
2570
+
2571
+ g_keycons.groups.forEach(type => {
2572
+ const groupNum = g_keycons[`${type}GroupNum`];
2573
+ storageObj[`${type}${addKey}`] = structuredClone(g_keyObj[`${type}${keyCtrlPtn}_${groupNum}`]);
2574
+ g_keyObj[`${type}${g_keyObj.currentKey}_-1_${groupNum}`] = structuredClone(g_keyObj[`${type}${keyCtrlPtn}_${groupNum}d`]);
2575
+ g_keyObj[`${type}${keyCtrlPtn}_${groupNum}`] = structuredClone(g_keyObj[`${type}${keyCtrlPtn}_${groupNum}d`]);
2576
+
2577
+ // 古いキーデータの削除 (互換用)
2578
+ if (storageObj[`${type}${g_keyObj.currentKey}_-1_-1`] !== undefined) {
2579
+ delete storageObj[`${type}${g_keyObj.currentKey}_-1_-1`];
2580
+ }
2581
+ });
2582
+
2583
+ if (!g_stateObj.extraKeyFlg) {
2584
+ localStorage.setItem(`danonicw-${g_keyObj.currentKey}k`, JSON.stringify(g_localKeyStorage));
2585
+ }
2586
+
2587
+ localStorage.setItem(g_localStorageUrl, JSON.stringify(g_localStorage));
2588
+ g_canLoadDifInfoFlg = true;
2589
+
2590
+ } else {
2591
+ // データ未保存 もしくは 別キーモード時はキーコンフィグ設定を初期化しない
2592
+ g_keyObj.prevKey = g_keyObj.currentKey;
2593
+ g_canLoadDifInfoFlg = false;
2594
+ }
2595
+ };
2596
+
2597
+ /**
2598
+ * キーコンフィグ保存処理
2599
+ * @param {object} _localStorage 保存先のローカルストレージ名
2600
+ * @param {number} _keyNum
2601
+ * @param {string} _keyCtrlPtn
2602
+ * @returns {any[][]}
2603
+ */
2604
+ const setKeyCtrl = (_localStorage, _keyNum, _keyCtrlPtn) => {
2605
+ const localPtn = `${g_keyObj.currentKey}_-1`;
2606
+ const keyCtrl = [...Array(_keyNum)].map(() => []);
2607
+ for (let j = 0; j < _keyNum; j++) {
2608
+ for (let k = 0; k < g_keyObj[`keyCtrl${_keyCtrlPtn}`][j].length; k++) {
2609
+ keyCtrl[j][k] = g_keyObj[`keyCtrl${_keyCtrlPtn}`][j][k];
2610
+ }
2611
+ if (g_keyObj[`keyCtrl${localPtn}`] !== undefined) {
2612
+ if (g_keyObj[`keyCtrl${_keyCtrlPtn}`][j].length < g_keyObj[`keyCtrl${localPtn}`][j].length) {
2613
+ for (let k = g_keyObj[`keyCtrl${_keyCtrlPtn}`][j].length; k < g_keyObj[`keyCtrl${localPtn}`][j].length; k++) {
2614
+ keyCtrl[j][k] = undefined;
2615
+ }
2616
+ }
2617
+ }
2618
+ }
2619
+ return keyCtrl;
2620
+ };
2621
+
2622
+ /*-----------------------------------------------------------*/
2623
+ /* その他の共通設定 */
2624
+ /*-----------------------------------------------------------*/
2625
+
2626
+ // WebAudioAPIでAudio要素風に再生するクラス
2627
+ class AudioPlayer {
2628
+ constructor() {
2629
+ this._context = getSharedAudioContext();
2630
+ this._gain = this._context.createGain();
2631
+ this._gain.connect(this._context.destination);
2632
+ this._startTime = 0;
2633
+ this._scheduledTime = 0;
2634
+ this._fadeinPosition = 0;
2635
+ this._eventListeners = {};
2636
+ this.playbackRate = 1;
2637
+ this._muted = false;
2638
+ this._savedVolume = 1;
2639
+ }
2640
+
2641
+ async init(_arrayBuffer) {
2642
+ this._arrayBuffer = _arrayBuffer;
2643
+ await this._context.decodeAudioData(this._arrayBuffer, _buffer => {
2644
+ this._duration = _buffer.duration;
2645
+ this._buffer = _buffer;
2646
+ });
2647
+ this._eventListeners[`canplaythrough`]?.forEach(_listener => _listener());
2648
+ }
2649
+
2650
+ /**
2651
+ * 再生処理
2652
+ * @param {number} _adjustmentTime
2653
+ * - 実際の再生開始時間は、scheduleLead + _adjustmentTime から開始される
2654
+ * - ただしゲーム内での経過時間計算は _adjustmentTime を基準に行う
2655
+ * - scheduleLead は安定した再生タイミングを確保するための内部マージン
2656
+ */
2657
+ play(_adjustmentTime = 0) {
2658
+ // AudioContextの時計は1回だけ読み、以降はその値を使い回す
2659
+ // - currentTimeはレンダークォンタム単位でしか進まないため、複数回読むと
2660
+ // 予約時刻と論理開始時刻が最大1クォンタム分ずれる
2661
+ const ctxNow = this._context.currentTime;
2662
+
2663
+ this._source = this._context.createBufferSource();
2664
+ this._source.buffer = this._buffer;
2665
+ this._source.playbackRate.value = this.playbackRate;
2666
+ this._source.connect(this._gain);
2667
+
2668
+ // 実際の予約時刻(内部スケジューリング用のマージンを含む)
2669
+ const startAt = ctxNow + g_scheduleLead + _adjustmentTime;
2670
+ this._source.start(startAt, this._fadeinPosition);
2671
+
2672
+ // ゲーム側の論理的開始時刻(g_scheduleLead を含めない)
2673
+ this._startTime = ctxNow + _adjustmentTime;
2674
+
2675
+ // 実際に音が鳴り始めるAudioContext上の時刻(フレーム同期の基準)
2676
+ this._scheduledTime = startAt;
2677
+ }
2678
+
2679
+ pause() {
2680
+ if (this._source) {
2681
+ this._source.stop(0);
2682
+ }
2683
+ }
2684
+
2685
+ close() {
2686
+ if (this._source) {
2687
+ this._source.stop(0);
2688
+ this._source.disconnect(this._gain);
2689
+ this._source = null;
2690
+ }
2691
+ if (this._gain) {
2692
+ this._gain.disconnect();
2693
+ this._gain = null;
2694
+ }
2695
+ }
2696
+
2697
+ get elapsedTime() {
2698
+ return (this._context.currentTime - this._scheduledTime) * this.playbackRate + g_scheduleLead + this._fadeinPosition;
2699
+ }
2700
+
2701
+ /** AudioContextの現在時刻(秒) */
2702
+ get contextTime() {
2703
+ return this._context.currentTime;
2704
+ }
2705
+
2706
+ /** 実際に音が鳴り始めるAudioContext上の時刻(秒) */
2707
+ get scheduledTime() {
2708
+ return this._scheduledTime;
2709
+ }
2710
+
2711
+ /** 音が出力デバイスから実際に出るまでの遅延(秒) */
2712
+ get outputLatency() {
2713
+ return this._context.outputLatency || this._context.baseLatency || 0;
2714
+ }
2715
+
2716
+ /** AudioContextの状態(running / suspended / closed) */
2717
+ get contextState() {
2718
+ return this._context.state;
2719
+ }
2720
+
2721
+ set currentTime(_currentTime) {
2722
+ this._fadeinPosition = _currentTime;
2723
+ }
2724
+
2725
+ get volume() {
2726
+ // ミュート中でも設定されている音量を返す
2727
+ return this._muted ? this._savedVolume : this._gain.gain.value;
2728
+ }
2729
+
2730
+ set volume(_volume) {
2731
+ if (this._muted) {
2732
+ // ミュート中でも音量設定は保存
2733
+ this._savedVolume = _volume;
2734
+ } else {
2735
+ this._gain.gain.value = _volume;
2736
+ }
2737
+ }
2738
+
2739
+ get duration() {
2740
+ return this._duration;
2741
+ }
2742
+
2743
+ get readyState() {
2744
+ if (this._duration) {
2745
+ return 4;
2746
+ } else {
2747
+ return 0;
2748
+ }
2749
+ }
2750
+
2751
+ get muted() {
2752
+ return this._muted;
2753
+ }
2754
+
2755
+ set muted(_muted) {
2756
+ if (this._muted === _muted) {
2757
+ return;
2758
+ }
2759
+ this._muted = _muted;
2760
+
2761
+ if (_muted) {
2762
+ // ミュート時:現在の音量を保存してゲインを0に
2763
+ this._savedVolume = this._gain.gain.value;
2764
+ this._gain.gain.value = 0;
2765
+ } else {
2766
+ // ミュート解除時:保存した音量を復元
2767
+ this._gain.gain.value = this._savedVolume;
2768
+ }
2769
+ }
2770
+
2771
+ addEventListener(_type, _listener) {
2772
+ this._eventListeners[_type]?.push(_listener) || (this._eventListeners[_type] = [_listener]);
2773
+ }
2774
+
2775
+ removeEventListener(_type, _listener) {
2776
+ if (this._eventListeners[_type] === undefined) {
2777
+ return;
2778
+ }
2779
+ this._eventListeners[_type] = this._eventListeners[_type].filter(_element => _element !== _listener);
2780
+ }
2781
+
2782
+ getBuffer() {
2783
+ return this._buffer;
2784
+ }
2785
+
2786
+ setBuffer(_buffer) {
2787
+ this._buffer = _buffer;
2788
+ this._duration = _buffer.duration;
2789
+ this._eventListeners[`canplaythrough`]?.forEach(_listener => _listener());
2790
+ }
2791
+
2792
+ load() { }
2793
+ dispatchEvent() { }
2794
+ }
2795
+
2796
+ // グローバルで1つだけ保持(遅延生成)
2797
+ const getSharedAudioContext = () => {
2798
+ if (!g_sharedAudioContext) {
2799
+ g_sharedAudioContext = new AudioContext();
2800
+ }
2801
+ // タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
2802
+ if (g_sharedAudioContext.state === `suspended`) {
2803
+ g_sharedAudioContext.resume();
2804
+ }
2805
+ return g_sharedAudioContext;
2806
+ };
2807
+
2808
+ /**
2809
+ * AudioContextのウォームアップ
2810
+ * - 生成直後・resume直後のAudioContextは出力デバイスの起動待ちのため、
2811
+ * しばらく currentTime が進まない(環境により数十〜数百ms)
2812
+ * - この状態で音源をスケジュールすると、起動に要した時間がそのまま
2813
+ * 音源と譜面のずれになるため、無音を1回鳴らして時計が動き出すまで待つ
2814
+ * @returns {Promise<void>}
2815
+ */
2816
+ const warmUpAudioContext = async () => {
2817
+ const ctx = getSharedAudioContext();
2818
+ if (ctx.state !== `running`) {
2819
+ await ctx.resume().catch(() => { });
2820
+ }
2821
+ if (ctx.state !== `running`) {
2822
+ return; // ジェスチャー未取得等でresumeできない場合は何もしない
2823
+ }
2824
+
2825
+ // 無音を1サンプルだけ鳴らして出力デバイスを起動させる
2826
+ const source = ctx.createBufferSource();
2827
+ source.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
2828
+ source.connect(ctx.destination);
2829
+ source.start();
2830
+
2831
+ // currentTimeが実際に進み始めるまで待機(最大500ms)
2832
+ const baseTime = ctx.currentTime;
2833
+ const limitTime = performance.now() + 500;
2834
+ while (ctx.currentTime === baseTime && performance.now() < limitTime) {
2835
+ await new Promise(resolve => g_timerHandler.setTimeout(resolve, 10));
2836
+ }
2837
+ };