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.
- package/js/danoni_main.js +302 -163
- 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/
|
|
7
|
+
* Revised : 2026/07/12
|
|
8
8
|
*
|
|
9
9
|
* https://github.com/cwtickle/danoniplus
|
|
10
10
|
*/
|
|
11
|
-
const g_version = `Ver 49.
|
|
12
|
-
const g_revisedDate = `2026/07/
|
|
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 = (
|
|
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
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
g_cxtDeleteFlg = {};
|
|
2125
|
+
g_btnAddFunc = {};
|
|
2126
|
+
g_btnDeleteFlg = {};
|
|
2127
|
+
g_cxtAddFunc = {};
|
|
2128
|
+
g_cxtDeleteFlg = {};
|
|
2129
2129
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2130
|
+
if (document.getElementById(`layer0`) === null ||
|
|
2131
|
+
(!g_headerObj[`customBack${_customDisplayName}Use`] && !g_headerObj.defaultSkinFlg)) {
|
|
2132
2132
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2133
|
+
$id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
|
|
2134
|
+
createEmptySprite(divRoot, `divBack`, { w: g_sWidth + diffX });
|
|
2135
|
+
}
|
|
2136
2136
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2137
|
+
// CSSスタイルの初期化
|
|
2138
|
+
Object.keys(g_cssBkProperties).forEach(prop =>
|
|
2139
|
+
document.documentElement.style.setProperty(prop, g_cssBkProperties[prop]));
|
|
2140
2140
|
|
|
2141
|
-
|
|
2142
|
-
|
|
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=
|
|
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(
|
|
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
|
-
|
|
6394
|
+
const cachedBuffer = getAudioBufferFromCache(url);
|
|
6395
|
+
if (cachedBuffer !== undefined) {
|
|
6395
6396
|
const tmpAudio = new AudioPlayer();
|
|
6396
|
-
tmpAudio.setBuffer(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
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
|
-
|
|
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`,
|
|
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
|
-
|
|
11991
|
-
|
|
12119
|
+
_lblLoading.textContent = g_lblNameObj.pleaseWait;
|
|
12120
|
+
resolve(blobUrl);
|
|
11992
12121
|
} else {
|
|
11993
|
-
makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(
|
|
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
|
-
|
|
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
|
-
|
|
12136
|
+
lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${Math.floor(rate * 100)}%`;
|
|
12005
12137
|
} else {
|
|
12006
|
-
|
|
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
|
-
|
|
12014
|
-
|
|
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
|
-
*
|
|
12022
|
-
*
|
|
12023
|
-
* @param {
|
|
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
|
|
12026
|
-
|
|
12027
|
-
const
|
|
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
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12069
|
-
const
|
|
12070
|
-
g_audio = new AudioPlayer();
|
|
12071
|
-
musicAfterLoaded();
|
|
12179
|
+
const g_audioBufferCache = new Map();
|
|
12180
|
+
const AUDIO_CACHE_MAX = 5;
|
|
12072
12181
|
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
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
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12118
|
-
loadingScoreInit();
|
|
12240
|
+
resolve();
|
|
12119
12241
|
} else {
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 && !
|
|
16193
|
-
|
|
16194
|
-
|
|
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(
|
|
16938
|
+
clearWindow();
|
|
16800
16939
|
g_currentPage = `result`;
|
|
16801
16940
|
|
|
16802
16941
|
// 結果画面用フレーム初期化
|