danoniplus 52.0.0 → 52.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2026/09/13
8
- * Revised : 2026/11/01 (v52.0.0)
8
+ * Revised : 2026/11/08 (v52.2.0)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -625,6 +625,7 @@ const drawDensityGraph = _scoreId => {
625
625
  updateScoreDetailLabel(`Density`, `Arrow`, obj.arrowCnts, 3, g_lblNameObj.s_arrow);
626
626
  updateScoreDetailLabel(`Density`, `Frz`, obj.frzCnts, 4,
627
627
  `${g_lblNameObj.s_frz}${g_headerObj.frzStartjdgUse ? ' <span class="common_bold">(2x)</span>' : ''}`);
628
+ updateScoreDetailLabel(`Density`, `Ring`, g_detailObj.ringCnt[_scoreId], 5, g_lblNameObj.s_ring);
628
629
  };
629
630
 
630
631
  /**
@@ -734,11 +735,13 @@ const makeDifInfoLabels = _scoreId => {
734
735
  // 縦連
735
736
  `${g_detailObj.toolDif[j].tate}\t` +
736
737
  // 総矢印数
737
- `${(obj.arrowCnts + obj.frzCnts)}\t` +
738
+ `${(obj.arrowCnts + obj.frzCnts + g_detailObj.ringCnt[j])}\t` +
738
739
  // 矢印
739
740
  `${obj.arrowCnts}\t` +
740
741
  // フリーズアロー
741
742
  `${obj.frzCnts}\t` +
743
+ // リングノート
744
+ `${g_detailObj.ringCnt[j]}\t` +
742
745
  // APM
743
746
  `${obj.apm}\t` +
744
747
  // 時間(分秒)
@@ -787,9 +790,10 @@ const makeDifInfo = _scoreId => {
787
790
  lblArrowInfo2.innerHTML = g_lblNameObj.s_linecnts.split(`{0}`)
788
791
  .join(`${makeDedupliArray(g_detailObj.toolDif[_scoreId].push3).length} /cnt:${g_detailObj.toolDif[_scoreId].push3cnt}`);
789
792
  dataArrowInfo.innerHTML =
790
- `${arrowCnts + frzCnts * (g_headerObj.frzStartjdgUse ? 2 : 1)} ` +
793
+ `${arrowCnts + frzCnts * (g_headerObj.frzStartjdgUse ? 2 : 1) + g_detailObj.ringCnt[_scoreId]} ` +
791
794
  `<span style="font-size:${wUnit(g_limitObj.difSelectorSiz)};">` +
792
- `(${arrowCnts} + ${frzCnts}${g_headerObj.frzStartjdgUse ? ' <span class="common_bold">x 2</span>' : ''})` +
795
+ `(${arrowCnts} + ${frzCnts}${g_headerObj.frzStartjdgUse ? ' <span class="common_bold">x 2</span>' : ''}` +
796
+ `${hasRingNotes() ? ` + ${g_detailObj.ringCnt[_scoreId]}` : ''})` +
793
797
  `</span>`;
794
798
 
795
799
  const makeArrowCntsView = (_cntlist) => {
@@ -917,25 +921,25 @@ const makeHighScore = _scoreId => {
917
921
  color: g_localStorage.highscores?.[scoreName]?.rankColor ?? `#666666`,
918
922
  fontFamily: getBasicFont(`"Bookman Old Style"`),
919
923
  }),
920
- createScoreLabel(`lblHDateTime`, g_localStorage.highscores?.[scoreName]?.dateTime ?? `----/--/-- --:--`, { yPos: 12 }),
921
- createScoreLabel(`lblHMarks`,
924
+ createScoreLabel(`dateTime`, g_localStorage.highscores?.[scoreName]?.dateTime ?? `----/--/-- --:--`, { yPos: 12 }),
925
+ createScoreLabel(`marks`,
922
926
  `${g_localStorage.highscores?.[scoreName]?.fullCombo ?? '' ? '<span class="result_FullCombo">◆</span>' : ''}` +
923
927
  `${g_localStorage.highscores?.[scoreName]?.perfect ?? '' ? '<span class="result_Perfect">◆</span>' : ''}` +
924
928
  `${g_localStorage.highscores?.[scoreName]?.allPerfect ?? '' ? '<span class="result_AllPerfect">◆</span>' : ''}`,
925
929
  { xPos: 1, dx: 20, yPos: 12, w: 100, align: C_ALIGN_CENTER }),
926
- createScoreLabel(`lblHClearLamps`,
930
+ createScoreLabel(`clearLamps`,
927
931
  `Cleared: ` + (g_localStorage.highscores?.[scoreName]?.clearLamps?.join(', ') ?? C_FLG_HYPHEN),
928
932
  { yPos: 13, overflow: C_DIS_AUTO, w: g_sWidth / 2 + 40, h: 37 }),
929
933
 
930
- createScoreLabel(`lblHShuffle`,
934
+ createScoreLabel(`shuffleS`,
931
935
  g_stateObj.shuffle.indexOf(`Mirror`) < 0
932
936
  ? `` : `Shuffle: <span class="common_shuffle">${g_stateObj.shuffle}</span>`,
933
937
  { yPos: 11.5, dx: -130 }),
934
- createScoreLabel(`lblHAssist`,
938
+ createScoreLabel(`assistS`,
935
939
  g_autoPlaysBase.includes(g_stateObj.autoPlay)
936
940
  ? `` : `Assist: <span class="common_assist">${g_stateObj.autoPlay}</span>`,
937
941
  { yPos: 12.5, dx: -130 }),
938
- createScoreLabel(`lblHAnother`,
942
+ createScoreLabel(`anotherS`,
939
943
  !hasVal(g_keyObj[`transKey${keyCtrlPtn}`])
940
944
  ? `` : `A.Keymode: <span class="common_another">${g_keyObj[`transKey${keyCtrlPtn}`]}</span>`,
941
945
  { yPos: 13.5, dx: -130 }),
@@ -1031,9 +1035,11 @@ const drawMinimap = (_scoreId, { _initFlg = false, _fadeinFlg = false } = {}) =>
1031
1035
 
1032
1036
  const params = g_detailObj.miniMapParams[_scoreId];
1033
1037
  const kPtn = params._keyCtrlPtn;
1034
- if (!g_detailObj.scoreMinimapHeader[kPtn]) {
1035
- // ヘッダーはキー種ごとに共通なので、未作成の場合のみ生成してキャッシュ
1036
- g_detailObj.scoreMinimapHeader[kPtn] = createMinimapHeader(params.config, kPtn, params._keyNum);
1038
+ // リング帯の有無で幅が変わるため、キー種 + リング帯有無でキャッシュする
1039
+ const headerKey = params.config.ringBandWidth > 0 ? `${kPtn}_ring` : kPtn;
1040
+ if (!g_detailObj.scoreMinimapHeader[headerKey]) {
1041
+ // ヘッダーはキー種(+リング帯有無)ごとに共通なので、未作成の場合のみ生成してキャッシュ
1042
+ g_detailObj.scoreMinimapHeader[headerKey] = createMinimapHeader(params.config, kPtn, params._keyNum);
1037
1043
  }
1038
1044
  if (!savedCanvases) {
1039
1045
  // 未作成の場合のみミニマップを生成(Lazy Generation)
@@ -1050,7 +1056,7 @@ const drawMinimap = (_scoreId, { _initFlg = false, _fadeinFlg = false } = {}) =>
1050
1056
  // --- ヘッダー部分 ---
1051
1057
  const detailMiniMapHeader = createEmptySprite(detailMiniMap, `detailMiniMapHeader`, g_windowObj.detailMiniMapHeader);
1052
1058
  $id(`detailMiniMapHeader`).top = wUnit(g_stateObj.miniMapRevFlg ? 230 + g_sHeight - 500 : 0);
1053
- detailMiniMapHeader.appendChild(g_detailObj.scoreMinimapHeader[kPtn]);
1059
+ detailMiniMapHeader.appendChild(g_detailObj.scoreMinimapHeader[headerKey]);
1054
1060
 
1055
1061
  // --- メイン(譜面)部分 ---
1056
1062
  const detailMiniMapSub = createEmptySprite(detailMiniMap, `detailMiniMapSub`, g_windowObj.detailMiniMapSub);
@@ -1128,7 +1134,7 @@ const drawMinimap = (_scoreId, { _initFlg = false, _fadeinFlg = false } = {}) =>
1128
1134
  * @return {HTMLCanvasElement} ヘッダー用のキャンバス要素
1129
1135
  */
1130
1136
  const createMinimapHeader = (_config, _keyCtrlPtn, _keyNum) => {
1131
- const { timeMargin, laneWidth, logicalWidth } = _config;
1137
+ const { timeMargin, laneWidth, logicalWidth, ringBandWidth = 0 } = _config;
1132
1138
  const headerHeight = 15; // ヘッダーの固定高
1133
1139
 
1134
1140
  const canvas = createCanvas(null, { w: logicalWidth, h: headerHeight, position: null });
@@ -1143,6 +1149,12 @@ const createMinimapHeader = (_config, _keyCtrlPtn, _keyNum) => {
1143
1149
  // 視覚的な中央調整で +2px
1144
1150
  fillCanvasText(ctx, keyText, x, headerHeight / 2 + 2, { siz: 10, color: '#999', align: C_ALIGN_CENTER, baseline: `middle` });
1145
1151
  }
1152
+ // リングノート帯の見出し
1153
+ if (ringBandWidth > 0) {
1154
+ const bandX = timeMargin + laneWidth * _keyNum + 4;
1155
+ fillCanvasText(ctx, `Ring`, bandX + (ringBandWidth - 4) / 2, headerHeight / 2 + 2,
1156
+ { siz: 10, color: '#999', align: C_ALIGN_CENTER, baseline: `middle` });
1157
+ }
1146
1158
 
1147
1159
  return canvas;
1148
1160
  };
@@ -1166,7 +1178,7 @@ const createMinimapHeader = (_config, _keyCtrlPtn, _keyNum) => {
1166
1178
  */
1167
1179
  const generateMinimapData = (_params, _isReverse) => {
1168
1180
  const { _scoreObj, _keyNum, _playingFrame, _firstArrowFrame, _keyCtrlPtn, config } = _params;
1169
- const { scale, timeMargin, laneWidth, logicalWidth, mmMarginY } = config;
1181
+ const { scale, timeMargin, laneWidth, logicalWidth, mmMarginY, ringBandWidth = 0 } = config;
1170
1182
 
1171
1183
  const mmHeightTotal = _playingFrame * scale + mmMarginY * 2;
1172
1184
  const canvases = createSplitCanvases(logicalWidth, mmHeightTotal);
@@ -1234,6 +1246,46 @@ const generateMinimapData = (_params, _isReverse) => {
1234
1246
  });
1235
1247
  }
1236
1248
 
1249
+ // 4. リングノート (レーンを持たないため、右側の専用帯に出現フレームのみ表示)
1250
+ // 座標は使わない。同一フレームに複数ある場合は帯内で横に散らして重なりを避ける
1251
+ if (ringBandWidth > 0 && _scoreObj.ringData !== undefined) {
1252
+ const bandX = timeMargin + laneWidth * _keyNum + 4;
1253
+ const bandW = ringBandWidth - 6;
1254
+ const cols = g_ringObj.cols;
1255
+
1256
+ // 帯の背景
1257
+ distributeDrawing(canvases, 0, mmHeightTotal, g_dpr, (ctx) => {
1258
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
1259
+ ctx.fillRect(bandX, 0, bandW, mmHeightTotal);
1260
+ });
1261
+
1262
+ // フレーム毎の出現数
1263
+ const frameCnts = new Map();
1264
+ for (let k = 0; k < _scoreObj.ringData.length; k += cols) {
1265
+ const frame = parseFloat(_scoreObj.ringData[k]);
1266
+ if (!isNaN(frame)) {
1267
+ frameCnts.set(frame, (frameCnts.get(frame) ?? 0) + 1);
1268
+ }
1269
+ }
1270
+
1271
+ const seen = new Map();
1272
+ for (let k = 0; k < _scoreObj.ringData.length; k += cols) {
1273
+ const frame = parseFloat(_scoreObj.ringData[k]);
1274
+ if (isNaN(frame)) continue;
1275
+
1276
+ const idx = seen.get(frame) ?? 0;
1277
+ seen.set(frame, idx + 1);
1278
+ const x = bandX + bandW * (idx + 1) / (frameCnts.get(frame) + 1);
1279
+ const y = getY(frame);
1280
+ distributeDrawing(canvases, y - 4, 8, g_dpr, (ctx) => {
1281
+ ctx.fillStyle = 'rgba(255, 170, 60, 0.8)';
1282
+ ctx.beginPath();
1283
+ ctx.arc(x, y, 3, 0, Math.PI * 2);
1284
+ ctx.fill();
1285
+ });
1286
+ }
1287
+ }
1288
+
1237
1289
  return canvases.map(item => item.canvas);
1238
1290
  };
1239
1291
 
@@ -1828,7 +1880,7 @@ const createOptionWindow = _sprite => {
1828
1880
 
1829
1881
  // ---------------------------------------------------
1830
1882
  // タイミング調整 (Adjustment)
1831
- // 縦位置: 10.5 短縮ショートカットあり
1883
+ // 縦位置: 10.6 短縮ショートカットあり
1832
1884
  createGeneralSetting(spriteList.adjustment, `adjustment`, {
1833
1885
  skipTerms: g_settings.adjustmentTerms, hiddenBtn: true, scLabel: g_lblNameObj.sc_adjustment, roundNum: 5,
1834
1886
  unitName: g_lblNameObj.frame, addRFunc: () => viewAdjustment(),
@@ -1857,7 +1909,7 @@ const createOptionWindow = _sprite => {
1857
1909
 
1858
1910
  // ---------------------------------------------------
1859
1911
  // フェードイン (Fadein)
1860
- // 縦位置: 11.5 スライダーあり
1912
+ // 縦位置: 11.6 スライダーあり
1861
1913
  spriteList.fadein.appendChild(createLblSetting(`Fadein`));
1862
1914
 
1863
1915
  const lnkFadein = createDivCss2Label(`lnkFadein`, `${g_stateObj.fadein}${g_lblNameObj.percent}`,
@@ -1893,7 +1945,7 @@ const createOptionWindow = _sprite => {
1893
1945
 
1894
1946
  // ---------------------------------------------------
1895
1947
  // ボリューム (Volume)
1896
- // 縦位置: 12.5
1948
+ // 縦位置: 12.6
1897
1949
  createGeneralSetting(spriteList.volume, `volume`, { unitName: g_lblNameObj.percent });
1898
1950
 
1899
1951
  // 譜面番号の再取得
@@ -2051,9 +2103,15 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2051
2103
  if (![`option`, `exSetting`, `settingsDisplay`].includes(g_currentPage)) return;
2052
2104
 
2053
2105
  // 現在値は、g_currentPage を切り替える前に取得する(getStgDetailName が画面名を参照するため)
2054
- const currentVal = g_stateObj[_stateKey];
2055
- const currentNames = [currentVal, getStgDetailName(currentVal)];
2056
- const optNames = new Map((g_settings[`${_stateKey}s`] ?? []).map(val => [val, [val, getStgDetailName(val)]]));
2106
+ const sub = g_helpPreviewDefs[_stateKey]?.sub || (_stateKey + `Type`);
2107
+ const subList = sub !== undefined ? (g_settings[`${sub}s`] ?? []) : [];
2108
+ const currentVals = [g_stateObj[_stateKey], ...(subList.length > 0 ? [g_stateObj[sub]] : [])];
2109
+ const currentNames = currentVals.flatMap(val => [val, getStgDetailName(val)]);
2110
+ const optNames = new Map([...(g_settings[`${_stateKey}s`] ?? []), ...subList]
2111
+ .map(val => [val, [val, getStgDetailName(val)]]));
2112
+
2113
+ // Typeがある設定は、本体の説明文の後にTypeの説明文を続ける
2114
+ const helpMsg = [_msg, ...(subList.length > 0 ? [g_msgObj[sub]] : [])].filter(hasVal).join(`\n\n`);
2057
2115
  const rowElms = [];
2058
2116
  const bright = g_headerObj.baseBrightFlg;
2059
2117
 
@@ -2080,7 +2138,7 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2080
2138
  });
2081
2139
  g_handler.addListener(body, `click`, evt => evt.stopPropagation());
2082
2140
 
2083
- parseHelpLines(_msg).forEach(line => {
2141
+ parseHelpLines(helpMsg).forEach(line => {
2084
2142
  const row = document.createElement(`div`);
2085
2143
  Object.assign(row.style, {
2086
2144
  fontFamily: getBasicFont(), fontSize: wUnit(14), lineHeight: `1.1`, textAlign: `left`,
@@ -2109,12 +2167,13 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2109
2167
  });
2110
2168
 
2111
2169
  // 説明文の選択行 (背景) をプレビューのカーソルに追従させる
2112
- const markHelpRows = _val => {
2113
- const names = optNames.get(_val) ?? [_val];
2170
+ const markHelpRows = (_vals, _focus = 0) => {
2171
+ const names = _vals.flatMap(val => optNames.get(val) ?? [val]);
2172
+ const focusNames = optNames.get(_vals[_focus]) ?? [_vals[_focus]];
2114
2173
  rowElms.forEach(({ nameList, row }) => {
2115
2174
  const on = nameList.some(name => names.includes(name));
2116
2175
  row.style.background = on ? `rgba(80,140,255,0.25)` : ``;
2117
- if (on) {
2176
+ if (nameList.some(name => focusNames.includes(name))) {
2118
2177
  // 本文がスクロールする場合は、選択行が見える位置へ寄せる
2119
2178
  const [top, bottom] = [row.offsetTop, row.offsetTop + row.offsetHeight];
2120
2179
  if (top < body.scrollTop) {
@@ -2125,7 +2184,7 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2125
2184
  }
2126
2185
  });
2127
2186
  };
2128
- markHelpRows(currentVal);
2187
+ markHelpRows(currentVals);
2129
2188
  g_helpSelectHook = markHelpRows;
2130
2189
 
2131
2190
  // 上下キー: プレビューの選択肢移動、または本文のスクロール
@@ -2150,10 +2209,25 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2150
2209
  createCss2Button(`btnHelpNext`, ``, () => true, {
2151
2210
  x: 0, y: 0, w: 0, h: 0, opacity: 0, resetFunc: () => moveHelp(1),
2152
2211
  }, g_cssObj.button_Mini),
2212
+
2213
+ // 操作対象の列の切り替え (本体 / Type)
2214
+ createCss2Button(`btnHelpLeft`, ``, () => true, {
2215
+ x: 0, y: 0, w: 0, h: 0, opacity: 0, resetFunc: () => g_helpPreviewColumn(-1),
2216
+ }, g_cssObj.button_Mini),
2217
+ createCss2Button(`btnHelpRight`, ``, () => true, {
2218
+ x: 0, y: 0, w: 0, h: 0, opacity: 0, resetFunc: () => g_helpPreviewColumn(1),
2219
+ }, g_cssObj.button_Mini),
2153
2220
  );
2154
2221
  createScText(btnHelpClose, `HelpClose`, { displayName: `helpOverlay`, targetLabel: `btnHelpClose`, x: -5 });
2155
2222
  if (previewH > 0) {
2156
- makeHelpPreview(overlay, _stateKey, g_sHeight - 30 - previewH, previewH);
2223
+ const def = g_helpPreviewDefs[_stateKey];
2224
+ let func;
2225
+ if (def?.ring) {
2226
+ func = makeRingHelpPreview;
2227
+ } else {
2228
+ func = makeHelpPreview;
2229
+ }
2230
+ func(overlay, _stateKey, g_sHeight - 30 - previewH, previewH);
2157
2231
  }
2158
2232
  // ユーザカスタムイベント(ヘルプ表示)
2159
2233
  safeExecuteCustomHooks(`g_customJsObj.helpOverlay`, g_customJsObj.helpOverlay);
@@ -2202,17 +2276,21 @@ const isLongHelp = _msg =>
2202
2276
  * - currentKey: サンプルキーではなく実際のキーに合わせてマッピングする
2203
2277
  * (StepArea, Swappingは実際のキーには非対応)
2204
2278
  * - frzUse : サンプルでフリーズアローを使用するかどうか
2279
+ * - sub : 派生する設定 (Typeなど)。選択肢の列を2列目に追加する
2280
+ * - ring : リングのプレビューを出す
2205
2281
  */
2206
2282
  const g_helpPreviewDefs = {
2207
2283
  motion: {},
2208
2284
  scroll: { currentKey: true },
2209
2285
  appearance: { splitLayer: true },
2210
- playWindow: {},
2286
+ playWindow: { sub: `playWindowType` },
2211
2287
  stepArea: {},
2212
2288
  shaking: { splitLayer: true },
2213
- frzReturn: {},
2214
- effect: { currentKey: true, frzUse: true },
2289
+ frzReturn: { sub: `frzReturnType` },
2290
+ effect: { frzUse: true, splitLayer: true },
2215
2291
  swapping: { frzUse: true },
2292
+ ringSpeed: { ring: true },
2293
+ ringGuide: { ring: true },
2216
2294
  };
2217
2295
 
2218
2296
  /**
@@ -2246,6 +2324,9 @@ let g_helpPreviewStop = () => true;
2246
2324
  /** プレビューの選択肢を前後へ動かす処理 (プレビュー表示中のみ差し替わる) */
2247
2325
  let g_helpPreviewMove = _dir => true;
2248
2326
 
2327
+ /** プレビューの操作対象の列を切り替える処理 (-1: 本体、1: Type。プレビュー表示中のみ差し替わる) */
2328
+ let g_helpPreviewColumn = _dir => true;
2329
+
2249
2330
  /**
2250
2331
  * 詳細ヘルプ内プレビューの作成
2251
2332
  * @param {HTMLDivElement} _parent オーバーレイ
@@ -2260,7 +2341,10 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2260
2341
  return;
2261
2342
  }
2262
2343
  const playH = g_headerObj.playingHeight || g_sHeight;
2263
- const chipW = 120;
2344
+ const subList = def.sub !== undefined ? g_settings[`${def.sub}s`] : undefined;
2345
+ const subUse = subList !== undefined;
2346
+ const colW = subUse ? 90 : 120;
2347
+ const chipW = subUse ? colW * 2 + 6 : colW; // 選択肢ボタン列の全体幅 (Typeがある場合は2列)
2264
2348
  const areaW = g_sWidth - 60;
2265
2349
 
2266
2350
  // レーン定義: Scrollのみ現在選択中のキー・パターンの定義を使用 (他は4レーン固定)
@@ -2432,6 +2516,8 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2432
2516
  let stride = Math.max(Math.round(visibleLanes.length * 0.6), 1);
2433
2517
  while (gcd(stride, visibleLanes.length) !== 1) stride++;
2434
2518
  let selected = ``;
2519
+ let subSelected = subUse ? (subList.includes(g_stateObj[def.sub]) ? g_stateObj[def.sub] : subList[0]) : ``;
2520
+ let activeCol = 0; // 上下キーの対象列 (0: 本体、1: Type)
2435
2521
  let frameCnt = 0;
2436
2522
  let noteCnt = 0;
2437
2523
  let autoCnt = 0; // 自動切替までの経過フレーム数
@@ -2443,7 +2529,7 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2443
2529
  let filterElms = []; // Appearanceのフィルターバー・ラベル
2444
2530
  const frzLen = 90; // Effectプレビュー用フリーズアローの長さ(px)
2445
2531
  const work = { drunkXFlg: true, drunkYFlg: true, drunkAxisFlg: false, drunkRotateFlg: false };
2446
- const frz = { seq: null, axes: [], start: 0, next: 0 };
2532
+ const frz = { seq: null, seqNo: 0, axes: [], start: 0, next: 0 };
2447
2533
 
2448
2534
  /** 付与したtransformの初期化 */
2449
2535
  const resetTransforms = () => spriteIds.forEach(id => {
@@ -2598,6 +2684,18 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2598
2684
  });
2599
2685
  };
2600
2686
 
2687
+ /** Typeの選択値を、ゲーム側の関数が参照する g_stateObj へ一時的に差し替えて実行 (g_playWindowDir など) */
2688
+ const withSub = func => {
2689
+ if (!subUse) return func();
2690
+ const org = g_stateObj[def.sub];
2691
+ g_stateObj[def.sub] = subSelected;
2692
+ try {
2693
+ return func();
2694
+ } finally {
2695
+ g_stateObj[def.sub] = org;
2696
+ }
2697
+ };
2698
+
2601
2699
  /**
2602
2700
  * 選択肢の適用
2603
2701
  * - Motion: 「配列の15がステップゾーン上」として、ステップゾーンからの距離を逆算する
@@ -2611,9 +2709,10 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2611
2709
 
2612
2710
  assignSwapping(_key === `swapping` ? opt : null);
2613
2711
  // レーンのレイヤー配置 (StepAreaは選択肢ごと、Shakingは右半分を逆スクロール側へ)
2614
- assignLanes((_key === `stepArea` ? g_helpStepAreaLayouts[opt] : null)
2615
- ?? (g_helpPreviewDefs[_key]?.currentKey ? getScrollLayout(opt) : null)
2616
- ?? (def.splitLayer ? [0, 0, 1, 1] : [0, 0, 0, 0]));
2712
+ assignLanes((g_helpPreviewDefs[_key]?.currentKey ? getScrollLayout(opt) : null)
2713
+ ?? (_key === `stepArea` ? g_helpStepAreaLayouts[opt] : null)
2714
+ ?? (def.splitLayer ? [0, 0, 1, 1] : [0, 0, 0, 0])
2715
+ .map(val => g_stateObj.reverse === C_FLG_OFF ? val : val ^ 1));
2617
2716
  applyFlat(opt);
2618
2717
  if (_key === `stepArea`) {
2619
2718
  runStepArea(opt);
@@ -2624,7 +2723,7 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2624
2723
 
2625
2724
  // SideScrollのみ矢印を90度回転 (ゲーム側の changeStepRtn と同じ符号)
2626
2725
  rtnOffset = (_key === `playWindow` && opt === `SideScroll` && g_stateObj.rotateEnabled)
2627
- ? 90 * (g_stateObj.playWindowType === C_FLG_REVERSE2 ? -1 : 1) : 0;
2726
+ ? 90 * (subSelected === C_FLG_REVERSE2 ? -1 : 1) : 0;
2628
2727
  if (g_stateObj.rotateEnabled) {
2629
2728
  laneInfo.forEach((lane, j) => {
2630
2729
  if (typeof getRtn(j) === `number`) {
@@ -2651,13 +2750,14 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2651
2750
  dist[b] = dist[b - 1] + frameSpd + (trace[b] || 0);
2652
2751
  }
2653
2752
  if (_key === `playWindow`) {
2654
- addTransform(`mainSprite`, `playWindow`, g_playWindowFunc.get(opt)(), g_transPriority.playWindow);
2753
+ addTransform(`mainSprite`, `playWindow`, withSub(() => g_playWindowFunc.get(opt)()), g_transPriority.playWindow);
2655
2754
  }
2656
2755
  if (_key === `shaking`) {
2657
2756
  Object.assign(work, { drunkXFlg: true, drunkYFlg: true, drunkAxisFlg: false, drunkRotateFlg: false });
2658
2757
  }
2659
2758
  if (_key === `frzReturn`) {
2660
2759
  frz.seq = null;
2760
+ frz.seqNo = 0;
2661
2761
  frz.next = frameCnt + 30;
2662
2762
  }
2663
2763
  };
@@ -2737,8 +2837,8 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2737
2837
  if (selected === C_FLG_OFF) return;
2738
2838
  if (frz.seq === null) {
2739
2839
  if (frameCnt < frz.next) return;
2740
- const seqs = (g_frzReturnSeqFunc.get(g_stateObj.frzReturnType) ?? g_frzReturnSeqFunc.get(`360deg`))();
2741
- frz.seq = seqs[Math.floor(Math.random() * seqs.length)];
2840
+ const seqs = (g_frzReturnSeqFunc.get(subSelected) ?? g_frzReturnSeqFunc.get(`360deg`))();
2841
+ frz.seq = seqs[frz.seqNo++ % seqs.length];
2742
2842
  frz.axes = [...g_frzReturnFunc.get(selected)()].filter(Boolean);
2743
2843
  frz.start = frameCnt;
2744
2844
  }
@@ -2813,38 +2913,83 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2813
2913
  spriteIds.forEach(id => delete g_transforms[id]);
2814
2914
  g_helpPreviewStop = () => true;
2815
2915
  g_helpPreviewMove = _dir => true;
2916
+ g_helpPreviewColumn = _dir => true;
2816
2917
  };
2817
2918
  g_helpPreviewStop = stop;
2818
2919
 
2819
2920
  // ---------------------------------------------------
2820
- // 選択肢ボタン (g_settings の一覧から作成)
2821
- const chipH = Math.min(20, Math.floor(_height / optList.length));
2921
+ // 選択肢ボタン (本体の列と、Typeがある設定のみTypeの列)
2922
+ const chipX = baseX + viewW + 15;
2923
+ const colMain = createEmptySprite(area, `pvColMain`, { x: chipX, y: 0, w: colW, h: _height });
2924
+ const colSub = subUse ? createEmptySprite(area, `pvColSub`, { x: chipX + colW + 6, y: 0, w: colW, h: _height }) : null;
2925
+
2926
+ const makeChips = (_list, _prefix, _onSelect, _title = ``) => {
2927
+ const chipH = Math.min(20, Math.floor(_height / _list.length));
2928
+ return _list.map((opt, i) =>
2929
+ createCss2Button(`${_prefix}${i}`, getStgDetailName(opt), () => _onSelect(opt), {
2930
+ x: 0, y: i * chipH, w: colW, h: chipH - 2, siz: 12, title: _title,
2931
+ }, g_cssObj.button_Default));
2932
+ };
2933
+ const chips = makeChips(optList, `pvChip`, opt => selectMain(opt));
2934
+ const subChips = subUse ? makeChips(subList, `pvSubChip`, opt => selectSub(opt), g_msgObj[def.sub] ?? ``) : [];
2935
+ multiAppend(colMain, ...chips);
2936
+ if (subUse) {
2937
+ multiAppend(colSub, ...subChips);
2938
+ }
2939
+
2822
2940
  const markChips = () => {
2823
- chips.forEach((chip, i) => {
2941
+ const mark = (_list, _chips, _current) => _chips.forEach((chip, i) => {
2824
2942
  chip.classList.remove(g_cssObj.button_Default, g_cssObj.button_Setting);
2825
- chip.classList.add(optList[i] === selected ? g_cssObj.button_Setting : g_cssObj.button_Default);
2943
+ chip.classList.add(_list[i] === _current ? g_cssObj.button_Setting : g_cssObj.button_Default);
2826
2944
  });
2827
- g_helpSelectHook(selected);
2945
+ mark(optList, chips, selected);
2946
+ if (subUse) {
2947
+ mark(subList, subChips, subSelected);
2948
+ colSub.style.opacity = activeCol === 1 ? 1 : 0.5; // 操作対象でない列は薄く表示
2949
+ }
2950
+ colMain.style.opacity = subUse && activeCol !== 0 ? 0.5 : 1;
2951
+ g_helpSelectHook(subUse ? [selected, subSelected] : [selected], activeCol);
2828
2952
  safeExecuteCustomHooks(`g_customJsObj.helpPreview`, g_customJsObj.helpPreview, _key, selected,
2829
- { pvFrame, mainSprite, stepSprites, arrowSprites, playW, playH });
2953
+ { pvFrame, mainSprite, stepSprites, arrowSprites, playW, playH, sub: subSelected });
2954
+ };
2955
+
2956
+ // 本体の選択(選択時は自動移動時間を2倍にする)
2957
+ const selectMain = opt => {
2958
+ autoCnt = -g_limitObj.helpPreviewAutoFrames;
2959
+ activeCol = 0;
2960
+ applyOption(opt);
2961
+ markChips();
2962
+ };
2963
+
2964
+ // Typeの選択 (本体がOFF/Defaultのときは、効果が見える基準の選択肢へ切り替える)
2965
+ const selectSub = opt => {
2966
+ autoCnt = -g_limitObj.helpPreviewAutoFrames;
2967
+ activeCol = 1;
2968
+ subSelected = opt;
2969
+ const base = optList[1];
2970
+ applyOption(selected === optList[0] && optList.includes(base) ? base : selected);
2971
+ markChips();
2830
2972
  };
2831
- const chips = optList.map((opt, i) =>
2832
- createCss2Button(`pvChip${i}`, getStgDetailName(opt), () => {
2833
- autoCnt = -g_limitObj.helpPreviewAutoFrames;
2834
- applyOption(opt);
2835
- markChips();
2836
- }, { x: baseX + viewW + 15, y: i * chipH, w: chipW, h: chipH - 2, siz: 12 }, g_cssObj.button_Default)
2837
- );
2838
- multiAppend(area, ...chips);
2839
2973
 
2974
+ // 上下キー: 操作対象の列の選択肢を前後に動かす (端では反対側へ循環)
2840
2975
  const moveOption = _dir => {
2841
2976
  applyOption(optList[nextPos(optList.indexOf(selected), _dir, optList.length)]);
2842
2977
  markChips();
2843
2978
  };
2844
- // 上下キー: 選択肢を前後に動かす (端では反対側へ循環)
2845
2979
  g_helpPreviewMove = _dir => {
2846
- autoCnt = -g_limitObj.helpPreviewAutoFrames;
2847
- moveOption(_dir);
2980
+ if (subUse && activeCol === 1) {
2981
+ selectSub(subList[nextPos(subList.indexOf(subSelected), _dir, subList.length)]);
2982
+ } else {
2983
+ autoCnt = -g_limitObj.helpPreviewAutoFrames;
2984
+ moveOption(_dir);
2985
+ }
2986
+ };
2987
+
2988
+ // 左右キー: 操作対象の列を切り替える (←: 本体、→: Type)
2989
+ g_helpPreviewColumn = _dir => {
2990
+ if (!subUse) return;
2991
+ activeCol = _dir > 0 ? 1 : 0;
2992
+ markChips();
2848
2993
  };
2849
2994
 
2850
2995
  // 初期表示: 現在値 (OFF/Default の場合は最初の非OFFの選択肢)
@@ -2854,6 +2999,227 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2854
2999
  timerId = g_timerHandler.setTimeout(tick, 1000 / g_fps);
2855
3000
  };
2856
3001
 
3002
+ /**
3003
+ * リングノート設定 (RingSpeed / RingGuide) 用の詳細ヘルプ内プレビュー
3004
+ * - ゲーム側 makeRing/updateRings と同じ構成 (ガイド円 + 収束する本体円) を縮小表示する
3005
+ * - 判定はオートプレイ相当 (到達フレームで判定済み扱い)。ポインタ判定は再現しない
3006
+ * - RingSpeed: 現在の倍率での出現~到達フレーム数で収束させる
3007
+ * - RingGuide: ガイドの違いが分かるよう、複数のリングが同時に出る固定の速さで表示する
3008
+ * @param {HTMLDivElement} _parent オーバーレイ
3009
+ * @param {string} _key ringSpeed / ringGuide
3010
+ * @param {number} _top 配置Y座標
3011
+ * @param {number} _height 領域の高さ
3012
+ */
3013
+ const makeRingHelpPreview = (_parent, _key, _top, _height) => {
3014
+ if (document.getElementById(`pvRingFrame`) !== null) {
3015
+ return;
3016
+ }
3017
+ const isSpeed = _key === `ringSpeed`;
3018
+ const optList = g_settings[`${_key}s`];
3019
+ const round2 = _val => Math.round(_val * 100) / 100;
3020
+ const sameVal = (_a, _b) => (isSpeed ? round2(_a) === round2(_b) : _a === _b);
3021
+
3022
+ // チップに出す選択肢 (RingSpeedは数が多いため 0.5x 刻みの代表値のみ)
3023
+ const chipList = isSpeed ? optList.filter(val => Math.round(val * 100) % 50 === 0) : optList;
3024
+
3025
+ const playW = g_headerObj.playingWidth || g_sWidth;
3026
+ const playH = g_headerObj.playingHeight || g_sHeight;
3027
+ const chipW = 100;
3028
+ const areaW = g_sWidth - 60;
3029
+ const rate = Math.min(g_limitObj.helpPreviewMaxRate, _height / playH, (areaW - chipW - 15) / playW);
3030
+ const viewW = Math.round(playW * rate);
3031
+ const baseX = Math.max(Math.round((areaW - viewW - chipW - 15) / 2), 0);
3032
+
3033
+ // 領域内のクリックでオーバーレイが閉じないようにする
3034
+ const area = createEmptySprite(_parent, `helpPreview`, {
3035
+ x: 30, y: _top, w: areaW, h: _height, pointerEvents: C_DIS_AUTO,
3036
+ });
3037
+ g_handler.addListener(area, `click`, evt => evt.stopPropagation());
3038
+
3039
+ // ---------------------------------------------------
3040
+ // ミニプレイ画面 (縮小表示)
3041
+ const scaler = createEmptySprite(area, `pvRingScale`, {
3042
+ x: baseX, y: 0, w: playW, h: playH, transform: `scale(${rate})`, transformOrigin: `top left`,
3043
+ });
3044
+ const pvFrame = createEmptySprite(scaler, `pvRingFrame`, {
3045
+ w: playW, h: playH, background: g_headerObj.baseBrightFlg ? `#eeeeee` : `#111111`,
3046
+ border: `1px solid #444444`, boxSizing: `border-box`, overflow: `hidden`,
3047
+ });
3048
+ const ringSprite = createEmptySprite(pvFrame, `pvRingSprite`, { w: playW, h: playH });
3049
+
3050
+ // ---------------------------------------------------
3051
+ // プレビュー状態
3052
+ const radius = g_ringObj.radius;
3053
+ const ringColor = g_headerObj.baseBrightFlg ? g_ringObj.colorLight : g_ringObj.color;
3054
+ const spots = [[0.3, 0.3], [0.65, 0.4], [0.45, 0.62], [0.75, 0.72], [0.25, 0.68], [0.55, 0.25]];
3055
+ const holdFrames = 6; // 到達後に残すフレーム数 (判定の瞬間を見せる)
3056
+ const guideDuration = 90, guideInterval = 28; // RingGuide用 (同時に3個前後出る)
3057
+ const speedInterval = 45; // RingSpeed用
3058
+
3059
+ let selected = isSpeed
3060
+ ? (optList.find(val => sameVal(val, g_stateObj.ringSpeed)) ?? optList[0])
3061
+ : (optList.includes(g_stateObj.ringGuide) ? g_stateObj.ringGuide : optList[0]);
3062
+ let rings = [];
3063
+ let frameCnt = 0;
3064
+ let noteCnt = 0;
3065
+ let autoCnt = 0;
3066
+ let timerId = null;
3067
+
3068
+ // ゲーム側 getRingDuration (ringSpeedData なし) と同じ式
3069
+ const getTotal = () => isSpeed ? Math.max(Math.round(g_ringObj.baseFrame / selected), 1) : guideDuration;
3070
+ const getInterval = () => isSpeed ? speedInterval : guideInterval;
3071
+ const guideMode = () => isSpeed ? `ALL` : selected;
3072
+
3073
+ const clearRings = () => {
3074
+ rings.forEach(ring => {
3075
+ ring.guide.remove();
3076
+ ring.body.remove();
3077
+ });
3078
+ rings = [];
3079
+ };
3080
+
3081
+ const spawn = () => {
3082
+ const [rx, ry] = spots[noteCnt % spots.length];
3083
+ const no = ++noteCnt;
3084
+ const base = {
3085
+ x: Math.round(playW * rx) - radius, y: Math.round(playH * ry) - radius,
3086
+ w: radius * 2, h: radius * 2, borderRadius: `50%`, boxSizing: `border-box`,
3087
+ };
3088
+ const guide = createEmptySprite(ringSprite, `pvRingGuide${no}`, {
3089
+ ...base, border: `solid ${g_ringObj.lineWidth}px ${g_ringObj.guideColor}`,
3090
+ visibility: guideMode() === `Hidden` ? `hidden` : `visible`,
3091
+ });
3092
+ const body = createEmptySprite(ringSprite, `pvRingBody${no}`, {
3093
+ ...base, border: `solid ${g_ringObj.lineWidth}px ${ringColor}`,
3094
+ transform: `scale(${g_ringObj.startRate})`,
3095
+ });
3096
+ const total = getTotal();
3097
+ rings.push({ guide, body, total, cnt: total + 1, guideVisible: guideMode() !== `Hidden` });
3098
+ };
3099
+
3100
+ const updateRings = () => {
3101
+ rings = rings.filter(ring => {
3102
+ ring.cnt--;
3103
+ if (ring.cnt < -holdFrames) {
3104
+ ring.guide.remove();
3105
+ ring.body.remove();
3106
+ return false;
3107
+ }
3108
+ // 本体の大きさ (cnt === 0 でガイドと一致)
3109
+ const t = Math.min(Math.max(1 - ring.cnt / ring.total, 0), 1);
3110
+ ring.body.style.transform = `scale(${g_ringObj.startRate + (1 - g_ringObj.startRate) * t})`;
3111
+ ring.body.style.opacity = ring.cnt > 0 ? 1 : Math.max((holdFrames + ring.cnt) / holdFrames, 0);
3112
+ return true;
3113
+ });
3114
+
3115
+ // ガイド表示 (ゲーム側 updateRings と同じ条件。Next: 到達済み or 次に判定するリングのみ)
3116
+ const mode = guideMode();
3117
+ const nextRing = rings.find(ring => ring.cnt > 0);
3118
+ rings.forEach(ring => {
3119
+ const visible = mode === `Hidden` ? false
3120
+ : mode === `Next` ? (ring.cnt <= 0 || ring === nextRing) : true;
3121
+ if (ring.guideVisible !== visible) {
3122
+ ring.guide.style.visibility = visible ? `visible` : `hidden`;
3123
+ ring.guideVisible = visible;
3124
+ }
3125
+ });
3126
+ };
3127
+
3128
+ // ---------------------------------------------------
3129
+ // 選択肢ボタン
3130
+ const colMain = createEmptySprite(area, `pvColMain`, { x: baseX + viewW + 15, y: 0, w: chipW, h: _height });
3131
+ const chipH = Math.min(20, Math.floor(_height / (chipList.length + 2)));
3132
+ const getChipName = opt => isSpeed ? `${opt} ${g_lblNameObj.multi}` : getStgDetailName(opt);
3133
+ const chips = chipList.map((opt, i) =>
3134
+ createCss2Button(`pvRingChip${i}`, getChipName(opt), () => selectOption(opt), {
3135
+ x: 0, y: i * chipH, w: chipW, h: chipH - 2, siz: 12,
3136
+ }, g_cssObj.button_Default));
3137
+ multiAppend(colMain, ...chips);
3138
+
3139
+ // RingSpeed: 現在の倍率と出現~到達フレーム数
3140
+ const lblInfo = createDivCss2Label(`pvRingInfo`, ``, {
3141
+ x: 0, y: chipList.length * chipH + 4, w: chipW, h: 32, siz: 12, align: C_ALIGN_LEFT,
3142
+ });
3143
+ colMain.appendChild(lblInfo);
3144
+
3145
+ const markChips = () => {
3146
+ chips.forEach((chip, i) => {
3147
+ chip.classList.remove(g_cssObj.button_Default, g_cssObj.button_Setting);
3148
+ chip.classList.add(sameVal(chipList[i], selected) ? g_cssObj.button_Setting : g_cssObj.button_Default);
3149
+ });
3150
+ lblInfo.textContent = isSpeed ? `${round2(selected)} ${g_lblNameObj.multi} (${getTotal()}f)` : ``;
3151
+ g_helpSelectHook([selected], 0);
3152
+ safeExecuteCustomHooks(`g_customJsObj.helpPreview`, g_customJsObj.helpPreview, _key, selected,
3153
+ { pvFrame, ringSprite, playW, playH });
3154
+ };
3155
+
3156
+ const applyOption = opt => {
3157
+ selected = opt;
3158
+ clearRings();
3159
+ frameCnt = 0;
3160
+ spawn();
3161
+ };
3162
+ const selectOption = opt => {
3163
+ autoCnt = -g_limitObj.helpPreviewAutoFrames;
3164
+ applyOption(opt);
3165
+ markChips();
3166
+ };
3167
+
3168
+ // 上下キー: RingSpeedは内側ボタンと同じ5つ刻み (0.25x)、RingGuideは1つずつ
3169
+ const moveOption = _step => {
3170
+ const idx = Math.max(optList.findIndex(val => sameVal(val, selected)), 0);
3171
+ applyOption(optList[nextPos(idx, _step, optList.length)]);
3172
+ markChips();
3173
+ };
3174
+ // 自動切替: RingSpeedは代表値 (チップ) を順に巡回
3175
+ const autoOption = () => {
3176
+ if (!isSpeed) {
3177
+ moveOption(1);
3178
+ return;
3179
+ }
3180
+ const next = chipList.find(val => round2(val) > round2(selected));
3181
+ applyOption(next ?? chipList[0]);
3182
+ markChips();
3183
+ };
3184
+
3185
+ const stop = () => {
3186
+ if (timerId !== null) {
3187
+ g_timerHandler.clearTimeout(timerId);
3188
+ }
3189
+ timerId = null;
3190
+ g_helpPreviewStop = () => true;
3191
+ g_helpPreviewMove = _dir => true;
3192
+ g_helpPreviewColumn = _dir => true;
3193
+ };
3194
+ g_helpPreviewStop = stop;
3195
+ g_helpPreviewMove = _dir => {
3196
+ autoCnt = -g_limitObj.helpPreviewAutoFrames;
3197
+ moveOption(_dir * (isSpeed ? g_settings.ringSpeedTerms[1] : 1));
3198
+ };
3199
+
3200
+ const tick = () => {
3201
+ if (document.getElementById(`pvRingFrame`) === null) {
3202
+ stop();
3203
+ return;
3204
+ }
3205
+ frameCnt++;
3206
+ if (g_limitObj.helpPreviewAutoFrames > 0 && ++autoCnt >= g_limitObj.helpPreviewAutoFrames) {
3207
+ autoCnt = 0;
3208
+ autoOption();
3209
+ }
3210
+ if (frameCnt % getInterval() === 0) {
3211
+ spawn();
3212
+ }
3213
+ updateRings();
3214
+ timerId = g_timerHandler.setTimeout(tick, 1000 / g_fps);
3215
+ };
3216
+
3217
+ // 初期表示: 現在の設定値
3218
+ applyOption(selected);
3219
+ markChips();
3220
+ timerId = g_timerHandler.setTimeout(tick, 1000 / g_fps);
3221
+ };
3222
+
2857
3223
  /**
2858
3224
  * 設定名の置き換え処理(未指定時は原則g_lblNameObjの値を使用、明示的な指定がある場合のみ元の名前を返却)
2859
3225
  * @param {string} _name
@@ -3005,7 +3371,8 @@ const gaugeFormat = (_mode, _border, _rcv, _dmg, _init, _lifeValFlg) => {
3005
3371
  let rcvText = round2(_rcv), dmgText = round2(_dmg);
3006
3372
  let realRcv = _rcv, realDmg = _dmg;
3007
3373
  const allCnt = sumData(g_detailObj.arrowCnt[g_stateObj.scoreId]) +
3008
- (g_headerObj.frzStartjdgUse ? 2 : 1) * sumData(g_detailObj.frzCnt[g_stateObj.scoreId]);
3374
+ (g_headerObj.frzStartjdgUse ? 2 : 1) * sumData(g_detailObj.frzCnt[g_stateObj.scoreId]) +
3375
+ g_detailObj.ringCnt[g_stateObj.scoreId];
3009
3376
 
3010
3377
  // ゲージ設定が矢印数依存の場合、実際の値に変換して表示する
3011
3378
  // 表示上、計算した値は小数第二位までの表示とする(それ以外はそのまま)
@@ -3985,8 +4352,30 @@ const createSettingsDisplayWindow = _sprite => {
3985
4352
  unitName: g_lblNameObj.pixel,
3986
4353
  addRFunc: () => updatePreviewHitPositionLine(g_stateObj.hitPosition),
3987
4354
  });
4355
+
4356
+ if (hasRingNotes()) {
4357
+ // ---------------------------------------------------
4358
+ // リングノートの速度 (RingSpeed)
4359
+ // 縦位置: 9.8
4360
+ g_settings.ringSpeedNum = Math.max(g_settings.ringSpeeds.indexOf(g_stateObj.ringSpeed), 0);
4361
+ g_stateObj.ringSpeed = g_settings.ringSpeeds[g_settings.ringSpeedNum];
4362
+ createGeneralSetting(spriteList.ringSpeed, `ringSpeed`, { skipTerms: g_settings.ringSpeedTerms, unitName: ` ${g_lblNameObj.multi}` });
4363
+
4364
+ // ---------------------------------------------------
4365
+ // リングノートのガイド表示 (RingGuide)
4366
+ // 縦位置: 10.8
4367
+ g_settings.ringGuideNum = Math.max(g_settings.ringGuides.indexOf(g_stateObj.ringGuide), 0);
4368
+ g_stateObj.ringGuide = g_settings.ringGuides[g_settings.ringGuideNum];
4369
+ createGeneralSetting(spriteList.ringGuide, `ringGuide`);
4370
+ }
3988
4371
  };
3989
4372
 
4373
+ /**
4374
+ * 選択中の譜面にリングノートが含まれるかをチェック
4375
+ * @returns {boolean}
4376
+ */
4377
+ const hasRingNotes = () => (g_detailObj.ringCnt[g_stateObj.scoreId] ?? 0) > 0;
4378
+
3990
4379
  /**
3991
4380
  * Displayボタンを切り替えたときに連動して切り替えるボタンの設定
3992
4381
  * @param {object} _headerObj
@@ -4155,7 +4544,10 @@ const createGeneralSettingEx = (_spriteList, _name, { defaultList = [C_FLG_OFF],
4155
4544
  funcEx();
4156
4545
  createExpandedScView(_name);
4157
4546
  },
4158
- title: g_msgObj[`${_name}Type`] ?? ``,
4547
+ // 拡張ボタンの説明文表示は「拡張元」設定のヘルプが長い場合、詳細ヘルプ表示側で表示するため省略する
4548
+ title: isLongHelp(g_msgObj[`${_name}`] ?? ``) || g_helpPreviewDefs[_name] !== undefined
4549
+ ? ``
4550
+ : (g_msgObj[`${_name}Type`] ?? ``),
4159
4551
  }, g_cssObj.button_Default, g_cssObj.button_RevON);
4160
4552
 
4161
4553
  /**