danoniplus 43.1.1 → 43.2.1

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.
Files changed (2) hide show
  1. package/js/danoni_main.js +241 -82
  2. package/package.json +1 -1
package/js/danoni_main.js CHANGED
@@ -4,12 +4,12 @@
4
4
  *
5
5
  * Source by tickle
6
6
  * Created : 2018/10/08
7
- * Revised : 2025/09/21
7
+ * Revised : 2025/12/30
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 43.1.1`;
12
- const g_revisedDate = `2025/09/21`;
11
+ const g_version = `Ver 43.2.1`;
12
+ const g_revisedDate = `2025/12/30`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -2350,13 +2350,28 @@ class AudioPlayer {
2350
2350
  this._eventListeners[`canplaythrough`]?.forEach(_listener => _listener());
2351
2351
  }
2352
2352
 
2353
+ /**
2354
+ * 再生処理
2355
+ * @param {number} _adjustmentTime
2356
+ * - 実際の再生開始時間は、scheduleLead + _adjustmentTime から開始される
2357
+ * - ただしゲーム内での経過時間計算は _adjustmentTime を基準に行う
2358
+ * - scheduleLead は安定した再生タイミングを確保するための内部マージン
2359
+ */
2353
2360
  play(_adjustmentTime = 0) {
2354
2361
  this._source = this._context.createBufferSource();
2355
2362
  this._source.buffer = this._buffer;
2356
2363
  this._source.playbackRate.value = this.playbackRate;
2357
2364
  this._source.connect(this._gain);
2358
- this._startTime = this._context.currentTime;
2359
- this._source.start(this._context.currentTime + _adjustmentTime, this._fadeinPosition);
2365
+
2366
+ // 内部スケジューリング用のマージン時間(100ms)
2367
+ const scheduleLead = 0.1;
2368
+
2369
+ // 実際の予約時刻(内部スケジューリング用のマージンを含む)
2370
+ const startAt = this._context.currentTime + scheduleLead + _adjustmentTime;
2371
+ this._source.start(startAt, this._fadeinPosition);
2372
+
2373
+ // ゲーム側の論理的開始時刻(scheduleLead を含めない)
2374
+ this._startTime = this._context.currentTime + _adjustmentTime;
2360
2375
  }
2361
2376
 
2362
2377
  pause() {
@@ -2697,7 +2712,7 @@ const initialControl = async () => {
2697
2712
 
2698
2713
  // 未使用のg_keyObjプロパティを削除
2699
2714
  const keyProp = g_keyCopyLists.simple.concat(g_keyCopyLists.multiple, `keyCtrl`, `keyName`, `minWidth`, `ptchara`);
2700
- const delKeyPropList = [`ptchara7`, `keyTransPattern`, `dfPtnNum`, `minKeyCtrlNum`, `minPatterns`];
2715
+ const delKeyPropList = [`ptchara7`, `dfPtnNum`, `minKeyCtrlNum`, `minPatterns`];
2701
2716
  Object.keys(g_keyObj).forEach(key => {
2702
2717
  const type = keyProp.find(prop => key.startsWith(prop)) || ``;
2703
2718
  if (type !== ``) {
@@ -4592,6 +4607,23 @@ const getKeyUnitName = _key => unEscapeHtml(escapeHtml(g_keyObj[`keyName${_key}`
4592
4607
  const getTransKeyName = (_spaceFlg = false) => hasVal(g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`])
4593
4608
  ? (_spaceFlg ? ` ` : ``) + `(${g_keyObj[`transKey${g_keyObj.currentKey}_${g_keyObj.currentPtn}`]})` : ``;
4594
4609
 
4610
+ /**
4611
+ * ハイスコア定義を行う際のストレージキー名の取得
4612
+ * @param {string} _key
4613
+ * @param {string} _transName
4614
+ * @param {string} _assistFlg
4615
+ * @param {string} _mirrorName
4616
+ * @param {string} _scoreId
4617
+ * @returns {string}
4618
+ */
4619
+ const getStorageKeyName = (_key, _transName, _assistFlg, _mirrorName, _scoreId) => {
4620
+ let scoreName = `${_key}${_transName}${getStgDetailName('k-')}${g_headerObj.difLabels[_scoreId]}${_assistFlg}${_mirrorName}`;
4621
+ if (g_headerObj.makerView) {
4622
+ scoreName += `-${g_headerObj.creatorNames[_scoreId]}`;
4623
+ }
4624
+ return scoreName;
4625
+ };
4626
+
4595
4627
  /**
4596
4628
  * KeyBoardEvent.code の値をCW Edition用のキーコードに変換
4597
4629
  * 簡略指定ができるように、以下の記述を許容
@@ -5525,7 +5557,7 @@ const pauseBGM = () => {
5525
5557
  }
5526
5558
  [`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`].forEach(id => {
5527
5559
  if (g_stateObj[id]) {
5528
- clearInterval(g_stateObj[id]);
5560
+ clearTimeout(g_stateObj[id]);
5529
5561
  g_stateObj[id] = null;
5530
5562
  }
5531
5563
  });
@@ -5549,95 +5581,177 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
5549
5581
  const musicEnd = g_headerObj.musicEnds?.[currentIdx] ?? 0;
5550
5582
  const isTitle = () => g_currentPage === `title` && _currentLoopNum === g_settings.musicLoopNum;
5551
5583
 
5584
+ /**
5585
+ * 汎用フェード処理
5586
+ * @param {number} startVolume - 開始音量 (0〜1)
5587
+ * @param {number} endVolume - 終了音量 (0〜1)
5588
+ * @param {number} step - 1ステップの増減量
5589
+ * @param {function} onEnd - フェード完了時の処理
5590
+ * @param {function} isValid - フェード継続条件(true: 継続, false: 中断)
5591
+ * @returns {number} timeoutId
5592
+ */
5593
+ const fadeVolume = (startVolume, endVolume, step, onEnd, isValid) => {
5594
+
5595
+ // 開始時点で終了音量とイコールの場合は終了
5596
+ if (startVolume === endVolume || step === 0) {
5597
+ g_audio.volume = endVolume;
5598
+ onEnd(true);
5599
+ return null;
5600
+ }
5601
+
5602
+ let volume = startVolume;
5603
+ g_audio.volume = startVolume;
5604
+
5605
+ const stepFunc = () => {
5606
+ // 継続条件チェック
5607
+ if (!isValid()) {
5608
+ onEnd(false); // 中断
5609
+ return;
5610
+ }
5611
+
5612
+ // 終了判定
5613
+ const reached =
5614
+ (startVolume < endVolume && volume >= endVolume) ||
5615
+ (startVolume > endVolume && volume <= endVolume);
5616
+
5617
+ if (reached) {
5618
+ g_audio.volume = endVolume;
5619
+ onEnd(true); // 正常終了
5620
+ return;
5621
+ }
5622
+
5623
+ // 音量更新
5624
+ volume += step;
5625
+ g_audio.volume = Math.min(Math.max(volume, 0), 1);
5626
+
5627
+ // 次のステップへ
5628
+ setTimeout(stepFunc, FADE_INTERVAL_MS);
5629
+ };
5630
+
5631
+ return setTimeout(stepFunc, FADE_INTERVAL_MS);
5632
+ };
5633
+
5634
+ /**
5635
+ * 汎用ポーリング(監視)処理
5636
+ * @param {function} check - 条件チェック関数(true なら終了)
5637
+ * @param {function} onEnd - 終了時の処理
5638
+ * @param {function} isValid - 継続条件(true: 継続, false: 中断)
5639
+ * @returns {number} timeoutId
5640
+ */
5641
+ const poll = (check, onEnd, isValid) => {
5642
+ const step = () => {
5643
+ // 継続条件チェック
5644
+ if (!isValid()) {
5645
+ onEnd(false); // 中断
5646
+ return;
5647
+ }
5648
+
5649
+ // 条件成立
5650
+ if (check()) {
5651
+ onEnd(true); // 正常終了
5652
+ return;
5653
+ }
5654
+
5655
+ // 次のチェックへ
5656
+ setTimeout(step, FADE_INTERVAL_MS);
5657
+ };
5658
+
5659
+ return setTimeout(step, FADE_INTERVAL_MS);
5660
+ };
5661
+
5552
5662
  /**
5553
5663
  * BGMのフェードアウトとシーク
5554
5664
  */
5555
5665
  const fadeOutAndSeek = () => {
5556
- let volume = g_audio.volume;
5557
- const fadeInterval = setInterval(() => {
5558
- if (volume > FADE_STEP && isTitle()) {
5559
- volume -= FADE_STEP;
5560
- g_audio.volume = Math.max(volume, 0);
5561
- } else {
5562
- clearInterval(fadeInterval);
5666
+ const start = g_audio.volume;
5667
+ const end = 0;
5668
+
5669
+ g_stateObj.bgmFadeOut = fadeVolume(
5670
+ start,
5671
+ end,
5672
+ -FADE_STEP,
5673
+ /* onEnd */
5674
+ (finished) => {
5563
5675
  g_stateObj.bgmFadeOut = null;
5676
+
5677
+ if (!finished) return; // 中断された
5678
+
5564
5679
  g_audio.pause();
5565
5680
  g_audio.currentTime = musicStart;
5566
5681
 
5567
- // フェードイン開始
5568
5682
  if (isTitle()) {
5569
5683
  setTimeout(() => {
5570
5684
  fadeIn();
5571
- if (encodeFlg) {
5572
- // base64エンコード時はtimeupdateイベントが発火しないため、
5573
- // setIntervalで時間を取得する
5574
- repeatBGM();
5575
- }
5685
+ if (encodeFlg) repeatBGM();
5576
5686
  }, FADE_DELAY_MS);
5577
5687
  } else {
5578
5688
  pauseBGM();
5579
5689
  }
5580
- }
5581
- }, FADE_INTERVAL_MS);
5582
- g_stateObj.bgmFadeOut = fadeInterval;
5690
+ },
5691
+ /* isValid */
5692
+ () =>
5693
+ isTitle() &&
5694
+ g_stateObj.bgmFadeOut !== null
5695
+ );
5583
5696
  };
5584
5697
 
5585
5698
  /**
5586
5699
  * BGMのフェードイン
5587
5700
  */
5588
5701
  const fadeIn = () => {
5589
- if (!(g_audio instanceof AudioPlayer) && !g_audio.src) {
5590
- return;
5591
- }
5592
- let volume = 0;
5702
+ if (!(g_audio instanceof AudioPlayer) && !g_audio.src) return;
5703
+
5704
+ const start = 0;
5705
+ const end = g_stateObj.bgmVolume / 100;
5706
+
5707
+ g_audio.volume = 0;
5593
5708
  g_audio.play();
5594
- const fadeInterval = setInterval(() => {
5595
- if (volume < g_stateObj.bgmVolume / 100 && isTitle()) {
5596
- volume += FADE_STEP;
5597
- g_audio.volume = Math.min(volume, 1);
5598
- } else {
5599
- clearInterval(fadeInterval);
5600
- g_stateObj.bgmFadeIn = null;
5601
- }
5602
5709
 
5603
- // フェードイン中に楽曲が変更された場合は停止
5604
- if (currentIdx !== g_headerObj.musicIdxList[g_settings.musicIdxNum]) {
5605
- pauseBGM();
5606
- clearInterval(fadeInterval);
5710
+ g_stateObj.bgmFadeIn = fadeVolume(
5711
+ start,
5712
+ end,
5713
+ FADE_STEP,
5714
+ /* onEnd */
5715
+ () => {
5607
5716
  g_stateObj.bgmFadeIn = null;
5608
- }
5609
- }, FADE_INTERVAL_MS);
5610
- g_stateObj.bgmFadeIn = fadeInterval;
5717
+ },
5718
+ /* isValid */
5719
+ () =>
5720
+ isTitle() &&
5721
+ g_stateObj.bgmFadeIn !== null &&
5722
+ currentIdx === g_headerObj.musicIdxList[g_settings.musicIdxNum]
5723
+ );
5611
5724
  };
5612
5725
 
5613
5726
  /**
5614
- * BGMのループ処理
5727
+ * BGMのループ処理 (base64エンコード時用)
5728
+ * - base64エンコード時はtimeupdateイベントが発火しないため、setIntervalで時間を取得する
5615
5729
  */
5616
5730
  const repeatBGM = () => {
5617
- if (encodeFlg) {
5618
- // base64エンコード時はtimeupdateイベントが発火しないため、setIntervalで時間を取得する
5619
- const repeatCheck = setInterval((num = g_settings.musicIdxNum) => {
5731
+ const numAtStart = g_settings.musicIdxNum;
5732
+
5733
+ g_stateObj.bgmLooped = poll(
5734
+ /* check */
5735
+ () => {
5620
5736
  try {
5621
- if (((g_audio.elapsedTime >= musicEnd) ||
5622
- num !== g_settings.musicIdxNum) && g_stateObj.bgmLooped !== null) {
5623
- clearInterval(repeatCheck);
5624
- g_stateObj.bgmLooped = null;
5625
- fadeOutAndSeek();
5626
- }
5627
- } catch (e) {
5628
- clearInterval(repeatCheck);
5629
- g_stateObj.bgmLooped = null;
5737
+ return (
5738
+ g_audio.elapsedTime >= musicEnd ||
5739
+ numAtStart !== g_settings.musicIdxNum
5740
+ );
5741
+ } catch {
5742
+ return true; // エラー時は終了扱い
5630
5743
  }
5631
- }, FADE_INTERVAL_MS);
5632
- g_stateObj.bgmLooped = repeatCheck;
5633
-
5634
- } else {
5635
- g_stateObj.bgmTimeupdateEvtId = g_handler.addListener(g_audio, "timeupdate", () => {
5636
- if (g_audio.currentTime >= musicEnd) {
5744
+ },
5745
+ /* onEnd */
5746
+ (finished) => {
5747
+ g_stateObj.bgmLooped = null;
5748
+ if (finished) {
5637
5749
  fadeOutAndSeek();
5638
5750
  }
5639
- });
5640
- }
5751
+ },
5752
+ /* isValid */
5753
+ () => g_stateObj.bgmLooped !== null
5754
+ );
5641
5755
  };
5642
5756
 
5643
5757
  if (encodeFlg) {
@@ -5659,9 +5773,10 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
5659
5773
  g_audio = tmpAudio;
5660
5774
  }
5661
5775
  g_audio.volume = g_stateObj.bgmVolume / 100;
5662
- if (g_currentPage === `title`) {
5776
+ if (g_currentPage === `title` && musicEnd > 0) {
5663
5777
  g_audio.currentTime = musicStart;
5664
5778
  g_audio.play();
5779
+ repeatBGM();
5665
5780
  }
5666
5781
  } catch (e) {
5667
5782
  // 音源の読み込みに失敗した場合、エラーを表示
@@ -5681,9 +5796,14 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
5681
5796
  g_audio.currentTime = musicStart;
5682
5797
  g_audio.play();
5683
5798
  }, { once: true });
5684
- }
5685
- if (musicEnd > 0) {
5686
- repeatBGM();
5799
+
5800
+ if (musicEnd > 0) {
5801
+ g_stateObj.bgmTimeupdateEvtId = g_handler.addListener(g_audio, "timeupdate", () => {
5802
+ if (g_audio.currentTime >= musicEnd) {
5803
+ fadeOutAndSeek();
5804
+ }
5805
+ });
5806
+ }
5687
5807
  }
5688
5808
  };
5689
5809
 
@@ -6861,9 +6981,24 @@ const makeHighScore = _scoreId => {
6861
6981
  const assistFlg = (g_autoPlaysBase.includes(g_stateObj.autoPlay) ? `` : `-${getStgDetailName(g_stateObj.autoPlay)}${getStgDetailName('less')}`);
6862
6982
  const mirrorName = (g_stateObj.shuffle === C_FLG_OFF ? `` : `-${g_stateObj.shuffle}`);
6863
6983
  const transKeyName = getTransKeyName();
6864
- let scoreName = `${g_headerObj.keyLabels[_scoreId]}${transKeyName}${getStgDetailName('k-')}${g_headerObj.difLabels[_scoreId]}${assistFlg}${mirrorName}`;
6865
- if (g_headerObj.makerView) {
6866
- scoreName += `-${g_headerObj.creatorNames[_scoreId]}`;
6984
+ let scoreName = getStorageKeyName(g_headerObj.keyLabels[_scoreId], transKeyName, assistFlg, mirrorName, _scoreId);
6985
+
6986
+ if (!hasVal(g_localStorage.highscores?.[scoreName])) {
6987
+
6988
+ // 古いキー定義の情報を検索
6989
+ const relatedKeys = Object.entries(g_keyObj.keyTransPattern)
6990
+ .filter(([key, value]) => value === g_headerObj.keyLabels[_scoreId])
6991
+ .map(([key]) => key);
6992
+
6993
+ // 古いキー定義のハイスコアがいる場合は、現行キー定義として表示
6994
+ for (const legacyKey of relatedKeys) {
6995
+ let tmpScoreName = getStorageKeyName(legacyKey, transKeyName, assistFlg, mirrorName, _scoreId);
6996
+ const src = g_localStorage.highscores?.[tmpScoreName];
6997
+ if (hasVal(src)) {
6998
+ g_localStorage.highscores[scoreName] = structuredClone(src);
6999
+ break;
7000
+ }
7001
+ }
6867
7002
  }
6868
7003
 
6869
7004
  const createScoreLabel = (_id, _text, { xPos = 0, yPos = 0, dx = 0, w = 150, h = 17, colorName = _id, align = C_ALIGN_LEFT, overflow = `visible` } = {}) =>
@@ -7792,26 +7927,28 @@ const gaugeFormat = (_mode, _border, _rcv, _dmg, _init, _lifeValFlg) => {
7792
7927
  const initVal = g_headerObj.maxLifeVal * _init / 100;
7793
7928
  const borderVal = g_headerObj.maxLifeVal * _border / 100;
7794
7929
 
7795
- // 整形用にライフ初期値を整数、回復・ダメージ量を小数第1位で丸める
7930
+ // 整形用にライフ初期値を整数、回復・ダメージ量を小数第2位に丸める
7796
7931
  const init = Math.round(initVal);
7797
7932
  const borderText = (_mode === C_LFE_BORDER && _border !== 0 ? Math.round(borderVal) : `-`);
7798
- const toFixed2 = _val => Math.round(_val * 100) / 100;
7933
+ const round2 = _val => Math.round(_val * 100) / 100;
7799
7934
 
7800
- let rcvText = toFixed2(_rcv), dmgText = toFixed2(_dmg);
7935
+ let rcvText = round2(_rcv), dmgText = round2(_dmg);
7801
7936
  let realRcv = _rcv, realDmg = _dmg;
7802
7937
  const allCnt = sumData(g_detailObj.arrowCnt[g_stateObj.scoreId]) +
7803
7938
  (g_headerObj.frzStartjdgUse ? 2 : 1) * sumData(g_detailObj.frzCnt[g_stateObj.scoreId]);
7804
7939
 
7940
+ // ゲージ設定が矢印数依存の場合、実際の値に変換して表示する
7941
+ // 表示上、計算した値は小数第二位までの表示とする(それ以外はそのまま)
7805
7942
  if (_lifeValFlg === C_FLG_ON) {
7806
7943
  rcvText = ``, dmgText = ``;
7807
7944
  if (allCnt > 0) {
7808
7945
  realRcv = Math.min(calcLifeVal(_rcv, allCnt), g_headerObj.maxLifeVal);
7809
7946
  realDmg = Math.min(calcLifeVal(_dmg, allCnt), g_headerObj.maxLifeVal);
7810
- rcvText = `${toFixed2(realRcv)}<br>`;
7811
- dmgText = `${toFixed2(realDmg)}<br>`;
7947
+ rcvText = `${realRcv.toFixed(2)}<br>`;
7948
+ dmgText = `${realDmg.toFixed(2)}<br>`;
7812
7949
  }
7813
- rcvText += `<span class="settings_lifeVal">(${toFixed2(_rcv)})</span>`;
7814
- dmgText += `<span class="settings_lifeVal">(${toFixed2(_dmg)})</span>`;
7950
+ rcvText += `<span class="settings_lifeVal">(${round2(_rcv)})</span>`;
7951
+ dmgText += `<span class="settings_lifeVal">(${round2(_dmg)})</span>`;
7815
7952
  }
7816
7953
 
7817
7954
  // 達成率(Accuracy)・許容ミス数の計算
@@ -10403,7 +10540,7 @@ const calcLifeVals = _allArrows => {
10403
10540
  * @param {number} _val
10404
10541
  * @param {number} _allArrows
10405
10542
  */
10406
- const calcLifeVal = (_val, _allArrows) => Math.round(_val * g_headerObj.maxLifeVal * 100 / _allArrows) / 100;
10543
+ const calcLifeVal = (_val, _allArrows) => _val * g_headerObj.maxLifeVal / _allArrows;
10407
10544
 
10408
10545
  /**
10409
10546
  * 最終フレーム数の取得
@@ -14004,10 +14141,8 @@ const resultInit = () => {
14004
14141
  // ハイスコア差分計算
14005
14142
  const assistFlg = (g_autoPlaysBase.includes(g_stateObj.autoPlay) ? `` : `-${g_stateObj.autoPlay}less`);
14006
14143
  const mirrorName = (g_stateObj.shuffle.indexOf(`Mirror`) !== -1 ? `-${g_stateObj.shuffle}` : ``);
14007
- let scoreName = `${g_headerObj.keyLabels[g_stateObj.scoreId]}${transKeyName}${getStgDetailName('k-')}${g_headerObj.difLabels[g_stateObj.scoreId]}${assistFlg}${mirrorName}`;
14008
- if (g_headerObj.makerView) {
14009
- scoreName += `-${g_headerObj.creatorNames[g_stateObj.scoreId]}`;
14010
- }
14144
+ let scoreName = getStorageKeyName(g_headerObj.keyLabels[g_stateObj.scoreId], transKeyName, assistFlg, mirrorName, g_stateObj.scoreId);
14145
+
14011
14146
  const highscoreDfObj = {
14012
14147
  ii: 0, shakin: 0, matari: 0, shobon: 0, uwan: 0,
14013
14148
  kita: 0, iknai: 0,
@@ -14038,6 +14173,30 @@ const resultInit = () => {
14038
14173
 
14039
14174
  if (highscoreCondition) {
14040
14175
 
14176
+ // 古いキー定義の情報を検索
14177
+ const relatedKeys = Object.entries(g_keyObj.keyTransPattern)
14178
+ .filter(([key, value]) => value === g_headerObj.keyLabels[g_stateObj.scoreId])
14179
+ .map(([key]) => key);
14180
+
14181
+ // 古いキー定義のスコアデータを現行キー定義に移行
14182
+ for (const legacyKey of relatedKeys) {
14183
+ let tmpScoreName = getStorageKeyName(
14184
+ legacyKey, transKeyName, assistFlg, mirrorName, g_stateObj.scoreId
14185
+ );
14186
+ const src = g_localStorage.highscores?.[tmpScoreName];
14187
+ if (!hasVal(src)) {
14188
+ continue;
14189
+ }
14190
+
14191
+ // 現行キー定義にスコアデータが存在しない場合、移行元のスコアデータをコピー
14192
+ if (!hasVal(g_localStorage.highscores?.[scoreName])) {
14193
+ g_localStorage.highscores[scoreName] = structuredClone(src);
14194
+ }
14195
+
14196
+ // 古いキー定義は見つかった最初の1件のみ移行し、以降は削除
14197
+ delete g_localStorage.highscores[tmpScoreName];
14198
+ }
14199
+
14041
14200
  Object.keys(jdgScoreObj).filter(judge => judge !== ``)
14042
14201
  .forEach(judge => highscoreDfObj[judge] = g_resultObj[judge] -
14043
14202
  (scoreName in g_localStorage.highscores ? g_localStorage.highscores[scoreName][judge] : 0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "43.1.1",
3
+ "version": "43.2.1",
4
4
  "description": "Dancing☆Onigiri (CW Edition) - Web-based Rhythm Game",
5
5
  "main": "./js/danoni_main.js",
6
6
  "scripts": {