danoniplus 49.6.1 → 50.1.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.
@@ -4,7 +4,7 @@
4
4
  本体cssファイル
5
5
 
6
6
  Created : 2018/10/08
7
- Revised : 2026/05/17 (v48.0.0)
7
+ Revised : 2026/08/29 (v50.1.0)
8
8
 
9
9
  https://github.com/cwtickle/danoniplus
10
10
  ------------------------------------------ */
@@ -24,6 +24,11 @@
24
24
  overflow: hidden;
25
25
  }
26
26
 
27
+ /* タブのバックグラウンド化に伴う一時停止(danoni_main.js側でdivRootにgamePausedクラスを付け外し) */
28
+ #divRoot.gamePaused * {
29
+ animation-play-state: paused !important;
30
+ }
31
+
27
32
  input[type="range"] {
28
33
  appearance: none;
29
34
  background: transparent;
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/08/19
7
+ * Revised : 2026/08/29
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 49.6.1`;
12
- const g_revisedDate = `2026/08/19`;
11
+ const g_version = `Ver 50.1.0`;
12
+ const g_revisedDate = `2026/08/29`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -123,6 +123,12 @@ const waitUntilLoaded = () => {
123
123
  await loadScript2(`${g_rootPath}../js/lib/danoni_constants.js?${g_versionForUrl}`);
124
124
  await loadScript2(`${g_rootPath}../js/lib/legacy_functions.js?${g_versionForUrl.split(`.`)[0]}`, false);
125
125
  initialControl();
126
+
127
+ // プレイ画面(g_currentPage === `main`)でのみ有効化される、タブ非表示検知(常時1個だけ登録)
128
+ g_handler.addListener(document, `visibilitychange`, () => {
129
+ if (g_currentPage !== `main`) return;
130
+ document.hidden ? g_timelineHooks.pause() : g_timelineHooks.resume();
131
+ });
126
132
  })();
127
133
 
128
134
  /*-----------------------------------------------------------*/
@@ -269,6 +275,11 @@ let g_audioForMS = null;
269
275
  let g_timeoutEvtId = 0;
270
276
  let g_timeoutEvtTitleId = 0;
271
277
  let g_timeoutEvtResultId = 0;
278
+
279
+ // タブのバックグラウンド化に伴う一時停止/再開のフック
280
+ let g_timelineHooks = { pause: () => { }, resume: () => { } };
281
+ let g_timelineSessionId = 0; // mainInit()呼び出しごとに採番。古いセッションのfinishResume無効化に使用
282
+
272
283
  let g_inputKeyBuffer = {};
273
284
 
274
285
  // 音楽ファイル エンコードフラグ
@@ -606,9 +617,9 @@ const formatObject = (_obj, _indent = 0, { colorFmt = true, rootKey = `` } = {})
606
617
  // scrollDirXのスクロール方向表示処理
607
618
  return _value === 1 ? `1|<span style="color:#ff9999">↑</span>` : `-1|<span style="color:#66ff66">↓</span>`;
608
619
 
609
- } else if (listMatching(_rootKey, [`keyCtrl`, `keyRetry`, `keyTitleBack`], { prefix: `^` })
620
+ } else if (listMatching(_rootKey, [`keyCtrl`, `keyRetry`, `keyTitleBack`, `keyPause`], { prefix: `^` })
610
621
  && !_rootKey.startsWith(`keyCtrlPtn`)) {
611
- // keyCtrlX, keyRetryX, keyTitleBackX の対応キー表示処理
622
+ // keyCtrlX, keyRetryX, keyTitleBackX, keyPauseX の対応キー表示処理
612
623
  return (g_kCd[_value] && _value !== 0) ? `${_value}|<span style="color:#ffff66">${g_kCd[_value]}</span>` : `----`;
613
624
  }
614
625
  } else if (isObj(_value)) {
@@ -2640,7 +2651,7 @@ class AudioPlayer {
2640
2651
  }
2641
2652
 
2642
2653
  get elapsedTime() {
2643
- return this._context.currentTime - this._startTime + this._fadeinPosition;
2654
+ return (this._context.currentTime - this._scheduledTime) * this.playbackRate + g_scheduleLead + this._fadeinPosition;
2644
2655
  }
2645
2656
 
2646
2657
  /** AudioContextの現在時刻(秒) */
@@ -4232,6 +4243,9 @@ const headerConvert = _dosObj => {
4232
4243
  obj.keyTitleBack = setIntVal(getKeyCtrlVal(_dosObj.keyTitleBack), C_KEY_TITLEBACK);
4233
4244
  obj.keyTitleBackDef = obj.keyTitleBack;
4234
4245
  obj.keyTitleBackDef2 = obj.keyTitleBack;
4246
+ obj.keyPause = setIntVal(getKeyCtrlVal(_dosObj.keyPause), C_KEY_PAUSE);
4247
+ obj.keyPauseDef = obj.keyPause;
4248
+ obj.keyPauseDef2 = obj.keyPause;
4235
4249
 
4236
4250
  // フリーズアローの許容フレーム数設定
4237
4251
  obj.frzAttempt = setIntVal(_dosObj.frzAttempt, C_FRM_FRZATTEMPT);
@@ -5702,6 +5716,9 @@ const keysConvert = (_dosObj, { keyExtraList = _dosObj.keyExtraList?.split(`,`)
5702
5716
  // プレイ中ショートカット:タイトルバック (keyTitleBackX_Y)
5703
5717
  newKeySingleParam(newKey, `keyTitleBack`, C_TYP_STRING, C_KEY_TITLEBACK);
5704
5718
 
5719
+ // プレイ中ショートカット:タイトルバック (keyPauseX_Y)
5720
+ newKeySingleParam(newKey, `keyPause`, C_TYP_STRING, C_KEY_PAUSE);
5721
+
5705
5722
  // 別キーフラグ (transKeyX_Y)
5706
5723
  newKeySingleParam(newKey, `transKey`, C_TYP_STRING, ``);
5707
5724
 
@@ -5740,8 +5757,9 @@ const keysConvert = (_dosObj, { keyExtraList = _dosObj.keyExtraList?.split(`,`)
5740
5757
  // カスタムキーで定義されたtransKeyPtnを補完
5741
5758
  completeTransKeyPtn([newKey]);
5742
5759
 
5743
- // keyRetry, keyTitleBackのキー名をキーコードに変換
5744
- const keyTypePatterns = Object.keys(g_keyObj).filter(val => val.startsWith(`keyRetry${newKey}`) || val.startsWith(`keyTitleBack${newKey}`));
5760
+ // keyRetry, keyTitleBack, keyPauseのキー名をキーコードに変換
5761
+ const keyTypePatterns = Object.keys(g_keyObj).filter(val =>
5762
+ val.startsWith(`keyRetry${newKey}`) || val.startsWith(`keyTitleBack${newKey}`) || val.startsWith(`keyPause${newKey}`));
5745
5763
  keyTypePatterns.forEach(name => g_keyObj[name] = getKeyCtrlVal(g_keyObj[name]));
5746
5764
  } catch (e) {
5747
5765
  g_headerObj.undefinedKeyListFinal.push(newKey);
@@ -8461,6 +8479,7 @@ const setDifficulty = (_initFlg) => {
8461
8479
 
8462
8480
  // ユーザカスタムイベント(初期)
8463
8481
  safeExecuteCustomHooks(`g_customJsObj.difficulty`, g_customJsObj.difficulty, _initFlg, g_canLoadDifInfoFlg);
8482
+ resolveKeyFamily();
8464
8483
 
8465
8484
  // 設定サマリー表示の更新
8466
8485
  updateSettingSummary();
@@ -8470,6 +8489,77 @@ const setDifficulty = (_initFlg) => {
8470
8489
  g_canLoadDifInfoFlg = true;
8471
8490
  };
8472
8491
 
8492
+ /**
8493
+ * keyLabelの系統ごとに、選択時に強制したい設定値・選択/離脱時の個別処理を登録する
8494
+ * @param {string} _name 系統名
8495
+ * @param {(key: string) => boolean} _match この系統に属するkeyLabelかどうか
8496
+ * @param {object} [_fields] path -> 値 or 値を返す関数(単純代入で済むもの)
8497
+ * @param {object} [_hooks] { onAcquire, onRelease } 選択/離脱の瞬間に1回だけ呼ばれる(複合処理用)
8498
+ * @param {object} [_appliers] path -> 個別の適用関数(単純代入で済まないfields用)
8499
+ */
8500
+ const registerKeyFamily = (_name, _match, _fields = {}, _hooks = {}, _appliers = {}) => {
8501
+ g_familyObj.families.push({ name: _name, match: _match, fields: _fields, appliers: _appliers, ..._hooks });
8502
+ };
8503
+
8504
+ /**
8505
+ * 現在のキー状態に応じて適用すべき設定群を判定し、
8506
+ * 各パラメータの値・スナップショット・フックを管理・適用する
8507
+ */
8508
+ const resolveKeyFamily = () => {
8509
+ // 1. 現在のキーに一致するファミリーを検索(見つからなければ null = 標準状態)
8510
+ const newFamily = g_familyObj.families.find(f => f.match(g_keyObj.currentKey)) ?? null;
8511
+
8512
+ // 2. ファミリーが切り替わった瞬間(入場・離脱)にフック(固有の副作用)を実行
8513
+ if (newFamily !== g_familyObj.currentFamily) {
8514
+ g_familyObj.currentFamily?.onRelease?.();
8515
+ newFamily?.onAcquire?.();
8516
+ g_familyObj.currentFamily = newFamily;
8517
+ }
8518
+
8519
+ // 3. 登録されているすべてのファミリーが持つパス(プロパティ)の全リストを取得
8520
+ const allPaths = new Set(g_familyObj.families.flatMap(f => Object.keys(f.fields)));
8521
+ allPaths.forEach(path => {
8522
+ const spec = newFamily?.fields[path];
8523
+ // 該当プロパティのカスタム処理があれば優先し、なければ通常のプロパティ代入を使用
8524
+ const applier = g_familyObj.families.map(f => f.appliers[path]).find(Boolean) ?? (v => setPathVal(path, v));
8525
+
8526
+ if (spec !== undefined) {
8527
+ // 【変更値の適用】現在のファミリーにそのプロパティの設定がある場合
8528
+ // 初めて書き換えるプロパティの場合のみ、元の値をスナップショット(初期値)として退避
8529
+ if (!g_familyObj.ownership[path]) {
8530
+ g_familyObj.snapshot[path] = getPathVal(path);
8531
+ g_familyObj.ownership[path] = true;
8532
+ }
8533
+ // 関数なら評価し、値ならそのまま適用値とする
8534
+ applyIfChanged(path, typeof spec === `function` ? spec() : spec, applier);
8535
+ } else if (g_familyObj.ownership[path]) {
8536
+ // 【標準への復元】別のファミリーに移行し、かつ元々自分が書き換えていたプロパティの場合
8537
+ // スナップショットから元の値(初期値)を復元し、オーナーシップを解放
8538
+ applyIfChanged(path, g_familyObj.snapshot[path], applier);
8539
+ g_familyObj.ownership[path] = false;
8540
+ }
8541
+ // ownership[path]がfalseのままなら、標準同士の切り替えでも一切触らない
8542
+ });
8543
+ };
8544
+
8545
+ /**
8546
+ * 値に変更がある場合のみ、代入またはカスタム処理を実行してキャッシュを更新する
8547
+ * @param {string} _path 設定のパス
8548
+ * @param {string|number} _value 適用する値
8549
+ * @param {Function} _applier 実際の適用処理を行う関数
8550
+ */
8551
+ const applyIfChanged = (_path, _value, _applier) => {
8552
+ // すでに同じ値が適用済みの場合は、無駄な再描画や再代入を防ぐためスキップ
8553
+ if (g_familyObj.appliedPaths.has(_path) && g_familyObj.applied[_path] === _value) return;
8554
+
8555
+ // 適用値をキャッシュに保存
8556
+ g_familyObj.applied[_path] = _value;
8557
+ g_familyObj.appliedPaths.add(_path);
8558
+
8559
+ // 実際の反映処理(値の代入、または画像再描画などの副作用)を実行
8560
+ _applier(_value);
8561
+ };
8562
+
8473
8563
  /**
8474
8564
  * 設定・オプション画面のラベル・ボタン処理の描画
8475
8565
  * @param {Object} _sprite 基準とするスプライト(ここで指定する座標は、そのスプライトからの相対位置)
@@ -9786,7 +9876,7 @@ const buildPreviewUI = (_frame, _playW, _playH) => {
9786
9876
  // ============================================================
9787
9877
  const scGroup = createEmptySprite(_frame, `previewScGroup`, {
9788
9878
  x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
9789
- y: _playH - 65 + g_diffObj.shortcutY, w: 80, h: 65, pointerEvents: C_DIS_AUTO,
9879
+ y: _playH - 100 + g_diffObj.shortcutY, w: 80, h: 100, pointerEvents: C_DIS_AUTO,
9790
9880
  });
9791
9881
  multiAppend(scGroup,
9792
9882
  createDivCss2Label(`lblRetry`, `[${g_lblNameObj.l_retry}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 0 }),
@@ -9795,15 +9885,18 @@ const buildPreviewUI = (_frame, _playW, _playH) => {
9795
9885
  createDivCss2Label(`lblTitleBack`, `[${g_lblNameObj.l_titleBack}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 35 }),
9796
9886
  createDivCss2Label(`lblTitleBackSc`, g_isMac ? `Shift+${g_kCd[g_headerObj.keyRetry]}` : g_kCd[g_headerObj.keyTitleBack],
9797
9887
  { ...g_lblPosObj.lblMainScKey, x: 0, y: 50, fontWeight: g_headerObj.keyTitleBack === C_KEY_TITLEBACK ? `normal` : `bold` }),
9888
+ createDivCss2Label(`lblPause`, `[${g_lblNameObj.l_pause}]`, { ...g_lblPosObj.lblMainScHeader, x: 0, y: 70 }),
9889
+ createDivCss2Label(`lblPauseSc`, g_kCd[g_headerObj.keyPause],
9890
+ { ...g_lblPosObj.lblMainScKey, x: 0, y: 85, fontWeight: g_headerObj.keyPause === C_KEY_PAUSE ? `normal` : `bold` }),
9798
9891
  );
9799
9892
  const scConfig = {
9800
9893
  toastTitle: g_lblNameObj.shortcutUpdate,
9801
9894
  getStdPos: () => ({
9802
9895
  x: g_sWidth + g_headerObj.scAreaWidth - 85,
9803
- y: _playH - 65
9896
+ y: _playH - 100
9804
9897
  }),
9805
9898
  };
9806
- makeElementDraggable(scGroup, `shortcut`, _playW, _playH, { w: 80, h: 65 }, scConfig);
9899
+ makeElementDraggable(scGroup, `shortcut`, _playW, _playH, { w: 80, h: 100 }, scConfig);
9807
9900
 
9808
9901
  // ユーザカスタムイベント(プレビュー表示用)
9809
9902
  safeExecuteCustomHooks(`g_customJsObj.displayPreview`, g_customJsObj.displayPreview, _frame, _playW, _playH);
@@ -10828,13 +10921,22 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10828
10921
 
10829
10922
  // リトライのショートカットキー変更
10830
10923
  createCss2Button(`scRetry`, getScMsg.Retry(), () => {
10831
- cursor.style.left = wUnit(g_btnX(5 / 8) + kcSubX);
10924
+ cursor.style.left = wUnit(g_btnX(1 / 2) + kcSubX);
10832
10925
  cursor.style.top = wUnit(g_sHeight - 160 + kcSubY);
10833
10926
  selectedKc = `Retry`;
10834
10927
  }, g_lblPosObj.scRetry, g_cssObj.button_Default_NoColor,
10835
10928
  g_headerObj.keyRetry === g_headerObj.keyRetryDef2 ?
10836
10929
  g_cssObj.title_base : g_cssObj.keyconfig_Changekey),
10837
10930
 
10931
+ // ポーズのショートカットキー変更
10932
+ createCss2Button(`scPause`, getScMsg.Pause(), () => {
10933
+ cursor.style.left = wUnit(g_btnX(3 / 4) + kcSubX);
10934
+ cursor.style.top = wUnit(g_sHeight - 160 + kcSubY);
10935
+ selectedKc = `Pause`;
10936
+ }, g_lblPosObj.scPause, g_cssObj.button_Default_NoColor,
10937
+ g_headerObj.keyPause === g_headerObj.keyPauseDef2 ?
10938
+ g_cssObj.title_base : g_cssObj.keyconfig_Changekey),
10939
+
10838
10940
  // 別キーモード警告メッセージ
10839
10941
  createDivCss2Label(
10840
10942
  `kcMsg`,
@@ -11342,8 +11444,8 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
11342
11444
  } else if ((keyIsDown(g_kCdNameObj.metaLKey) || keyIsDown(g_kCdNameObj.metaRKey)) && keyIsShift()) {
11343
11445
  return;
11344
11446
  }
11345
- if (selectedKc === `TitleBack` || selectedKc === `Retry`) {
11346
- // タイトルバックキー、リトライキーはプレイ中の操作キーと重複しないようにする(簡易的)
11447
+ if (selectedKc === `TitleBack` || selectedKc === `Retry` || selectedKc === `Pause`) {
11448
+ // タイトルバックキー、リトライキー、ポーズキーはプレイ中の操作キーと重複しないようにする(簡易的)
11347
11449
  if (g_keyObj[`keyCtrl${keyCtrlPtn}`].flat().includes(setKey)) {
11348
11450
  makeInfoWindow(g_msgInfoObj.I_0002, `fadeOut0`);
11349
11451
  return;
@@ -11353,10 +11455,10 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
11353
11455
  g_headerObj[`key${selectedKc}Def`] = setKey;
11354
11456
  document.getElementById(`sc${selectedKc}`).textContent = getScMsg[selectedKc]();
11355
11457
  document.getElementById(`sc${selectedKc}`).style.fontSize =
11356
- wUnit(getFontSize2(getScMsg[selectedKc](), g_btnWidth(5 / 12) - 40, { maxSiz: g_limitObj.mainSiz }));
11458
+ wUnit(getFontSize2(getScMsg[selectedKc](), g_btnWidth(1 / 4) - 40, { maxSiz: g_limitObj.mainSiz }));
11357
11459
  if (g_isMac) {
11358
11460
  scTitleBack.textContent = getScMsg.TitleBack();
11359
- scTitleBack.style.fontSize = wUnit(getFontSize2(getScMsg.TitleBack(), g_btnWidth(5 / 12) - 40, { maxSiz: g_limitObj.mainSiz }));
11461
+ scTitleBack.style.fontSize = wUnit(getFontSize2(getScMsg.TitleBack(), g_btnWidth(1 / 4) - 40, { maxSiz: g_limitObj.mainSiz }));
11360
11462
  }
11361
11463
  changeConfigColor(document.getElementById(`sc${selectedKc}`),
11362
11464
  g_headerObj[`key${selectedKc}`] === g_headerObj[`key${selectedKc}Def2`] ?
@@ -12087,9 +12189,9 @@ const keyconfigKeyboardPreview = (() => {
12087
12189
  _state.mappedSet = new Set(ctrl.map(arr => toCodeStr(arr[0])).filter(v => v !== ``));
12088
12190
  _state.altSet = new Set(ctrl.flatMap(arr => arr.slice(1)).map(toCodeStr).filter(v => v !== ``));
12089
12191
 
12090
- // プレイ中ショートカット: keyRetry / keyTitleBack は g_headerObj から取得、PgDn(34) / PgUp(33) は固定
12192
+ // プレイ中ショートカット: keyRetry / keyTitleBack / keyPause は g_headerObj から取得、PgDn(34) / PgUp(33) は固定
12091
12193
  _state.shortcutSet = new Set(
12092
- [g_headerObj.keyRetry, g_headerObj.keyTitleBack, 34, 33].map(toCodeStr).filter(v => v !== ``)
12194
+ [g_headerObj.keyRetry, g_headerObj.keyTitleBack, g_headerObj.keyPause, 34, 33].map(toCodeStr).filter(v => v !== ``)
12093
12195
  );
12094
12196
 
12095
12197
  if (_state.visible) drawMap();
@@ -12171,6 +12273,9 @@ const setPlayingShortcut = () => {
12171
12273
  if (g_headerObj.keyTitleBackDef === C_KEY_TITLEBACK) {
12172
12274
  g_headerObj.keyTitleBack = setIntVal(getKeyCtrlVal(g_keyObj[`keyTitleBack${keyCtrlPtn}`]), g_headerObj.keyTitleBackDef);
12173
12275
  }
12276
+ if (g_headerObj.keyPauseDef === C_KEY_PAUSE) {
12277
+ g_headerObj.keyPause = setIntVal(getKeyCtrlVal(g_keyObj[`keyPause${keyCtrlPtn}`]), g_headerObj.keyPauseDef);
12278
+ }
12174
12279
  };
12175
12280
 
12176
12281
  /**
@@ -14684,8 +14789,12 @@ const getArrowSettings = () => {
14684
14789
  g_workObj.arrowReturnVal = 0;
14685
14790
  g_gameOverFlg = false;
14686
14791
  g_finishFlg = true;
14687
- g_workObj.nonDefaultSc = g_headerObj.keyRetry !== C_KEY_RETRY || g_headerObj.keyTitleBack !== C_KEY_TITLEBACK;
14688
- if (g_headerObj.scAreaWidth === 0 && (g_headerObj.keyRetry !== g_headerObj.keyRetryDef2 || g_headerObj.keyTitleBack !== g_headerObj.keyTitleBackDef2)) {
14792
+ g_workObj.nonDefaultSc = g_headerObj.keyRetry !== C_KEY_RETRY || g_headerObj.keyTitleBack !== C_KEY_TITLEBACK || g_headerObj.keyPause !== C_KEY_PAUSE;
14793
+ if (g_headerObj.scAreaWidth === 0 && (
14794
+ g_headerObj.keyRetry !== g_headerObj.keyRetryDef2 ||
14795
+ g_headerObj.keyTitleBack !== g_headerObj.keyTitleBackDef2 ||
14796
+ g_headerObj.keyPause !== g_headerObj.keyPauseDef2
14797
+ )) {
14689
14798
  g_workObj.nonDefaultSc = false;
14690
14799
  }
14691
14800
  if (g_diffObj.shortcutX !== 0 || g_diffObj.shortcutY !== 0) {
@@ -15032,7 +15141,7 @@ const mainInit = () => {
15032
15141
 
15033
15142
  // 開始位置、楽曲再生位置の設定
15034
15143
  const firstFrame = g_scoreObj.frameNum;
15035
- const musicStartFrame = firstFrame + g_headerObj.blankFrame;
15144
+ let musicStartFrame = firstFrame + g_headerObj.blankFrame;
15036
15145
  const fadeFlgs = { fadein: [`In`, `Out`], fadeout: [`Out`, `In`] };
15037
15146
  g_audio.volume = (firstFrame === 0 ? g_stateObj.volume / 100 : 0);
15038
15147
 
@@ -15042,6 +15151,12 @@ const mainInit = () => {
15042
15151
  let musicStartTime;
15043
15152
  let musicStartCtxTime;
15044
15153
  let musicStartFlg = false;
15154
+ let isPaused = false; // 一時停止中かどうか(経路によらず共通のフラグ)
15155
+ let manualPauseFlg = false; // 手動ポーズによる一時停止かどうか(true時はvisibilitychangeでの自動再開をスキップ)
15156
+ let pausedElapsedTime = null; // [AudioPlayer/開始後のみ] 一時停止時点の再生位置(秒)
15157
+ let pausedStartAdjustment = null; // [AudioPlayer/開始前のみ] 開始予定までの残り時間(秒)
15158
+ let countdownTimeoutId = null; // 再開前カウントダウンのタイマーID。null=カウントダウン中でない
15159
+ const mySessionId = ++g_timelineSessionId; // このmainInit()実行を識別するID
15045
15160
 
15046
15161
  g_inputKeyBuffer = {};
15047
15162
 
@@ -15163,29 +15278,30 @@ const mainInit = () => {
15163
15278
  }
15164
15279
 
15165
15280
  if (g_workObj.nonDefaultSc) {
15281
+ const diffX = g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX;
15282
+ const diffY = g_headerObj.playingHeight + g_diffObj.shortcutY;
15166
15283
  multiAppend(infoSprite,
15167
15284
  createDivCss2Label(`lblRetry`, `[${g_lblNameObj.l_retry}]`, {
15168
- ...g_lblPosObj.lblMainScHeader,
15169
- x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
15170
- y: g_headerObj.playingHeight - 65 + g_diffObj.shortcutY,
15285
+ ...g_lblPosObj.lblMainScHeader, x: diffX, y: diffY - 95,
15171
15286
  }),
15172
15287
  createDivCss2Label(`lblRetrySc`, g_kCd[g_headerObj.keyRetry], {
15173
- ...g_lblPosObj.lblMainScKey,
15174
- x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
15175
- y: g_headerObj.playingHeight - 50 + g_diffObj.shortcutY,
15288
+ ...g_lblPosObj.lblMainScKey, x: diffX, y: diffY - 80,
15176
15289
  fontWeight: g_headerObj.keyRetry === C_KEY_RETRY ? `normal` : `bold`,
15177
15290
  }),
15178
15291
  createDivCss2Label(`lblTitleBack`, `[${g_lblNameObj.l_titleBack}]`, {
15179
- ...g_lblPosObj.lblMainScHeader,
15180
- x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
15181
- y: g_headerObj.playingHeight - 35 + g_diffObj.shortcutY,
15292
+ ...g_lblPosObj.lblMainScHeader, x: diffX, y: diffY - 65,
15182
15293
  }),
15183
15294
  createDivCss2Label(`lblTitleBackSc`, g_isMac ? `Shift+${g_kCd[g_headerObj.keyRetry]}` : g_kCd[g_headerObj.keyTitleBack], {
15184
- ...g_lblPosObj.lblMainScKey,
15185
- x: g_sWidth + g_headerObj.scAreaWidth - 85 + g_diffObj.shortcutX,
15186
- y: g_headerObj.playingHeight - 20 + g_diffObj.shortcutY,
15295
+ ...g_lblPosObj.lblMainScKey, x: diffX, y: diffY - 50,
15187
15296
  fontWeight: g_headerObj.keyTitleBack === C_KEY_TITLEBACK ? `normal` : `bold`,
15188
15297
  }),
15298
+ createDivCss2Label(`lblPause`, `[${g_lblNameObj.l_pause}]`, {
15299
+ ...g_lblPosObj.lblMainScHeader, x: diffX, y: diffY - 35,
15300
+ }),
15301
+ createDivCss2Label(`lblPauseSc`, g_kCd[g_headerObj.keyPause], {
15302
+ ...g_lblPosObj.lblMainScKey, x: diffX, y: diffY - 20,
15303
+ fontWeight: g_headerObj.keyPause === C_KEY_PAUSE ? `normal` : `bold`,
15304
+ }),
15189
15305
  );
15190
15306
  }
15191
15307
 
@@ -15340,6 +15456,13 @@ const mainInit = () => {
15340
15456
  if (evt.repeat && !g_mainRepeatObj.key.includes(setCode)) {
15341
15457
  return blockCode(setCode);
15342
15458
  }
15459
+ if (setCode === g_kCdN[g_headerObj.keyPause]) {
15460
+ isPaused ? resumeTimeline(true) : pauseTimeline(true);
15461
+ return blockCode(setCode);
15462
+ }
15463
+ if (isPaused) {
15464
+ return blockCode(setCode);
15465
+ }
15343
15466
  g_inputKeyBuffer[setCode] = true;
15344
15467
  mainKeyDownActFunc[g_stateObj.autoAll](setCode);
15345
15468
 
@@ -16302,6 +16425,149 @@ const mainInit = () => {
16302
16425
  Math.min(Math.max(1000 / g_fps - buffTime, 0), g_maxFrameWait));
16303
16426
  }
16304
16427
  };
16428
+
16429
+ /*
16430
+ * タブのバックグラウンド化に伴う一時停止/再開
16431
+ * - 【既知の制限】movLock/initManual設定(movArrowYがCSS animation任せになるケース)では、
16432
+ * 矢印移動そのものがCSS animation駆動になるため、この一時停止機能はバックグラウンド中の
16433
+ * 一時停止を保証できない(演出効果と異なり判定位置に直結するため実プレイには不適)。
16434
+ * 対処は行わず、既知の制限として明記するに留める。
16435
+ */
16436
+ const cancelResumeCountdown = () => {
16437
+ if (countdownTimeoutId !== null) {
16438
+ clearTimeout(countdownTimeoutId);
16439
+ countdownTimeoutId = null;
16440
+ }
16441
+ document.getElementById(`lblResumeCountdown`)?.remove();
16442
+ };
16443
+
16444
+ let nativeAudioWasPlaying = false;
16445
+
16446
+ const pauseTimeline = (_manual = false) => {
16447
+ // カウントダウン中に再度バックグラウンド化した場合は、カウントダウンのみ中断する
16448
+ // (音源は既に停止済み・gamePausedも付与済みのため、以降の処理は不要)
16449
+ cancelResumeCountdown();
16450
+ if (isPaused) {
16451
+ if (_manual) {
16452
+ // カウントダウン中に手動ポーズキーが押された場合、手動扱いに切り替える
16453
+ manualPauseFlg = true;
16454
+ }
16455
+ return;
16456
+ }
16457
+ isPaused = true;
16458
+ manualPauseFlg = _manual;
16459
+ pausedElapsedTime = null;
16460
+ pausedStartAdjustment = null;
16461
+ if (document.getElementById(`lblPauseMark`) == null) {
16462
+ const countdownLabel = createDivCss2Label(`lblPauseMark`, `Pause`, {
16463
+ ...g_lblPosObj.lblPauseMark, x: g_workObj.playingX,
16464
+ });
16465
+ divRoot.appendChild(countdownLabel);
16466
+ }
16467
+
16468
+ if (g_audio instanceof AudioPlayer) {
16469
+ if (g_audio.contextTime < g_audio.scheduledTime) {
16470
+ // 音源開始前は、再生位置ではなく開始予定時刻までの残り時間を保持する。
16471
+ // g_audio.play() は g_scheduleLead を加算するため、その分を除く。
16472
+ pausedStartAdjustment = g_audio.scheduledTime
16473
+ - g_audio.contextTime - g_scheduleLead;
16474
+ } else {
16475
+ // 音源開始後: 再生位置を保存する。
16476
+ pausedElapsedTime = g_audio.elapsedTime - g_scheduleLead;
16477
+ }
16478
+ } else {
16479
+ // `paused` が false の場合だけ、再開時に play() を呼ぶ
16480
+ nativeAudioWasPlaying = !g_audio.paused;
16481
+ }
16482
+ clearTimeout(g_timeoutEvtId);
16483
+ g_audio.pause();
16484
+
16485
+ // フォーカスを失うとkeyupが届かなくなり、押しっぱなし判定・表示が残り得るため、
16486
+ // ここで強制的に全キー「離した」状態に戻す
16487
+ g_inputKeyBuffer = {};
16488
+ g_workObj.keyHitFlg.forEach(lane => lane.fill(false));
16489
+ mainKeyUpActFunc[g_stateObj.autoAll]();
16490
+ divRoot.classList.add(`gamePaused`);
16491
+ };
16492
+
16493
+ const resumeTimeline = (_manual = false) => {
16494
+ if (!isPaused || countdownTimeoutId !== null) {
16495
+ return;
16496
+ }
16497
+ if (!_manual && manualPauseFlg) {
16498
+ // 手動ポーズ中は、タブ復帰による自動再開をスキップする
16499
+ if (document.getElementById(`lblPauseMark`) == null) {
16500
+ const countdownLabel = createDivCss2Label(`lblPauseMark`, `Pause`, {
16501
+ ...g_lblPosObj.lblPauseMark, x: g_workObj.playingX,
16502
+ });
16503
+ divRoot.appendChild(countdownLabel);
16504
+ }
16505
+ return;
16506
+ }
16507
+ deleteDiv(divRoot, `lblPauseMark`);
16508
+ const countdownLabel = createDivCss2Label(`lblResumeCountdown`, ``, {
16509
+ ...g_lblPosObj.lblPauseMark, x: g_workObj.playingX,
16510
+ });
16511
+ divRoot.appendChild(countdownLabel);
16512
+
16513
+ const finishResume = () => {
16514
+ countdownTimeoutId = null;
16515
+ countdownLabel.remove();
16516
+
16517
+ // カウントダウン中に曲中リトライ等でmainInit()が再実行されたりプレイ画面外に移動した場合、
16518
+ // 古いセッションのfinishResumeが後から発火して二重再生・二重ループを起こすのを防ぐ
16519
+ if (mySessionId !== g_timelineSessionId || g_currentPage !== `main`) {
16520
+ return;
16521
+ }
16522
+
16523
+ if (g_audio instanceof AudioPlayer) {
16524
+ // バックグラウンド中にsuspendされていた場合に備え、先に明示的にresumeを試みる
16525
+ getSharedAudioContext();
16526
+
16527
+ if (pausedStartAdjustment !== null) {
16528
+ // 開始前: 音源と譜面の残り開始時間を両方維持する。
16529
+ g_audio.play(pausedStartAdjustment);
16530
+ musicStartCtxTime = g_audio.scheduledTime
16531
+ + (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
16532
+ // musicStartFrame は変更しない。
16533
+ } else {
16534
+ // 開始後: 再生位置から再開し、現在フレームを基準に再アンカーする。
16535
+ g_audio.currentTime = pausedElapsedTime;
16536
+ g_audio.play();
16537
+ musicStartCtxTime = g_audio.scheduledTime
16538
+ + (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
16539
+ musicStartFrame = g_scoreObj.frameNum;
16540
+ }
16541
+
16542
+ } else {
16543
+ if (nativeAudioWasPlaying) {
16544
+ g_audio.play();
16545
+ }
16546
+ }
16547
+ // バックグラウンド経過時間をフレーム計算から除外する
16548
+ musicStartTime = performance.now() - (g_scoreObj.frameNum - musicStartFrame) * 1000 / g_fps;
16549
+
16550
+ divRoot.classList.remove(`gamePaused`);
16551
+ isPaused = false;
16552
+ manualPauseFlg = false;
16553
+ pausedElapsedTime = null;
16554
+ pausedStartAdjustment = null;
16555
+ g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
16556
+ };
16557
+
16558
+ const tick = _remaining => {
16559
+ if (_remaining <= 0) {
16560
+ finishResume();
16561
+ return;
16562
+ }
16563
+ countdownLabel.innerHTML = String(_remaining);
16564
+ countdownTimeoutId = setTimeout(() => tick(_remaining - 1), 1000);
16565
+ };
16566
+ tick(3);
16567
+ };
16568
+
16569
+ g_timelineHooks.pause = pauseTimeline;
16570
+ g_timelineHooks.resume = resumeTimeline;
16305
16571
  safeExecuteCustomHooks(`g_skinJsObj.main`, g_skinJsObj.main);
16306
16572
 
16307
16573
  g_audio.currentTime = firstFrame / g_fps * g_headerObj.playbackRate;
@@ -16319,7 +16585,11 @@ const mainInit = () => {
16319
16585
  + (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
16320
16586
  }
16321
16587
 
16322
- g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
16588
+ if (document.hidden) {
16589
+ pauseTimeline();
16590
+ } else {
16591
+ g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
16592
+ }
16323
16593
  };
16324
16594
 
16325
16595
  /**
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2019/11/19
8
- * Revised : 2026/08/16 (v49.6.0)
8
+ * Revised : 2026/08/29 (v50.1.0)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -186,6 +186,7 @@ const g_lblPosObj = {};
186
186
  const getScMsg = {
187
187
  TitleBack: () => g_lblNameObj.kcShortcutDesc1.split(`{0}`).join(g_isMac ? `Shift+${g_kCd[g_headerObj.keyRetry]}` : g_kCd[g_headerObj.keyTitleBack]),
188
188
  Retry: () => g_lblNameObj.kcShortcutDesc2.split(`{1}`).join(g_kCd[g_headerObj.keyRetry]),
189
+ Pause: () => g_lblNameObj.kcShortcutDesc3.split(`{2}`).join(g_kCd[g_headerObj.keyPause]),
189
190
  };
190
191
 
191
192
  /**
@@ -676,11 +677,15 @@ const updateWindowSiz = () => {
676
677
  },
677
678
  scTitleBack: {
678
679
  x: g_btnX(1 / 4) + 20, y: g_sHeight - 50, align: C_ALIGN_LEFT,
679
- w: g_btnWidth(5 / 12) - 40, h: C_KYC_REPHEIGHT, siz: getFontSize2(getScMsg.TitleBack(), g_btnWidth(5 / 12) - 40, { maxSiz: 13 }),
680
+ w: g_btnWidth(1 / 4) - 40, h: C_KYC_REPHEIGHT, siz: getFontSize2(getScMsg.TitleBack(), g_btnWidth(1 / 4) - 40, { maxSiz: 13 }),
680
681
  },
681
682
  scRetry: {
682
- x: g_btnX(5 / 8) + 20, y: g_sHeight - 50, align: C_ALIGN_LEFT,
683
- w: g_btnWidth(5 / 12) - 40, h: C_KYC_REPHEIGHT, siz: getFontSize2(getScMsg.Retry(), g_btnWidth(5 / 12) - 40, getBasicFont(), { maxSiz: 13 }),
683
+ x: g_btnX(1 / 2) + 20, y: g_sHeight - 50, align: C_ALIGN_LEFT,
684
+ w: g_btnWidth(1 / 4) - 40, h: C_KYC_REPHEIGHT, siz: getFontSize2(getScMsg.Retry(), g_btnWidth(1 / 4) - 40, { maxSiz: 13 }),
685
+ },
686
+ scPause: {
687
+ x: g_btnX(3 / 4) + 20, y: g_sHeight - 50, align: C_ALIGN_LEFT,
688
+ w: g_btnWidth(1 / 4) - 40, h: C_KYC_REPHEIGHT, siz: getFontSize2(getScMsg.Pause(), g_btnWidth(1 / 4) - 40, { maxSiz: 13 }),
684
689
  },
685
690
 
686
691
  /** メイン画面 */
@@ -739,6 +744,10 @@ const updateWindowSiz = () => {
739
744
  lblMainScKey: {
740
745
  x: g_sWidth + g_headerObj.scAreaWidth - 85, w: 80, h: 20, siz: 12, align: C_ALIGN_RIGHT,
741
746
  },
747
+ lblPauseMark: {
748
+ y: g_headerObj.playingY + (g_headerObj.playingHeight + g_posObj.stepYR) / 2 - 75,
749
+ w: g_sWidth, h: 50, siz: 40, background: `#000000cc`, align: C_ALIGN_CENTER,
750
+ },
742
751
 
743
752
  /** 結果画面 */
744
753
  lblRank: {
@@ -1126,6 +1135,7 @@ const C_FRM_FRZATTEMPT = 5;
1126
1135
  /** ショートカットキー */
1127
1136
  const C_KEY_RETRY = 8;
1128
1137
  const C_KEY_TITLEBACK = 46;
1138
+ const C_KEY_PAUSE = 19;
1129
1139
 
1130
1140
  /** 判定系共通オブジェクト */
1131
1141
  const g_judgObj = {
@@ -4841,6 +4851,7 @@ const g_lang_lblNameObj = {
4841
4851
  kcShortcutDesc: `プレイ中ショートカット:`,
4842
4852
  kcShortcutDesc1: `タイトルバック: {0}`,
4843
4853
  kcShortcutDesc2: `リトライ: {1}`,
4854
+ kcShortcutDesc3: `ポーズ: {2}`,
4844
4855
  transKeyDesc: `別キーモードではキーコンフィグ、ColorType等は保存されません`,
4845
4856
  colorTypeDesc: `現在のColorTypeの設定では、色変化(Display:Color)は自動的にOFFになります`,
4846
4857
  sdShortcutDesc: `Hid+/Sud+時ショートカット:「pageUp」カバーを上へ / 「pageDown」下へ`,
@@ -4888,6 +4899,7 @@ const g_lang_lblNameObj = {
4888
4899
 
4889
4900
  l_retry: `リトライ`,
4890
4901
  l_titleBack: `タイトルバック`,
4902
+ l_pause: `ポーズ`,
4891
4903
 
4892
4904
  helpUrl: `https://github.com/cwtickle/danoniplus/wiki/AboutGameSystem`,
4893
4905
  securityUrl: `https://github.com/cwtickle/danoniplus/security/policy`,
@@ -4905,6 +4917,7 @@ const g_lang_lblNameObj = {
4905
4917
  kcShortcutDesc: `Shortcut during play:`,
4906
4918
  kcShortcutDesc1: `Return to title: {0}`,
4907
4919
  kcShortcutDesc2: `Retry the game: {1}`,
4920
+ kcShortcutDesc3: `Pause: {2}`,
4908
4921
  transKeyDesc: `Key config, Color type, etc. are not saved for alternate keymode`,
4909
4922
  colorTypeDesc: `With the current ColorType setting, color change (Display:Color) will be automatically turned OFF.`,
4910
4923
  sdShortcutDesc: `When "Hidden+" or "Sudden+" select, "pageUp" cover up / "pageDown" cover down`,
@@ -4952,6 +4965,7 @@ const g_lang_lblNameObj = {
4952
4965
 
4953
4966
  l_retry: `Retry`,
4954
4967
  l_titleBack: `Go to title`,
4968
+ l_pause: `Pause`,
4955
4969
 
4956
4970
  helpUrl: `https://github.com/cwtickle/danoniplus-docs/wiki/AboutGameSystem`,
4957
4971
  securityUrl: `https://github.com/cwtickle/danoniplus-docs/wiki/SecurityPolicy`,
@@ -5273,6 +5287,27 @@ const g_skinJsObj = {
5273
5287
  result: [],
5274
5288
  };
5275
5289
 
5290
+ const g_root = {
5291
+ get g_diffObj() { return g_diffObj; },
5292
+ get g_posObj() { return g_posObj; },
5293
+ get g_headerObj() { return g_headerObj; },
5294
+ get g_stateObj() { return g_stateObj; },
5295
+ get g_keycons() { return g_keycons; },
5296
+ };
5297
+ const getPathVal = _path => _path.split(`.`).reduce((o, k) => o?.[k], g_root);
5298
+ const setPathVal = (_path, _value) => {
5299
+ const keys = _path.split(`.`);
5300
+ const last = keys.pop();
5301
+ const target = keys.reduce((o, k) => o?.[k], g_root);
5302
+ if (target == null || !Object.hasOwn(target, last)) {
5303
+ console.warn(`setPathVal: path not found for "${_path}"`);
5304
+ return;
5305
+ }
5306
+ target[last] = _value;
5307
+ };
5308
+
5309
+ const g_familyObj = { families: [], currentFamily: null, ownership: {}, snapshot: {}, applied: {}, appliedPaths: new Set() };
5310
+
5276
5311
  const g_errorCache = {
5277
5312
  'g_customJsObj.titleEnterFrame': [],
5278
5313
  'g_customJsObj.mainEnterFrame': [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "49.6.1",
3
+ "version": "50.1.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",