danoniplus 52.1.0 → 52.2.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.
- package/css/danoni_main.css +16 -1
- package/danoni/ringtest.html +98 -0
- package/js/danoni_main.js +14 -3
- package/js/lib/danoni_constants.js +109 -11
- package/js/lib/dataLoader.js +333 -9
- package/js/lib/dosConverter.js +32 -5
- package/js/lib/mainWindow.js +382 -26
- package/js/lib/result.js +3 -1
- package/js/lib/settings.js +327 -21
- package/package.json +1 -1
- package/skin/danoni_skin_default.css +3 -1
- package/skin/danoni_skin_light.css +3 -1
- package/skin/danoni_skin_skyblue.css +3 -1
- package/sound/nosound.mp3 +0 -0
package/js/lib/dataLoader.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Source by tickle
|
|
7
7
|
* Created : 2026/09/13
|
|
8
|
-
* Revised : 2026/
|
|
8
|
+
* Revised : 2026/11/08 (v52.2.0)
|
|
9
9
|
*
|
|
10
10
|
* https://github.com/cwtickle/danoniplus
|
|
11
11
|
*/
|
|
@@ -35,10 +35,13 @@ const loadMusic = async () => {
|
|
|
35
35
|
// 譜面データ読込・変換処理Promise(g_audioに依存しない部分)
|
|
36
36
|
const chartReadyPromise = loadChartFile().then(() => prepareScoreData());
|
|
37
37
|
|
|
38
|
+
// リングノート用の効果音 (失敗しても譜面進行は止めない)
|
|
39
|
+
const ringSoundPromise = loadRingSounds();
|
|
40
|
+
|
|
38
41
|
// 両方の完了を待つ
|
|
39
42
|
let loadSucceeded = true;
|
|
40
43
|
try {
|
|
41
|
-
await Promise.all([audioReadyPromise, chartReadyPromise]);
|
|
44
|
+
await Promise.all([audioReadyPromise, chartReadyPromise, ringSoundPromise]);
|
|
42
45
|
} catch (e) {
|
|
43
46
|
console.warn(`Loading error: ${e}`);
|
|
44
47
|
loadSucceeded = false;
|
|
@@ -279,6 +282,190 @@ const base64ToUint8Array = (_base64Str) => {
|
|
|
279
282
|
return array;
|
|
280
283
|
};
|
|
281
284
|
|
|
285
|
+
const g_ringSoundBuffers = new Map(); // 音番号 -> AudioBuffer (現在の作品)
|
|
286
|
+
const g_ringSoundCache = new Map(); // URL -> AudioBuffer (リトライ・再プレイ用)
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 帯域ノイズ型の効果音を合成 (_def.bands を持つデフォルト音用)
|
|
290
|
+
* 白色ノイズを帯域ごとの共鳴フィルタ(バイクアッド)に通し、帯域ごとの2段減衰エンベロープを掛けて足し合わせる
|
|
291
|
+
* @param {object} _def
|
|
292
|
+
* @param {number} _rate サンプルレート
|
|
293
|
+
* @returns {Float32Array}
|
|
294
|
+
*/
|
|
295
|
+
const synthRingBands = (_def, _rate) => {
|
|
296
|
+
const len = Math.max(Math.floor(_rate * _def.dur), 1);
|
|
297
|
+
const out = new Float32Array(len);
|
|
298
|
+
const attack = Math.max(Math.floor(_rate * (_def.attack ?? 0.008)), 1);
|
|
299
|
+
|
|
300
|
+
// 種付き乱数 (毎回同じ音になる)
|
|
301
|
+
let seed = 12345;
|
|
302
|
+
const rnd = () => {
|
|
303
|
+
seed = (seed + 0x6D2B79F5) >>> 0;
|
|
304
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
305
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
306
|
+
return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * 2 - 1;
|
|
307
|
+
};
|
|
308
|
+
const noise = new Float32Array(len).map(() => rnd());
|
|
309
|
+
|
|
310
|
+
_def.bands.forEach(b => {
|
|
311
|
+
const w0 = 2 * Math.PI * b.f / _rate;
|
|
312
|
+
const alpha = Math.sin(w0) / (2 * b.q);
|
|
313
|
+
const a0 = 1 + alpha;
|
|
314
|
+
const b0 = alpha / a0, b2 = -alpha / a0;
|
|
315
|
+
const a1 = -2 * Math.cos(w0) / a0, a2 = (1 - alpha) / a0;
|
|
316
|
+
const norm = 1 / Math.max(alpha / a0 * Math.sqrt(b.q) * 4, 1e-6);
|
|
317
|
+
const mix2 = b.mix2 ?? 0, tau2 = b.tau2 ?? b.tau;
|
|
318
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
319
|
+
for (let i = 0; i < len; i++) {
|
|
320
|
+
const x = noise[i];
|
|
321
|
+
const y = b0 * x + b2 * x2 - a1 * y1 - a2 * y2;
|
|
322
|
+
x2 = x1; x1 = x; y2 = y1; y1 = y;
|
|
323
|
+
const t = i / _rate;
|
|
324
|
+
const env = (1 - mix2) * Math.exp(-t / b.tau) + mix2 * Math.exp(-t / tau2);
|
|
325
|
+
out[i] += y * norm * b.gain * env * Math.min(1, (i + 1) / attack);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// 高域カット (2次ローパスを lpPasses 段)
|
|
330
|
+
if (_def.lowpass) {
|
|
331
|
+
const w0 = 2 * Math.PI * _def.lowpass / _rate;
|
|
332
|
+
const alpha = Math.sin(w0) / (2 * Math.SQRT1_2);
|
|
333
|
+
const a0 = 1 + alpha, cs = Math.cos(w0);
|
|
334
|
+
const b0 = (1 - cs) / 2 / a0, b1 = (1 - cs) / a0, a1 = -2 * cs / a0, a2 = (1 - alpha) / a0;
|
|
335
|
+
for (let pass = 0; pass < (_def.lpPasses ?? 2); pass++) {
|
|
336
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
337
|
+
for (let i = 0; i < len; i++) {
|
|
338
|
+
const x = out[i];
|
|
339
|
+
const y = b0 * x + b1 * x1 + b0 * x2 - a1 * y1 - a2 * y2;
|
|
340
|
+
x2 = x1; x1 = x; y2 = y1; y1 = y; out[i] = y;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 末尾2msをフェードアウト (クリック防止) + ピーク正規化
|
|
346
|
+
const fade = Math.floor(_rate * 0.002);
|
|
347
|
+
for (let i = 0; i < fade; i++) {
|
|
348
|
+
out[len - 1 - i] *= i / fade;
|
|
349
|
+
}
|
|
350
|
+
const peak = out.reduce((max, v) => Math.max(max, Math.abs(v)), 0) || 1;
|
|
351
|
+
return out.map(v => v * (_def.level ?? 0.5) / peak);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* デフォルト音の生成 (外部ファイル不要)
|
|
356
|
+
* @param {{wave: string, freq: number, dur: number}} _def
|
|
357
|
+
* @returns {AudioBuffer}
|
|
358
|
+
*/
|
|
359
|
+
const makeDefaultRingSound = _def => {
|
|
360
|
+
const ctx = getSharedAudioContext();
|
|
361
|
+
const rate = ctx.sampleRate;
|
|
362
|
+
const len = Math.max(Math.floor(rate * _def.dur), 1);
|
|
363
|
+
const buffer = ctx.createBuffer(1, len, rate);
|
|
364
|
+
const data = buffer.getChannelData(0);
|
|
365
|
+
|
|
366
|
+
// 帯域ノイズ型 (bands を持つ定義)
|
|
367
|
+
if (_def.bands) {
|
|
368
|
+
data.set(synthRingBands(_def, rate));
|
|
369
|
+
return buffer;
|
|
370
|
+
}
|
|
371
|
+
const waves = {
|
|
372
|
+
sine: p => Math.sin(2 * Math.PI * p),
|
|
373
|
+
triangle: p => 2 * Math.abs(2 * (p - Math.floor(p + 0.5))) - 1,
|
|
374
|
+
square: p => (p % 1 < 0.5 ? 1 : -1) * 0.6,
|
|
375
|
+
};
|
|
376
|
+
const wave = waves[_def.wave] ?? waves.sine;
|
|
377
|
+
const noiseMix = _def.noise ?? 0;
|
|
378
|
+
const decay = _def.decay ?? 6;
|
|
379
|
+
const freqEnd = _def.freqEnd ?? _def.freq;
|
|
380
|
+
const sweepRate = rate * (_def.sweep ?? 0.03);
|
|
381
|
+
let phase = 0;
|
|
382
|
+
let prevNoise = 0;
|
|
383
|
+
for (let i = 0; i < len; i++) {
|
|
384
|
+
// ピッチは freq から freqEnd へ指数的に収束 (freqEnd 未指定なら一定)
|
|
385
|
+
const freq = freqEnd + (_def.freq - freqEnd) * Math.exp(-i / sweepRate);
|
|
386
|
+
phase += freq / rate;
|
|
387
|
+
|
|
388
|
+
// ノイズは1次差分で高域寄りにする (破裂音の「パ」成分)
|
|
389
|
+
const n = Math.random() * 2 - 1;
|
|
390
|
+
const highNoise = (n - prevNoise) * 0.5;
|
|
391
|
+
prevNoise = n;
|
|
392
|
+
|
|
393
|
+
// 立ち上がり2ms + 指数減衰 (クリックノイズ防止)
|
|
394
|
+
const env = Math.min(1, i / (rate * 0.002)) * Math.exp(-decay * i / len);
|
|
395
|
+
data[i] = 0.5 * env * ((1 - noiseMix) * wave(phase) + noiseMix * highNoise);
|
|
396
|
+
}
|
|
397
|
+
return buffer;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* リングノート用効果音の実際のパスを取得
|
|
402
|
+
* @param {string} _soundUrl
|
|
403
|
+
* @returns {string}
|
|
404
|
+
*/
|
|
405
|
+
const getRingSoundUrl = (_soundUrl = ``) => {
|
|
406
|
+
const [soundFile, soundPath] = getFilePath(_soundUrl, C_DIR_SOUND);
|
|
407
|
+
return `${soundPath}${soundFile}`;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* リングノート用効果音の読込
|
|
412
|
+
* - |ringSound=a.mp3,b.mp3| の並び順が音番号 (1始まり)。0は無音
|
|
413
|
+
* - 読込失敗時は該当の音のみ無音とし、例外は投げない
|
|
414
|
+
* @returns {Promise<void>}
|
|
415
|
+
*/
|
|
416
|
+
const loadRingSounds = async () => {
|
|
417
|
+
g_ringSoundBuffers.clear();
|
|
418
|
+
// デフォルト音 (音番号1~)
|
|
419
|
+
g_ringDefaultSounds.forEach((def, i) => {
|
|
420
|
+
const key = `default:${i + 1}`;
|
|
421
|
+
if (!g_ringSoundCache.has(key)) {
|
|
422
|
+
g_ringSoundCache.set(key, makeDefaultRingSound(def));
|
|
423
|
+
}
|
|
424
|
+
g_ringSoundBuffers.set(i + 1, g_ringSoundCache.get(key));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// |ringSound=| の指定でデフォルト音を上書き (空欄はデフォルトのまま)
|
|
428
|
+
const names = g_headerObj.ringSound.split(`,`).map(val => trimStr(val));
|
|
429
|
+
if (names.every(name => !hasVal(name))) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (g_isFile) {
|
|
433
|
+
console.warn(`ringSound: file scheme is not supported (default sounds are used)`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await Promise.all(names.map(async (name, i) => {
|
|
437
|
+
if (!hasVal(name)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const url = getRingSoundUrl(name);
|
|
441
|
+
try {
|
|
442
|
+
let buffer = g_ringSoundCache.get(url);
|
|
443
|
+
if (buffer === undefined) {
|
|
444
|
+
const controller = new AbortController();
|
|
445
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
446
|
+
let arrayBuffer;
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new Error(`HTTP ${response.status}`);
|
|
452
|
+
}
|
|
453
|
+
arrayBuffer = await response.arrayBuffer();
|
|
454
|
+
} finally {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
buffer = await getSharedAudioContext().decodeAudioData(arrayBuffer);
|
|
459
|
+
g_ringSoundCache.set(url, buffer);
|
|
460
|
+
}
|
|
461
|
+
g_ringSoundBuffers.set(i + 1, buffer);
|
|
462
|
+
} catch (e) {
|
|
463
|
+
g_ringSoundBuffers.delete(i + 1);
|
|
464
|
+
console.warn(`ringSound load error (${name}): ${e}`);
|
|
465
|
+
}
|
|
466
|
+
}));
|
|
467
|
+
};
|
|
468
|
+
|
|
282
469
|
/**
|
|
283
470
|
* 音源の再生準備完了を待つ
|
|
284
471
|
* - g_audio が Audio要素の場合は canplaythrough/error イベントの発火を待つ
|
|
@@ -383,8 +570,14 @@ const prepareScoreData = (_retryFlg = false) => {
|
|
|
383
570
|
// フレーム・曲開始位置調整
|
|
384
571
|
let preblankFrame = 0;
|
|
385
572
|
if (g_scoreObj.frameNum === 0) {
|
|
386
|
-
|
|
387
|
-
|
|
573
|
+
// 矢印・リングノートそれぞれについて、出現が開始前になる場合の補完フレーム数を求め、大きい方を採用
|
|
574
|
+
const arrowLead = firstArrowFrame - g_limitObj.adjustment < arrivalFrame
|
|
575
|
+
? arrivalFrame - firstArrowFrame + g_limitObj.adjustment : 0;
|
|
576
|
+
const ringLead = getRingLeadFrame(g_scoreObj);
|
|
577
|
+
if (arrowLead > 0 || ringLead > 0) {
|
|
578
|
+
preblankFrame = Math.max(arrowLead, ringLead);
|
|
579
|
+
console.log(firstArrowFrame, arrivalFrame, g_limitObj.adjustment, arrowLead, ringLead, preblankFrame)
|
|
580
|
+
console.log(arrowLead)
|
|
388
581
|
|
|
389
582
|
// 譜面データの再読み込み
|
|
390
583
|
const noteExistObj = {
|
|
@@ -447,6 +640,8 @@ const prepareScoreData = (_retryFlg = false) => {
|
|
|
447
640
|
g_allFrz = 0;
|
|
448
641
|
g_scoreObj.arrowData.forEach(data => g_allArrow += data.length);
|
|
449
642
|
g_scoreObj.frzData.forEach(data => g_allFrz += Math.floor(data.length / 2) * 2);
|
|
643
|
+
// リングノート数を矢印数に加算 (ライフ回復量・パーフェクト判定等の母数に含める)
|
|
644
|
+
g_allArrow += Math.floor((g_scoreObj.ringData?.length ?? 0) / g_ringObj.cols);
|
|
450
645
|
|
|
451
646
|
// ライフ回復・ダメージ量の計算
|
|
452
647
|
// フリーズ始点でも通常判定させる場合は総矢印数を水増しする
|
|
@@ -978,6 +1173,42 @@ const scoreConvert = (_dosObj, _scoreId, _preblankFrame, _dummyNo = ``,
|
|
|
978
1173
|
return [];
|
|
979
1174
|
};
|
|
980
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* リングノートデータの分解 (1行1ノート)
|
|
1178
|
+
* 入力: 到達フレーム, X, Y, [音番号], [半径], [色], [出現~到達フレーム数]
|
|
1179
|
+
* 出力: g_ringObj.cols 個で1セットの単純配列
|
|
1180
|
+
* [到達フレーム, X, Y, 音番号, 半径, 色, 出現~到達フレーム数(0:未指定)]
|
|
1181
|
+
* @param {string} _scoreNo
|
|
1182
|
+
* @returns {any[]}
|
|
1183
|
+
*/
|
|
1184
|
+
const setRingData = _scoreNo => {
|
|
1185
|
+
const dosRingData = getRefData(`ring`, `${_scoreNo}_data`);
|
|
1186
|
+
if (!hasVal(dosRingData)) {
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1189
|
+
const ringData = [];
|
|
1190
|
+
const getNum = (_val, _default, _condition = true, _default2 = _default) =>
|
|
1191
|
+
hasVal(_val) && !isNaN(parseFloat(_val))
|
|
1192
|
+
? (_condition ? parseFloat(_val) : _default2)
|
|
1193
|
+
: _default;
|
|
1194
|
+
|
|
1195
|
+
splitLF(dosRingData).filter(data => hasVal(data)).forEach(tmpData => {
|
|
1196
|
+
const t = tmpData.split(`,`).map(val => trimStr(val));
|
|
1197
|
+
if ([0, 1, 2].some(k => isNaN(parseFloat(t[k])))) {
|
|
1198
|
+
console.warn(`ring_data: 到達フレーム,X,Y は必須です: ${tmpData}`);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
ringData.push([
|
|
1202
|
+
calcFrame(t[0]), parseFloat(t[1]), parseFloat(t[2]),
|
|
1203
|
+
getNum(t[3], g_ringObj.sound),
|
|
1204
|
+
getNum(t[4], g_ringObj.radius, t[4] >= g_ringObj.minRadius, g_ringObj.minRadius),
|
|
1205
|
+
hasVal(t[5]) ? t[5] : (g_headerObj.baseBrightFlg ? g_ringObj.colorLight : g_ringObj.color),
|
|
1206
|
+
getNum(t[6], 0),
|
|
1207
|
+
]);
|
|
1208
|
+
});
|
|
1209
|
+
return ringData.sort((_a, _b) => _a[0] - _b[0]).flat();
|
|
1210
|
+
};
|
|
1211
|
+
|
|
981
1212
|
/**
|
|
982
1213
|
* 譜面データに別の関連名が含まれていた場合、関連名の変数を返す
|
|
983
1214
|
* 例) |backA2_data=back_data| -> back_dataで定義された値を使用
|
|
@@ -1181,6 +1412,10 @@ const scoreConvert = (_dosObj, _scoreId, _preblankFrame, _dummyNo = ``,
|
|
|
1181
1412
|
obj.ncolorDummyData = setColor2Data(`ncolor`, _dummyNo);
|
|
1182
1413
|
}
|
|
1183
1414
|
|
|
1415
|
+
// リングノート・リング速度変化の分解 (速度変化は既存のsetSpeedDataを流用)
|
|
1416
|
+
obj.ringSpeedData = setSpeedData(`ringspeed`, scoreIdHeader);
|
|
1417
|
+
obj.ringData = setRingData(scoreIdHeader);
|
|
1418
|
+
|
|
1184
1419
|
if (_scoreAnalyzeFlg) {
|
|
1185
1420
|
return obj;
|
|
1186
1421
|
}
|
|
@@ -1310,6 +1545,12 @@ const getLastFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj
|
|
|
1310
1545
|
}
|
|
1311
1546
|
});
|
|
1312
1547
|
}
|
|
1548
|
+
|
|
1549
|
+
// リングノートの最終到達フレーム (到達フレーム昇順・6要素1セット)
|
|
1550
|
+
const ringData = _dataObj.ringData ?? [];
|
|
1551
|
+
if (ringData.length >= g_ringObj.cols) {
|
|
1552
|
+
tmpLastNum = Math.max(tmpLastNum, ringData[ringData.length - g_ringObj.cols]);
|
|
1553
|
+
}
|
|
1313
1554
|
return tmpLastNum;
|
|
1314
1555
|
};
|
|
1315
1556
|
|
|
@@ -1317,9 +1558,10 @@ const getLastFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj
|
|
|
1317
1558
|
* 最初の矢印フレームの取得
|
|
1318
1559
|
* @param {object} _dataObj
|
|
1319
1560
|
* @param {string} [_keyCtrlPtn]
|
|
1561
|
+
* @param {boolean} [_includeRing=false] リングノートも含めるか
|
|
1320
1562
|
* @returns {number}
|
|
1321
1563
|
*/
|
|
1322
|
-
const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}
|
|
1564
|
+
const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`, _includeRing = false) => {
|
|
1323
1565
|
|
|
1324
1566
|
let tmpFirstNum = Infinity;
|
|
1325
1567
|
const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
|
|
@@ -1338,7 +1580,16 @@ const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_
|
|
|
1338
1580
|
}
|
|
1339
1581
|
});
|
|
1340
1582
|
}
|
|
1341
|
-
|
|
1583
|
+
if (_includeRing) {
|
|
1584
|
+
const ringData = _dataObj.ringData ?? [];
|
|
1585
|
+
for (let k = 0; k + g_ringObj.cols <= ringData.length; k += g_ringObj.cols) {
|
|
1586
|
+
const frame = ringData[k];
|
|
1587
|
+
if (Number.isFinite(frame)) {
|
|
1588
|
+
tmpFirstNum = Math.min(tmpFirstNum, frame);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return (tmpFirstNum === Infinity ? g_headerObj.blankFrame : tmpFirstNum);
|
|
1342
1593
|
};
|
|
1343
1594
|
|
|
1344
1595
|
/**
|
|
@@ -1652,6 +1903,27 @@ const pushArrows = (_dataObj, _speedOnFrame, _firstArrivalFrame) => {
|
|
|
1652
1903
|
calcNotes(j, _dataObj.dummyFrzData[j], `dummyFrz`, true);
|
|
1653
1904
|
}
|
|
1654
1905
|
|
|
1906
|
+
// リングノートの出現フレーム別格納 (矢印とは独立のため速度計算は不要)
|
|
1907
|
+
// - 開始フレーム前に到達済みのものは除外
|
|
1908
|
+
// - 出現が開始フレーム前のもの(フェードイン時)は、開始フレームから途中の大きさで出現させる
|
|
1909
|
+
g_workObj.mkRing = [];
|
|
1910
|
+
g_workObj.ringArrivals = []; // 通し番号 -> 到達フレーム (到達順)
|
|
1911
|
+
const ringData = _dataObj.ringData ?? [];
|
|
1912
|
+
const cols = g_ringObj.cols;
|
|
1913
|
+
for (let k = 0; k < ringData.length; k += cols) {
|
|
1914
|
+
const [arrival, x, y, sound, radius, color, frames] = ringData.slice(k, k + cols);
|
|
1915
|
+
if (arrival < g_scoreObj.frameNum) {
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
const total = getRingDuration(_dataObj.ringSpeedData, arrival, frames);
|
|
1919
|
+
const appear = Math.max(arrival - total, g_scoreObj.frameNum);
|
|
1920
|
+
(g_workObj.mkRing[appear] ??= []).push({
|
|
1921
|
+
x, y, radius, color, sound, total, arrivalFrame: arrival - appear,
|
|
1922
|
+
arrival, seq: g_workObj.ringArrivals.length,
|
|
1923
|
+
});
|
|
1924
|
+
g_workObj.ringArrivals.push(arrival);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1655
1927
|
/**
|
|
1656
1928
|
* 色変化・モーションデータ・スクロール反転データのタイミング更新
|
|
1657
1929
|
* - この関数を使用する場合、配列グループの先頭2つが「フレーム数、矢印番号」となっていないと動作しない
|
|
@@ -1865,6 +2137,50 @@ const getFrzLength = (_speedOnFrame, _startFrame, _endFrame) => {
|
|
|
1865
2137
|
return frzLength;
|
|
1866
2138
|
};
|
|
1867
2139
|
|
|
2140
|
+
/**
|
|
2141
|
+
* 指定フレーム時点で有効なリング速度の取得
|
|
2142
|
+
* @param {number[]} _ringSpeedData [フレーム数, 速度, ...] (フレーム昇順)
|
|
2143
|
+
* @param {number} _frame
|
|
2144
|
+
* @returns {number}
|
|
2145
|
+
*/
|
|
2146
|
+
const getRingSpeed = (_ringSpeedData, _frame) => {
|
|
2147
|
+
let speed = 1;
|
|
2148
|
+
for (let k = 0; k < (_ringSpeedData?.length ?? 0); k += 2) {
|
|
2149
|
+
if (_ringSpeedData[k] > _frame) {
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
speed = _ringSpeedData[k + 1];
|
|
2153
|
+
}
|
|
2154
|
+
return speed > 0 ? speed : 1;
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* リングの出現~到達フレーム数
|
|
2159
|
+
* - 個別指定(_frames > 0)があればそれを優先、なければ到達フレーム時点のリング速度に反比例
|
|
2160
|
+
* - リング速度オプション(g_stateObj.ringSpeed)はどちらにも掛かる
|
|
2161
|
+
*/
|
|
2162
|
+
const getRingDuration = (_ringSpeedData, _arrivalFrame, _frames = 0) => {
|
|
2163
|
+
const base = _frames > 0 ? _frames : g_ringObj.baseFrame / getRingSpeed(_ringSpeedData, _arrivalFrame);
|
|
2164
|
+
return Math.max(Math.round(base / g_stateObj.ringSpeed), 1);
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* リングを0フレーム以降に出現させるために必要な補完フレーム数
|
|
2169
|
+
* (矢印のプレブランク計算と同様、Adjustment分の余裕を含める)
|
|
2170
|
+
*/
|
|
2171
|
+
const getRingLeadFrame = _dataObj => {
|
|
2172
|
+
const ringData = _dataObj.ringData ?? [];
|
|
2173
|
+
let lead = 0;
|
|
2174
|
+
const cols = g_ringObj.cols;
|
|
2175
|
+
for (let k = 0; k < ringData.length; k += cols) {
|
|
2176
|
+
const duration = getRingDuration(_dataObj.ringSpeedData, ringData[k], ringData[k + cols - 1]);
|
|
2177
|
+
if (ringData[k] - g_limitObj.adjustment < duration) {
|
|
2178
|
+
lead = Math.max(lead, duration - ringData[k] + g_limitObj.adjustment);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return lead;
|
|
2182
|
+
};
|
|
2183
|
+
|
|
1868
2184
|
/**
|
|
1869
2185
|
* キーパターン(デフォルト)に対応する矢印番号を格納
|
|
1870
2186
|
* - 色変化、矢印・フリーズアローモーション、スクロール変化で
|
|
@@ -2835,13 +3151,21 @@ class AudioPlayer {
|
|
|
2835
3151
|
}
|
|
2836
3152
|
|
|
2837
3153
|
// グローバルで1つだけ保持(遅延生成)
|
|
3154
|
+
let g_sharedAudioResumePending = false;
|
|
3155
|
+
let g_sharedAudioResumeNextAt = 0;
|
|
2838
3156
|
const getSharedAudioContext = () => {
|
|
2839
3157
|
if (!g_sharedAudioContext) {
|
|
2840
3158
|
g_sharedAudioContext = new AudioContext();
|
|
2841
3159
|
}
|
|
2842
|
-
//
|
|
2843
|
-
|
|
2844
|
-
|
|
3160
|
+
// 復帰待ちのPromiseは重複させず、失敗時の再試行は1秒以上空ける。
|
|
3161
|
+
const now = performance.now();
|
|
3162
|
+
if ((g_sharedAudioContext.state === `suspended` || g_sharedAudioContext.state === `interrupted`) &&
|
|
3163
|
+
!g_sharedAudioResumePending && now >= g_sharedAudioResumeNextAt) {
|
|
3164
|
+
g_sharedAudioResumePending = true;
|
|
3165
|
+
g_sharedAudioResumeNextAt = now + 1000;
|
|
3166
|
+
g_sharedAudioContext.resume().catch(() => { }).finally(() => {
|
|
3167
|
+
g_sharedAudioResumePending = false;
|
|
3168
|
+
});
|
|
2845
3169
|
}
|
|
2846
3170
|
return g_sharedAudioContext;
|
|
2847
3171
|
};
|
package/js/lib/dosConverter.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Source by tickle
|
|
7
7
|
* Created : 2026/09/13
|
|
8
|
-
* Revised : 2026/
|
|
8
|
+
* Revised : 2026/11/08 (v52.2.0)
|
|
9
9
|
*
|
|
10
10
|
* https://github.com/cwtickle/danoniplus
|
|
11
11
|
*/
|
|
@@ -502,6 +502,8 @@ const loadLocalStorage = (_musicId = ``) => {
|
|
|
502
502
|
checkLocalParam(`appearance`);
|
|
503
503
|
checkLocalParam(`opacity`, C_TYP_NUMBER, g_settings.opacitys.length - 1);
|
|
504
504
|
checkLocalParam(`hitPosition`, C_TYP_FLOAT, g_settings.hitPositionNum);
|
|
505
|
+
checkLocalParam(`ringSpeed`, C_TYP_FLOAT, Math.max(g_settings.ringSpeeds.indexOf(g_stateObj.ringSpeed), 0));
|
|
506
|
+
checkLocalParam(`ringGuide`);
|
|
505
507
|
|
|
506
508
|
// ハイスコア取得準備
|
|
507
509
|
if (g_localStorage.highscores === undefined) {
|
|
@@ -645,12 +647,12 @@ const getFullMusicUrl = (_musicUrl = ``) => {
|
|
|
645
647
|
const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
|
|
646
648
|
const lastFrame = getLastFrame(_scoreObj, _keyCtrlPtn) + 1;
|
|
647
649
|
const startFrame = getStartFrame(lastFrame, 0, _scoreId);
|
|
648
|
-
const firstArrowFrame = getFirstArrowFrame(_scoreObj, _keyCtrlPtn);
|
|
650
|
+
const firstArrowFrame = getFirstArrowFrame(_scoreObj, _keyCtrlPtn, true);
|
|
649
651
|
const playingFrame = lastFrame - firstArrowFrame;
|
|
650
652
|
const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
|
|
651
653
|
|
|
652
654
|
// 譜面密度グラフ用のデータ作成
|
|
653
|
-
const noteCnt = { arrow: [], frz: [] };
|
|
655
|
+
const noteCnt = { arrow: [], frz: [], ring: Math.floor((_scoreObj.ringData?.length ?? 0) / g_ringObj.cols) };
|
|
654
656
|
const densityData = fillArray(g_limitObj.densityDivision);
|
|
655
657
|
let allData = 0;
|
|
656
658
|
|
|
@@ -675,6 +677,20 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
|
|
|
675
677
|
}));
|
|
676
678
|
fullData = fullData.concat(..._scoreObj.arrowData[j], ...tmpFrzData);
|
|
677
679
|
}
|
|
680
|
+
if (_scoreObj.ringData !== undefined) {
|
|
681
|
+
const ringData = _scoreObj.ringData.filter((data, k) => k % g_ringObj.cols === 0);
|
|
682
|
+
ringData.forEach(note => {
|
|
683
|
+
if (isNaN(parseFloat(note))) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const point = Math.floor((note - firstArrowFrame) / playingFrame * g_limitObj.densityDivision);
|
|
687
|
+
if (point >= 0) {
|
|
688
|
+
densityData[point]++;
|
|
689
|
+
allData++;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
fullData = fullData.concat(ringData ?? []);
|
|
693
|
+
}
|
|
678
694
|
|
|
679
695
|
fullData = fullData.filter(val => !isNaN(parseFloat(val))).sort((a, b) => a - b);
|
|
680
696
|
let pushCnt = 1;
|
|
@@ -725,6 +741,7 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
|
|
|
725
741
|
|
|
726
742
|
g_detailObj.arrowCnt[_scoreId] = noteCnt.arrow.concat();
|
|
727
743
|
g_detailObj.frzCnt[_scoreId] = noteCnt.frz.map((val, k) => _scoreObj.frzData[k].length % 2 === 0 ? val : val - 0.5);
|
|
744
|
+
g_detailObj.ringCnt[_scoreId] = noteCnt.ring;
|
|
728
745
|
g_detailObj.startFrame[_scoreId] = startFrame;
|
|
729
746
|
g_detailObj.playingFrame[_scoreId] = playingFrame;
|
|
730
747
|
g_detailObj.playingFrameWithBlank[_scoreId] = lastFrame - startFrame;
|
|
@@ -740,11 +757,13 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
|
|
|
740
757
|
timeMargin: 35,
|
|
741
758
|
mmWidthBase: (g_sWidth - 500) / 2 + 290,
|
|
742
759
|
mmMarginY: 2,
|
|
760
|
+
// リングノート用の帯 (リングがある譜面のみ。余白4px + 帯28px)
|
|
761
|
+
ringBandWidth: noteCnt.ring > 0 ? 32 : 0,
|
|
743
762
|
get laneWidth() {
|
|
744
|
-
return Math.min((this.mmWidthBase - this.timeMargin) / keyNum, 40);
|
|
763
|
+
return Math.min((this.mmWidthBase - this.timeMargin - this.ringBandWidth) / keyNum, 40);
|
|
745
764
|
},
|
|
746
765
|
get logicalWidth() {
|
|
747
|
-
const logicalWidth = this.timeMargin + (this.laneWidth * keyNum);
|
|
766
|
+
const logicalWidth = this.timeMargin + (this.laneWidth * keyNum) + this.ringBandWidth;
|
|
748
767
|
return Math.ceil(logicalWidth * g_dpr) / g_dpr;
|
|
749
768
|
}
|
|
750
769
|
},
|
|
@@ -1370,6 +1389,11 @@ const headerConvert = _dosObj => {
|
|
|
1370
1389
|
// カラーコードのゼロパディング有無設定
|
|
1371
1390
|
obj.colorCdPaddingUse = setBoolVal(_dosObj.colorCdPaddingUse);
|
|
1372
1391
|
|
|
1392
|
+
// 明背景の場合はガイドリング色を明背景用に切り替える
|
|
1393
|
+
if (obj.baseBrightFlg) {
|
|
1394
|
+
g_ringObj.guideColor = g_ringObj.guideColorLight;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1373
1397
|
// 最大ライフ
|
|
1374
1398
|
obj.maxLifeVal = setVal(_dosObj.maxLifeVal, C_VAL_MAXLIFE, C_TYP_FLOAT);
|
|
1375
1399
|
if (obj.maxLifeVal <= 0) {
|
|
@@ -1521,6 +1545,9 @@ const headerConvert = _dosObj => {
|
|
|
1521
1545
|
makeWarningWindow(g_msgInfoObj.E_0031);
|
|
1522
1546
|
}
|
|
1523
1547
|
|
|
1548
|
+
// リング音の設定
|
|
1549
|
+
obj.ringSound = _dosObj.ringSound ?? ``;
|
|
1550
|
+
|
|
1524
1551
|
// ハッシュタグ
|
|
1525
1552
|
obj.hashTag = _dosObj.hashTag ?? ``;
|
|
1526
1553
|
|