danoniplus 49.1.1 → 49.3.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.
Files changed (2) hide show
  1. package/js/danoni_main.js +302 -163
  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 : 2026/07/04
7
+ * Revised : 2026/07/12
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 49.1.1`;
12
- const g_revisedDate = `2026/07/04`;
11
+ const g_version = `Ver 49.3.0`;
12
+ const g_revisedDate = `2026/07/12`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -177,6 +177,10 @@ let g_maxScore = 1000000;
177
177
  let g_gameOverFlg = false;
178
178
  let g_finishFlg = true;
179
179
 
180
+ // 音源のAudioContext管理、リトライ中フラグ
181
+ let g_sharedAudioContext = null;
182
+ let g_retryInProgress = false;
183
+
180
184
  /** 共通オブジェクト */
181
185
  const g_loadObj = {};
182
186
  const g_rootObj = {};
@@ -2060,10 +2064,9 @@ const makeBgCanvas = (_ctx, { w = g_sWidth, h = g_sHeight } = {}) => {
2060
2064
  * - divオブジェクト(ボタンなど)はdivRoot配下で管理しているため、子要素のみを全削除している。
2061
2065
  * - dicRoot自体を削除しないよう注意すること。
2062
2066
  * - 再描画時に共通で表示する箇所はここで指定している。
2063
- * @param {boolean} [_redrawFlg=false] 画面横幅を再定義し、Canvas背景を再描画するかどうか
2064
2067
  * @param {string} [_customDisplayName=''] 画面名(メイン画面: 'Main', それ以外: 空)
2065
2068
  */
2066
- const clearWindow = (_redrawFlg = false, _customDisplayName = ``) => {
2069
+ const clearWindow = (_customDisplayName = ``) => {
2067
2070
  closeDisplayPreview();
2068
2071
  resetKeyControl();
2069
2072
  resetTransform();
@@ -2110,37 +2113,33 @@ const clearWindow = (_redrawFlg = false, _customDisplayName = ``) => {
2110
2113
  getLayerWithClear(`layer2`);
2111
2114
  }
2112
2115
 
2113
- if (_redrawFlg) {
2114
- // 画面背景を指定 (background-color)
2115
- $id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
2116
- layer0.width = g_sWidth + diffX;
2117
- if (!g_headerObj[`customBack${_customDisplayName}Use`]) {
2118
- makeBgCanvas(l0ctx, { w: g_sWidth + diffX });
2119
- }
2116
+ // 画面背景を指定 (background-color)
2117
+ $id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
2118
+ layer0.width = g_sWidth + diffX;
2119
+ if (!g_headerObj[`customBack${_customDisplayName}Use`]) {
2120
+ makeBgCanvas(l0ctx, { w: g_sWidth + diffX });
2120
2121
  }
2121
2122
  }
2122
2123
 
2123
2124
  // 背景を再描画
2124
- if (_redrawFlg) {
2125
- g_btnAddFunc = {};
2126
- g_btnDeleteFlg = {};
2127
- g_cxtAddFunc = {};
2128
- g_cxtDeleteFlg = {};
2125
+ g_btnAddFunc = {};
2126
+ g_btnDeleteFlg = {};
2127
+ g_cxtAddFunc = {};
2128
+ g_cxtDeleteFlg = {};
2129
2129
 
2130
- if (document.getElementById(`layer0`) === null ||
2131
- (!g_headerObj[`customBack${_customDisplayName}Use`] && !g_headerObj.defaultSkinFlg)) {
2130
+ if (document.getElementById(`layer0`) === null ||
2131
+ (!g_headerObj[`customBack${_customDisplayName}Use`] && !g_headerObj.defaultSkinFlg)) {
2132
2132
 
2133
- $id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
2134
- createEmptySprite(divRoot, `divBack`, { w: g_sWidth + diffX });
2135
- }
2133
+ $id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
2134
+ createEmptySprite(divRoot, `divBack`, { w: g_sWidth + diffX });
2135
+ }
2136
2136
 
2137
- // CSSスタイルの初期化
2138
- Object.keys(g_cssBkProperties).forEach(prop =>
2139
- document.documentElement.style.setProperty(prop, g_cssBkProperties[prop]));
2137
+ // CSSスタイルの初期化
2138
+ Object.keys(g_cssBkProperties).forEach(prop =>
2139
+ document.documentElement.style.setProperty(prop, g_cssBkProperties[prop]));
2140
2140
 
2141
- Object.keys(g_headerObj).filter(val => val.startsWith(`--`) && hasVal(g_headerObj[val])).forEach(prop =>
2142
- document.documentElement.style.setProperty(prop, getCssCustomProperty(prop, g_headerObj[prop])));
2143
- }
2141
+ Object.keys(g_headerObj).filter(val => val.startsWith(`--`) && hasVal(g_headerObj[val])).forEach(prop =>
2142
+ document.documentElement.style.setProperty(prop, getCssCustomProperty(prop, g_headerObj[prop])));
2144
2143
  };
2145
2144
 
2146
2145
  /**
@@ -2653,7 +2652,6 @@ class AudioPlayer {
2653
2652
  }
2654
2653
 
2655
2654
  // グローバルで1つだけ保持(遅延生成)
2656
- let g_sharedAudioContext = null;
2657
2655
  const getSharedAudioContext = () => {
2658
2656
  if (!g_sharedAudioContext) {
2659
2657
  g_sharedAudioContext = new AudioContext();
@@ -3254,7 +3252,6 @@ const loadChartFile = async (_scoreId = g_stateObj.scoreId) => {
3254
3252
 
3255
3253
  await loadScript2(`${filename}?${Date.now()}`, false, charset);
3256
3254
  if (typeof externalDosInit === C_TYP_FUNCTION) {
3257
- deleteDiv(divRoot, `lblLoading`);
3258
3255
 
3259
3256
  // 外部データを読込(ファイルが見つからなかった場合は譜面追記をスキップ)
3260
3257
  externalDosInit();
@@ -5016,7 +5013,7 @@ const setColorList = (_data, _colorInit, _colorInitLength,
5016
5013
 
5017
5014
  /**
5018
5015
  * 複合カスタムゲージの定義設定
5019
- * |customGauge=Original::F,Normal::V,Escape::V|
5016
+ * |customGauge=_Original::F::Original,_Normal::V::Normal,Escape::V|
5020
5017
  * @param {object} _dosObj
5021
5018
  * @param {string} [object.scoreId=0]
5022
5019
  * @returns {object} ※Object.assign(obj, resetCustomGauge(...))の形で呼び出しが必要
@@ -5044,6 +5041,9 @@ const resetCustomGauge = (_dosObj, { scoreId = 0 } = {}) => {
5044
5041
  const customGaugeSets = customGauges[j].split(`::`);
5045
5042
  obj[`custom${scoreId}`][j] = customGaugeSets[0];
5046
5043
  obj[`varCustom${scoreId}`][j] = boolToSwitch(customGaugeSets[1] === `V`);
5044
+ if (hasVal(customGaugeSets[2])) {
5045
+ g_lblNameObj[`u_${customGaugeSets[0]}`] = customGaugeSets[2];
5046
+ }
5047
5047
  }
5048
5048
  if (scoreId === 0) {
5049
5049
  obj.custom = obj.custom0.concat();
@@ -5660,7 +5660,7 @@ const setKeyDfVal = _ptnName => {
5660
5660
  */
5661
5661
  const titleInit = (_initFlg = false) => {
5662
5662
 
5663
- clearWindow(true);
5663
+ clearWindow();
5664
5664
  g_currentPage = `title`;
5665
5665
  g_stateObj.settingSummaryVisible = false;
5666
5666
 
@@ -6391,9 +6391,10 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
6391
6391
  if (encodeFlg) {
6392
6392
  try {
6393
6393
  closeExistingAudio();
6394
- if (g_audioBufferCache.has(url)) {
6394
+ const cachedBuffer = getAudioBufferFromCache(url);
6395
+ if (cachedBuffer !== undefined) {
6395
6396
  const tmpAudio = new AudioPlayer();
6396
- tmpAudio.setBuffer(g_audioBufferCache.get(url)); // 新設メソッド、decode不要
6397
+ tmpAudio.setBuffer(cachedBuffer);
6397
6398
  g_audioForMS = tmpAudio;
6398
6399
  } else {
6399
6400
  await loadScript2(url);
@@ -6668,7 +6669,7 @@ const setWindowStyle = (_text, _bkColor, _textColor, _align = C_ALIGN_LEFT, { _x
6668
6669
  /*-----------------------------------------------------------*/
6669
6670
 
6670
6671
  const dataMgtInit = () => {
6671
- clearWindow(true);
6672
+ clearWindow();
6672
6673
  pauseBGM();
6673
6674
  const prevPage = g_currentPage;
6674
6675
  g_currentPage = `dataMgt`;
@@ -6906,7 +6907,7 @@ const dataMgtInit = () => {
6906
6907
  /*-----------------------------------------------------------*/
6907
6908
 
6908
6909
  const preconditionInit = () => {
6909
- clearWindow(true);
6910
+ clearWindow();
6910
6911
  pauseBGM();
6911
6912
  const prevPage = g_currentPage;
6912
6913
  g_currentPage = `precondition`;
@@ -7134,7 +7135,7 @@ const makePlayButton = _func => createCss2Button(`btnPlay`, g_lblNameObj.b_play,
7134
7135
  */
7135
7136
  const optionInit = () => {
7136
7137
 
7137
- clearWindow(true);
7138
+ clearWindow();
7138
7139
  pauseBGM();
7139
7140
  const divRoot = document.getElementById(`divRoot`);
7140
7141
  g_currentPage = `option`;
@@ -9212,7 +9213,7 @@ const resetGroupList = (_type, _keyCtrlPtn) => {
9212
9213
 
9213
9214
  const settingsDisplayInit = () => {
9214
9215
 
9215
- clearWindow(true);
9216
+ clearWindow();
9216
9217
  const divRoot = document.getElementById(`divRoot`);
9217
9218
  g_currentPage = `settingsDisplay`;
9218
9219
 
@@ -10019,7 +10020,7 @@ const interlockingButton = (_headerObj, _name, _current, _next, _buttonFlg = fal
10019
10020
  /*-----------------------------------------------------------*/
10020
10021
 
10021
10022
  const exSettingInit = () => {
10022
- clearWindow(true);
10023
+ clearWindow();
10023
10024
  g_currentPage = `exSetting`;
10024
10025
 
10025
10026
  multiAppend(divRoot,
@@ -10180,7 +10181,7 @@ const createGeneralSettingEx = (_spriteList, _name, { defaultList = [C_FLG_OFF],
10180
10181
  */
10181
10182
  const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10182
10183
 
10183
- clearWindow(true);
10184
+ clearWindow();
10184
10185
  const divRoot = document.getElementById(`divRoot`);
10185
10186
  g_kcType = _kcType;
10186
10187
  g_currentPage = `keyConfig`;
@@ -10756,6 +10757,7 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10756
10757
  document.getElementById(`key${_num}`).classList.replace(g_cssObj.button_Mini, g_cssObj.button_Next);
10757
10758
  }
10758
10759
  }
10760
+ keyconfigKeyboardPreview.refresh();
10759
10761
  };
10760
10762
 
10761
10763
  /**
@@ -11414,6 +11416,7 @@ const keyconfigKeyboardPreview = (() => {
11414
11416
  canvasMap: null,
11415
11417
  keyDataList: [], // { code, x, y, w, h, label } — drawMap で照合するキャッシュ
11416
11418
  scale: 1, // BASE_KEY_W/H に掛けるスケール係数
11419
+ cvsX: 0, // Canvas の左上 X 座標(divRoot 内の相対座標)
11417
11420
  cvsW: 500, // 実際の Canvas 幅(スケール計算後)
11418
11421
  cvsH: 240, // 実際の Canvas 高さ(スケール計算後)
11419
11422
  };
@@ -11455,7 +11458,12 @@ const keyconfigKeyboardPreview = (() => {
11455
11458
  const totalW = baseMainW + BASE_KEY_GAP * 2 + BASE_NAV_W + BASE_KEY_GAP * 2
11456
11459
  + BASE_KEY_GAP * 3 + BASE_NUM_W;
11457
11460
 
11458
- const availW = g_btnWidth();
11461
+ const tkObj = getKeyInfo();
11462
+ const keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`;
11463
+ const configKeyGroupList = g_headerObj.keyGroupOrder[g_stateObj.scoreId] ??
11464
+ g_keyObj[`keyGroupOrder${keyCtrlPtn}`] ?? tkObj.keyGroupList;
11465
+
11466
+ const availW = g_btnWidth() + (configKeyGroupList.length > 1 ? -g_lblPosObj.lnkKeySwitch.w - 10 : 0);
11459
11467
  const availH = g_sHeight - 200 - LEGEND_H; // 下部 UI ぶんを除いた高さ
11460
11468
 
11461
11469
  // 縦幅基準: MAIN/NAV 高さ と テンキー高さ(余白込み)の大きい方
@@ -11465,6 +11473,7 @@ const keyconfigKeyboardPreview = (() => {
11465
11473
  const scaleH = availH / totalH;
11466
11474
  _state.scale = Math.min(scaleW, scaleH, 1.5); // 最大 1.5 倍まで拡大可
11467
11475
 
11476
+ _state.cvsX = configKeyGroupList.length > 1 ? (-g_lblPosObj.lnkKeySwitch.w - 10) / 2 : 0;
11468
11477
  _state.cvsW = Math.floor(totalW * _state.scale);
11469
11478
  _state.cvsH = Math.floor(totalH * _state.scale) + LEGEND_H;
11470
11479
  };
@@ -11737,7 +11746,7 @@ const keyconfigKeyboardPreview = (() => {
11737
11746
  divRoot.appendChild(btn);
11738
11747
 
11739
11748
  // プレビューエリア: 水平・垂直センタリング
11740
- const areaX = g_btnX() + Math.floor((g_btnWidth() - _state.cvsW) / 2);
11749
+ const areaX = g_btnX() + Math.floor((g_btnWidth() - _state.cvsW) / 2) + _state.cvsX;
11741
11750
  const areaY = 130;
11742
11751
 
11743
11752
  const areaDiv = createEmptySprite(divRoot, C_PREVIEW_ID, {
@@ -11956,9 +11965,9 @@ const changeShuffleConfigColor = (_keyCtrlPtn, _vals, _j = -1) => {
11956
11965
  /* Scene : LOADING [strawberry] */
11957
11966
  /*-----------------------------------------------------------*/
11958
11967
 
11959
- const loadMusic = () => {
11968
+ const loadMusic = async () => {
11960
11969
 
11961
- clearWindow(true);
11970
+ clearWindow();
11962
11971
  pauseBGM();
11963
11972
  g_currentPage = `loading`;
11964
11973
 
@@ -11971,128 +11980,228 @@ const loadMusic = () => {
11971
11980
  const lblLoading = getLoadingLabel();
11972
11981
  divRoot.appendChild(lblLoading);
11973
11982
 
11974
- // ローカル動作時
11975
- if (g_isFile) {
11976
- setAudio(url);
11977
- return;
11983
+ // 音源準備Promise(ローカル/オンライン双方を吸収)
11984
+ const audioReadyPromise = loadAndSetupAudio(url, lblLoading);
11985
+
11986
+ // 譜面データ読込・変換処理Promise(g_audioに依存しない部分)
11987
+ const chartReadyPromise = loadChartFile().then(() => prepareScoreData());
11988
+
11989
+ // 両方の完了を待つ
11990
+ let loadSucceeded = true;
11991
+ try {
11992
+ await Promise.all([audioReadyPromise, chartReadyPromise]);
11993
+ } catch (e) {
11994
+ console.warn(`Loading error: ${e}`);
11995
+ loadSucceeded = false;
11996
+ } finally {
11997
+ deleteDiv(divRoot, `lblLoading`);
11998
+ }
11999
+
12000
+ if (loadSucceeded) {
12001
+ mainInit(); // 音源・譜面変換双方の完了後にまとめて呼ぶ
11978
12002
  }
12003
+ };
11979
12004
 
11980
- // XHRで読み込み
12005
+ /**
12006
+ * 音源の取得とセットアップ
12007
+ * - エンコード形式(base64)か通常の音声ファイルかを判定し、それぞれの準備処理に振り分ける
12008
+ * - iOSの場合はユーザー操作(ジェスチャー)を待ってから再生準備を行う(readyToStart経由)
12009
+ * - キャッシュヒット時はダウンロード自体を行わないよう、取得処理はsetupWebAudioへ
12010
+ * コールバックとして渡し、キャッシュミス時にのみ評価されるようにしている
12011
+ * @param {string} _url 音源の取得元URL
12012
+ * @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
12013
+ * @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
12014
+ */
12015
+ const loadAndSetupAudio = async (_url, _lblLoading) => {
12016
+
12017
+ /**
12018
+ * iOSの場合はユーザー操作(ジェスチャー)を待ってから_funcを実行する
12019
+ * - AudioContextの制約上、iOSはジェスチャーを起点にしないと音声再生が許可されないため
12020
+ * @param {() => Promise<void>} _func 実行する音源準備処理
12021
+ * @returns {Promise<void>}
12022
+ */
12023
+ const readyToStart = _func => {
12024
+ if (!g_isIos) {
12025
+ return _func(); // 通常環境はそのまま実行するだけ
12026
+ }
12027
+ return new Promise((resolve, reject) => {
12028
+ g_currentPage = `loadingIos`;
12029
+ _lblLoading.textContent = `Click to Start!`;
12030
+ divRoot.appendChild(makePlayButton(evt => {
12031
+ getSharedAudioContext().resume();
12032
+ g_currentPage = `loading`;
12033
+ resetKeyControl();
12034
+ divRoot.removeChild(evt.target);
12035
+ _func().then(resolve).catch(reject);
12036
+ }));
12037
+ setShortcutEvent(g_currentPage);
12038
+ });
12039
+ };
12040
+
12041
+ // 音源準備処理そのものを、条件に応じて組み立てる
12042
+ const setupAudioFunc = (() => {
12043
+
12044
+ // エンコードなし & ローカル実行: Audio要素で直接再生
12045
+ if (!g_musicEncodedFlg && g_isFile) {
12046
+ return () => {
12047
+ g_audio = new Audio();
12048
+ g_audio.src = _url;
12049
+ return musicAfterLoaded();
12050
+ };
12051
+ }
12052
+
12053
+ // エンコードなし & オンライン: WebAudioAPI経由(URLからfetch)
12054
+ if (!g_musicEncodedFlg) {
12055
+ return () => setupWebAudio(async () => {
12056
+ const blobUrl = await fetchMusicBlobUrl(_url, _lblLoading);
12057
+ try {
12058
+ const response = await fetch(blobUrl);
12059
+ return await response.arrayBuffer();
12060
+ } finally {
12061
+ URL.revokeObjectURL(blobUrl);
12062
+ }
12063
+ }, _url);
12064
+ }
12065
+
12066
+ // エンコードあり: スクリプト読込・musicInit実行を経てWebAudioAPI準備
12067
+ return () => setupWebAudio(async () => {
12068
+ const scriptSrc = g_isFile ? _url : await fetchMusicBlobUrl(_url, _lblLoading);
12069
+ try {
12070
+ await loadScript2(scriptSrc);
12071
+ } finally {
12072
+ if (!g_isFile) {
12073
+ URL.revokeObjectURL(scriptSrc);
12074
+ }
12075
+ }
12076
+ if (typeof musicInit !== C_TYP_FUNCTION) {
12077
+ makeWarningWindow(g_msgInfoObj.E_0031, { backBtnUse: true });
12078
+ throw new Error(`musicInit is not defined`);
12079
+ }
12080
+ musicInit();
12081
+ return base64ToUint8Array(g_musicdata).buffer;
12082
+ }, _url);
12083
+ })();
12084
+
12085
+ return readyToStart(setupAudioFunc);
12086
+ };
12087
+
12088
+ /**
12089
+ * XHRによる音源ファイルのダウンロード(Promise化)
12090
+ * - ダウンロードして Blob URL を返す
12091
+ * @param {string} _url 音源ファイルのURL
12092
+ * @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
12093
+ * @returns {Promise<string>} Blob URL
12094
+ */
12095
+ const fetchMusicBlobUrl = (_url, _lblLoading) => new Promise((resolve, reject) => {
11981
12096
  const request = new XMLHttpRequest();
11982
- request.open(`GET`, url, true);
12097
+ request.open(`GET`, _url, true);
11983
12098
  request.responseType = `blob`;
11984
12099
 
12100
+ const STALL_TIMEOUT_MS = 30000; // 30秒間、進捗がなければ停滞とみなす
12101
+ let stallTimer = null;
12102
+
12103
+ // 停滞タイマーをリセット
12104
+ const resetStallTimer = () => {
12105
+ clearTimeout(stallTimer);
12106
+ stallTimer = setTimeout(() => {
12107
+ request.abort();
12108
+ makeWarningWindow(g_msgInfoObj.E_0033, { backBtnUse: true });
12109
+ reject(new Error(`stalled`));
12110
+ }, STALL_TIMEOUT_MS);
12111
+ };
12112
+
11985
12113
  // 読み込み完了時
11986
12114
  request.addEventListener(`load`, () => {
12115
+ clearTimeout(stallTimer);
11987
12116
  if (request.status >= 200 && request.status < 300) {
11988
12117
  const blobUrl = URL.createObjectURL(request.response);
11989
12118
  createEmptySprite(divRoot, `loader`, g_windowObj.loader);
11990
- lblLoading.textContent = g_lblNameObj.pleaseWait;
11991
- setAudio(blobUrl, url);
12119
+ _lblLoading.textContent = g_lblNameObj.pleaseWait;
12120
+ resolve(blobUrl);
11992
12121
  } else {
11993
- makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(url))}<br>(${request.status} ${request.statusText})`, { backBtnUse: true });
12122
+ makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(_url))}<br>(${request.status} ${request.statusText})`, { backBtnUse: true });
12123
+ reject(new Error(`HTTP ${request.status}`));
11994
12124
  }
11995
12125
  });
11996
12126
 
11997
12127
  // 進捗時
11998
12128
  request.addEventListener(`progress`, _event => {
11999
- const lblLoading = document.getElementById(`lblLoading`);
12129
+ resetStallTimer(); // 進捗があるたびにタイマーをリセット
12130
+ const lblLoadingElem = document.getElementById(`lblLoading`);
12131
+ if (lblLoadingElem === null) return; // 並列処理で先に削除されている場合の防御
12000
12132
 
12001
12133
  if (_event.lengthComputable) {
12002
12134
  const rate = _event.loaded / _event.total;
12003
12135
  createEmptySprite(divRoot, `loader`, { y: g_sHeight - 10, h: 10, w: g_sWidth * rate, backgroundColor: `#eeeeee` });
12004
- lblLoading.textContent = `${g_lblNameObj.nowLoading} ${Math.floor(rate * 100)}%`;
12136
+ lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${Math.floor(rate * 100)}%`;
12005
12137
  } else {
12006
- lblLoading.textContent = `${g_lblNameObj.nowLoading} ${_event.loaded}Bytes`;
12138
+ lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${_event.loaded}Bytes`;
12007
12139
  }
12008
12140
  // ユーザカスタムイベント
12009
12141
  safeExecuteCustomHooks(`g_customJsObj.progress`, g_customJsObj.progress, _event);
12010
12142
  });
12011
12143
 
12012
- // エラー処理
12013
- request.addEventListener(`timeout`, () => makeWarningWindow(g_msgInfoObj.E_0033, { backBtnUse: true }));
12014
- request.addEventListener(`error`, () => makeWarningWindow(g_msgInfoObj.E_0034, { backBtnUse: true }));
12144
+ request.addEventListener(`error`, () => {
12145
+ clearTimeout(stallTimer);
12146
+ makeWarningWindow(g_msgInfoObj.E_0034, { backBtnUse: true });
12147
+ reject(new Error(`network error`));
12148
+ });
12015
12149
 
12150
+ resetStallTimer(); // 初回(最初のprogressが来るまで)のタイマーも開始
12016
12151
  request.send();
12017
- };
12152
+ });
12018
12153
 
12019
12154
  /**
12020
- * 音楽データの設定
12021
- * iOSの場合はAudioタグによる再生
12022
- * @param {string} _url
12023
- * @param {string} _cacheKey
12155
+ * WebAudioAPIによる音源再生の準備(共通処理)
12156
+ * - AudioPlayerを生成し、canplaythrough/errorの発火待ちを開始した上で、
12157
+ * ArrayBufferを取得してデコードする(キャッシュヒット時はデコードをスキップ)
12158
+ * @param {() => Promise<ArrayBuffer>} _fetchArrayBuffer 未キャッシュの場合にArrayBufferを取得する関数
12159
+ * @param {string} [_cacheKey] AudioBufferキャッシュ照合用のキー(省略時はキャッシュを使わない)
12160
+ * @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
12024
12161
  */
12025
- const setAudio = async (_url, _cacheKey = _url) => {
12026
-
12027
- const loadMp3 = () => {
12028
- if (g_isFile) {
12029
- g_audio = new Audio();
12030
- g_audio.src = _url;
12031
- musicAfterLoaded();
12032
- } else {
12033
- initWebAudioAPIfromURL(_url);
12034
- }
12035
- };
12036
-
12037
- const readyToStart = _func => {
12038
- if (g_isIos) {
12039
- g_currentPage = `loadingIos`;
12040
- lblLoading.textContent = `Click to Start!`;
12041
- divRoot.appendChild(makePlayButton(evt => {
12042
- getSharedAudioContext().resume();
12043
- g_currentPage = `loading`;
12044
- resetKeyControl();
12045
- divRoot.removeChild(evt.target);
12046
- _func();
12047
- }));
12048
- setShortcutEvent(g_currentPage);
12049
- } else {
12050
- _func();
12051
- }
12052
- };
12162
+ const setupWebAudio = async (_fetchArrayBuffer, _cacheKey) => {
12163
+ g_audio = new AudioPlayer();
12164
+ const loadedPromise = musicAfterLoaded(); // canplaythrough/errorの発火をここで待つ
12053
12165
 
12054
- if (g_musicEncodedFlg) {
12055
- await loadScript2(_url);
12056
- if (typeof musicInit === C_TYP_FUNCTION) {
12057
- musicInit();
12058
- readyToStart(() => initWebAudioAPIfromBase64(g_musicdata, _cacheKey));
12059
- } else {
12060
- makeWarningWindow(g_msgInfoObj.E_0031);
12061
- musicAfterLoaded();
12062
- }
12166
+ const cachedBuffer = _cacheKey ? getAudioBufferFromCache(_cacheKey) : undefined;
12167
+ if (cachedBuffer !== undefined) {
12168
+ g_audio.setBuffer(cachedBuffer);
12063
12169
  } else {
12064
- readyToStart(() => loadMp3());
12170
+ const arrayBuffer = await _fetchArrayBuffer();
12171
+ await g_audio.init(arrayBuffer);
12172
+ if (_cacheKey) {
12173
+ cacheAudioBuffer(_cacheKey, g_audio.getBuffer());
12174
+ }
12065
12175
  }
12176
+ return loadedPromise;
12066
12177
  };
12067
12178
 
12068
- // Base64から音声データに変換してWebAudioAPIで再生する準備
12069
- const initWebAudioAPIfromBase64 = async (_base64, _cacheKey) => {
12070
- g_audio = new AudioPlayer();
12071
- musicAfterLoaded();
12179
+ const g_audioBufferCache = new Map();
12180
+ const AUDIO_CACHE_MAX = 5;
12072
12181
 
12073
- if (_cacheKey && g_audioBufferCache.has(_cacheKey)) {
12074
- g_audio.setBuffer(g_audioBufferCache.get(_cacheKey)); // デコード完全スキップ
12075
- return;
12076
- }
12077
- const array = base64ToUint8Array(_base64);
12078
- await g_audio.init(array.buffer);
12079
- if (_cacheKey) {
12080
- cacheAudioBuffer(_cacheKey, g_audio.getBuffer());
12182
+ /**
12183
+ * デコード済みAudioBufferのキャッシュからの取得
12184
+ * - Mapは挿入順を保持するのみでアクセス順を保持しないため、
12185
+ * ヒット時にエントリを一度削除して再挿入し、最新として扱う(LRU方式)
12186
+ * @param {string} _key キャッシュキー(楽曲の実URL)
12187
+ * @returns {AudioBuffer|undefined} キャッシュされたAudioBuffer。存在しない場合はundefined
12188
+ */
12189
+ const getAudioBufferFromCache = (_key) => {
12190
+ if (!g_audioBufferCache.has(_key)) {
12191
+ return undefined;
12081
12192
  }
12193
+ const buffer = g_audioBufferCache.get(_key);
12194
+ g_audioBufferCache.delete(_key);
12195
+ g_audioBufferCache.set(_key, buffer); // 末尾(最新)に再挿入
12196
+ return buffer;
12082
12197
  };
12083
12198
 
12084
- // 音声ファイルを読み込んでWebAudioAPIで再生する準備
12085
- const initWebAudioAPIfromURL = async (_url) => {
12086
- g_audio = new AudioPlayer();
12087
- musicAfterLoaded();
12088
- const promise = await fetch(_url);
12089
- const arrayBuffer = await promise.arrayBuffer();
12090
- await g_audio.init(arrayBuffer);
12091
- };
12092
-
12093
- const g_audioBufferCache = new Map();
12094
- const AUDIO_CACHE_MAX = 5;
12095
-
12199
+ /**
12200
+ * デコード済みAudioBufferのキャッシュへの登録
12201
+ * - 直近 AUDIO_CACHE_MAX 件を保持するLRU方式。上限を超えた場合は最も古いエントリから破棄する
12202
+ * @param {string} _key キャッシュキー(楽曲の実URL)
12203
+ * @param {AudioBuffer} _buffer デコード済みのAudioBuffer
12204
+ */
12096
12205
  const cacheAudioBuffer = (_key, _buffer) => {
12097
12206
  g_audioBufferCache.set(_key, _buffer);
12098
12207
  if (g_audioBufferCache.size > AUDIO_CACHE_MAX) {
@@ -12100,6 +12209,13 @@ const cacheAudioBuffer = (_key, _buffer) => {
12100
12209
  }
12101
12210
  };
12102
12211
 
12212
+ /**
12213
+ * base64文字列をUint8Arrayに変換
12214
+ * - Uint8Array.from(atob(str), callback)によるコールバック呼び出し形式は
12215
+ * 大容量データで変換オーバーヘッドが大きいため、forループによる直接代入で高速化している
12216
+ * @param {string} _base64Str base64エンコードされた文字列
12217
+ * @returns {Uint8Array} 変換後のバイト配列
12218
+ */
12103
12219
  const base64ToUint8Array = (_base64Str) => {
12104
12220
  const binaryStr = atob(_base64Str);
12105
12221
  const len = binaryStr.length;
@@ -12110,34 +12226,41 @@ const base64ToUint8Array = (_base64Str) => {
12110
12226
  return array;
12111
12227
  };
12112
12228
 
12113
- const musicAfterLoaded = () => {
12229
+ /**
12230
+ * 音源の再生準備完了を待つ
12231
+ * - g_audio が Audio要素の場合は canplaythrough/error イベントの発火を待つ
12232
+ * - g_audio が AudioPlayer の場合、readyState が既に4(デコード済み、キャッシュヒット時など)であれば即時解決する
12233
+ * - canplaythrough/error のどちらが発火しても、もう一方のリスナーも確実に削除する(cleanup)
12234
+ * @returns {Promise<void>} 再生準備が完了したら解決し、読込エラー時は例外を投げて拒否するPromise
12235
+ */
12236
+ const musicAfterLoaded = () => new Promise((resolve, reject) => {
12114
12237
  g_audio.load();
12115
12238
 
12116
12239
  if (g_audio.readyState === 4) {
12117
- // audioの読み込みが終わった後の処理
12118
- loadingScoreInit();
12240
+ resolve();
12119
12241
  } else {
12120
- // 読込中の状態
12121
- g_audio.addEventListener(`canplaythrough`, (() => function f() {
12122
- g_audio.removeEventListener(`canplaythrough`, f, false);
12123
- loadingScoreInit();
12124
- })(), false);
12125
-
12126
- // エラー時
12127
- g_audio.addEventListener(`error`, (() => function f() {
12128
- g_audio.removeEventListener(`error`, f, false);
12242
+ const onCanPlay = () => { cleanup(); resolve(); };
12243
+ const onError = () => {
12244
+ cleanup();
12129
12245
  makeWarningWindow(g_msgInfoObj.E_0041.split(`{0}`).join(g_audio.src), { backBtnUse: true });
12130
- })(), false);
12246
+ reject(new Error(`audio load error`));
12247
+ };
12248
+ const cleanup = () => {
12249
+ g_audio.removeEventListener(`canplaythrough`, onCanPlay, false);
12250
+ g_audio.removeEventListener(`error`, onError, false);
12251
+ };
12252
+ g_audio.addEventListener(`canplaythrough`, onCanPlay, false);
12253
+ g_audio.addEventListener(`error`, onError, false);
12131
12254
  }
12132
- };
12255
+ });
12133
12256
 
12134
12257
  /**
12135
- * 読込画面初期化
12258
+ * 譜面データの変換処理
12259
+ * - 音源データの状態に依存しない部分のみを担う(g_audio非参照)
12260
+ * - loadMusic経由(並行フロー)、executeRetry経由(曲中リトライ)の両方から呼ばれる
12136
12261
  */
12137
- const loadingScoreInit = async () => {
12262
+ const prepareScoreData = () => {
12138
12263
 
12139
- // 譜面データの読み込み
12140
- await loadChartFile();
12141
12264
  const tkObj = getKeyInfo();
12142
12265
  const [keyCtrlPtn, keyNum] = [tkObj.keyCtrlPtn, tkObj.keyNum];
12143
12266
  g_headerObj.blankFrameDef = setVal(g_headerObj.blankFrameDefs[g_stateObj.scoreId], g_headerObj.blankFrameDefs[0]);
@@ -12285,8 +12408,6 @@ const loadingScoreInit = async () => {
12285
12408
 
12286
12409
  // ユーザカスタムイベント
12287
12410
  safeExecuteCustomHooks(`g_customJsObj.loading`, g_customJsObj.loading);
12288
-
12289
- mainInit();
12290
12411
  };
12291
12412
 
12292
12413
  /**
@@ -14244,9 +14365,6 @@ const getArrowSettings = () => {
14244
14365
  g_workObj.frzReturnTimerId = null;
14245
14366
  }
14246
14367
 
14247
- // AutoRetryの初期化
14248
- g_workObj.autoRetryFlg = false;
14249
-
14250
14368
  // Camoufrageの設定
14251
14369
  if (g_stateObj.camoufrage !== C_FLG_OFF) {
14252
14370
 
@@ -14423,7 +14541,7 @@ const setKeyCtrl = (_localStorage, _keyNum, _keyCtrlPtn) => {
14423
14541
  * メイン画面初期化
14424
14542
  */
14425
14543
  const mainInit = () => {
14426
- clearWindow(true, `Main`);
14544
+ clearWindow(`Main`);
14427
14545
  const divRoot = document.getElementById(`divRoot`);
14428
14546
  document.oncontextmenu = () => false;
14429
14547
  g_currentPage = `main`;
@@ -14876,7 +14994,7 @@ const mainInit = () => {
14876
14994
  };
14877
14995
 
14878
14996
  // キー操作イベント
14879
- document.onkeydown = evt => {
14997
+ document.onkeydown = async evt => {
14880
14998
  evt.preventDefault();
14881
14999
  const setCode = transCode(evt);
14882
15000
 
@@ -14888,17 +15006,16 @@ const mainInit = () => {
14888
15006
 
14889
15007
  // 曲中リトライ、タイトルバック
14890
15008
  if (setCode === g_kCdN[g_headerObj.keyRetry]) {
14891
- g_audio.pause();
14892
- clearTimeout(g_timeoutEvtId);
14893
15009
 
14894
15010
  if (g_isMac && keyIsShift()) {
14895
15011
  // Mac OS、IPad OSはDeleteキーが無いためShift+BSで代用
15012
+ g_audio.pause();
15013
+ clearTimeout(g_timeoutEvtId);
14896
15014
  titleInit();
14897
15015
 
14898
15016
  } else {
14899
15017
  // その他の環境では単にRetryに対応するキーのみで適用
14900
- clearWindow();
14901
- musicAfterLoaded();
15018
+ await executeRetry(`Retry`);
14902
15019
  }
14903
15020
 
14904
15021
  } else if (setCode === g_kCdN[g_headerObj.keyTitleBack]) {
@@ -16180,6 +16297,32 @@ const executeFrzReturn = (_seq, _idx, _axis) => {
16180
16297
  g_workObj.frzReturnTimerId = setTimeout(() => executeFrzReturn(_seq, _idx + 1, _axis), 20);
16181
16298
  };
16182
16299
 
16300
+ /**
16301
+ * 曲中リトライの共通処理
16302
+ * - 手動リトライ、AutoRetryの両方から呼ばれる
16303
+ * - g_retryInProgressによる多重実行防止、失敗時のログ出力・フラグ復帰を一括で行う
16304
+ * @param {string} [_logLabel='Retry'] エラーログに表示するラベル(手動/自動の区別用)
16305
+ */
16306
+ const executeRetry = async (_logLabel = `Retry`) => {
16307
+ if (g_retryInProgress) {
16308
+ return;
16309
+ }
16310
+ g_retryInProgress = true;
16311
+ try {
16312
+ g_audio.pause();
16313
+ clearTimeout(g_timeoutEvtId);
16314
+ clearWindow(`Main`);
16315
+ await musicAfterLoaded();
16316
+ await loadChartFile();
16317
+ prepareScoreData();
16318
+ mainInit();
16319
+ } catch (e) {
16320
+ console.warn(`${_logLabel} audio load error: ${e}`);
16321
+ } finally {
16322
+ g_retryInProgress = false;
16323
+ }
16324
+ };
16325
+
16183
16326
  /**
16184
16327
  * AutoRetryの設定
16185
16328
  * @param {string} _retryCondition リトライ基準となるAutoRetry名
@@ -16189,13 +16332,9 @@ const quickRetry = (_retryCondition) => {
16189
16332
  if (retryNum < 0) {
16190
16333
  return;
16191
16334
  }
16192
- if (g_settings.autoRetryNum >= retryNum && !g_workObj.autoRetryFlg) {
16193
- g_workObj.autoRetryFlg = true;
16194
- setTimeout(() => {
16195
- g_audio.pause();
16196
- clearTimeout(g_timeoutEvtId);
16197
- clearWindow();
16198
- musicAfterLoaded();
16335
+ if (g_settings.autoRetryNum >= retryNum && !g_retryInProgress) {
16336
+ setTimeout(async () => {
16337
+ await executeRetry(`AutoRetry`);
16199
16338
  }, 16);
16200
16339
  }
16201
16340
  };
@@ -16796,7 +16935,7 @@ const finishViewing = () => {
16796
16935
  */
16797
16936
  const resultInit = () => {
16798
16937
 
16799
- clearWindow(true);
16938
+ clearWindow();
16800
16939
  g_currentPage = `result`;
16801
16940
 
16802
16941
  // 結果画面用フレーム初期化
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "49.1.1",
3
+ "version": "49.3.0",
4
4
  "description": "Dancing☆Onigiri (CW Edition) - Web-based Rhythm Game",
5
5
  "main": "./js/danoni_main.js",
6
6
  "jsdelivr": "./js/danoni_main.js",