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.
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2026/09/13
8
- * Revised : 2026/10/20 (v51.4.1)
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
- if (firstArrowFrame - g_limitObj.adjustment < arrivalFrame) {
387
- preblankFrame = arrivalFrame - firstArrowFrame + g_limitObj.adjustment;
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
- return (tmpFirstNum === Infinity ? 0 : tmpFirstNum);
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
- // タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
2843
- if (g_sharedAudioContext.state === `suspended`) {
2844
- g_sharedAudioContext.resume();
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
  };
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2026/09/13
8
- * Revised : 2026/10/12 (v51.4.0)
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