danoniplus 52.1.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/03 (v52.1.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
  // 譜面番号の再取得
@@ -2168,7 +2220,14 @@ const openHelpOverlay = (_title, _msg, _stateKey) => {
2168
2220
  );
2169
2221
  createScText(btnHelpClose, `HelpClose`, { displayName: `helpOverlay`, targetLabel: `btnHelpClose`, x: -5 });
2170
2222
  if (previewH > 0) {
2171
- 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);
2172
2231
  }
2173
2232
  // ユーザカスタムイベント(ヘルプ表示)
2174
2233
  safeExecuteCustomHooks(`g_customJsObj.helpOverlay`, g_customJsObj.helpOverlay);
@@ -2218,6 +2277,7 @@ const isLongHelp = _msg =>
2218
2277
  * (StepArea, Swappingは実際のキーには非対応)
2219
2278
  * - frzUse : サンプルでフリーズアローを使用するかどうか
2220
2279
  * - sub : 派生する設定 (Typeなど)。選択肢の列を2列目に追加する
2280
+ * - ring : リングのプレビューを出す
2221
2281
  */
2222
2282
  const g_helpPreviewDefs = {
2223
2283
  motion: {},
@@ -2229,6 +2289,8 @@ const g_helpPreviewDefs = {
2229
2289
  frzReturn: { sub: `frzReturnType` },
2230
2290
  effect: { frzUse: true, splitLayer: true },
2231
2291
  swapping: { frzUse: true },
2292
+ ringSpeed: { ring: true },
2293
+ ringGuide: { ring: true },
2232
2294
  };
2233
2295
 
2234
2296
  /**
@@ -2937,6 +2999,227 @@ const makeHelpPreview = (_parent, _key, _top, _height) => {
2937
2999
  timerId = g_timerHandler.setTimeout(tick, 1000 / g_fps);
2938
3000
  };
2939
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
+
2940
3223
  /**
2941
3224
  * 設定名の置き換え処理(未指定時は原則g_lblNameObjの値を使用、明示的な指定がある場合のみ元の名前を返却)
2942
3225
  * @param {string} _name
@@ -3088,7 +3371,8 @@ const gaugeFormat = (_mode, _border, _rcv, _dmg, _init, _lifeValFlg) => {
3088
3371
  let rcvText = round2(_rcv), dmgText = round2(_dmg);
3089
3372
  let realRcv = _rcv, realDmg = _dmg;
3090
3373
  const allCnt = sumData(g_detailObj.arrowCnt[g_stateObj.scoreId]) +
3091
- (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];
3092
3376
 
3093
3377
  // ゲージ設定が矢印数依存の場合、実際の値に変換して表示する
3094
3378
  // 表示上、計算した値は小数第二位までの表示とする(それ以外はそのまま)
@@ -4068,8 +4352,30 @@ const createSettingsDisplayWindow = _sprite => {
4068
4352
  unitName: g_lblNameObj.pixel,
4069
4353
  addRFunc: () => updatePreviewHitPositionLine(g_stateObj.hitPosition),
4070
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
+ }
4071
4371
  };
4072
4372
 
4373
+ /**
4374
+ * 選択中の譜面にリングノートが含まれるかをチェック
4375
+ * @returns {boolean}
4376
+ */
4377
+ const hasRingNotes = () => (g_detailObj.ringCnt[g_stateObj.scoreId] ?? 0) > 0;
4378
+
4073
4379
  /**
4074
4380
  * Displayボタンを切り替えたときに連動して切り替えるボタンの設定
4075
4381
  * @param {object} _headerObj
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "52.1.0",
3
+ "version": "52.2.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",
@@ -5,7 +5,7 @@
5
5
  スキンcssファイル (default)
6
6
 
7
7
  Created : 2019/11/04
8
- Revised : 2026/05/17 (v48.0.0)
8
+ Revised : 2026/11/08 (v52.2.0)
9
9
 
10
10
  https://github.com/cwtickle/danoniplus
11
11
  ------------------------------------------ */
@@ -43,6 +43,8 @@
43
43
  --settings-appearance-x: #cc99ff;
44
44
  --settings-opacity-x: #ee99ff;
45
45
  --settings-hitPosition-x: #ff99ff;
46
+ --settings-ringSpeed-x: #ff99cc;
47
+ --settings-ringGuide-x: #ff9999;
46
48
  --settings-playWindow-x: #9999ff;
47
49
  --settings-stepArea-x: #6699ff;
48
50
  --settings-frzReturn-x: #99ccff;
@@ -5,7 +5,7 @@
5
5
  スキンcssファイル (light)
6
6
 
7
7
  Created : 2019/11/04
8
- Revised : 2026/05/17 (v48.0.0)
8
+ Revised : 2026/11/08 (v52.2.0)
9
9
 
10
10
  https://github.com/cwtickle/danoniplus
11
11
  ------------------------------------------ */
@@ -43,6 +43,8 @@
43
43
  --settings-appearance-x: #9900ff;
44
44
  --settings-opacity-x: #cc00ff;
45
45
  --settings-hitPosition-x: #ff00ff;
46
+ --settings-ringSpeed-x: #cc0066;
47
+ --settings-ringGuide-x: #cc3333;
46
48
  --settings-playWindow-x: #000099;
47
49
  --settings-stepArea-x: #222299;
48
50
  --settings-frzReturn-x: #003377;
@@ -5,7 +5,7 @@
5
5
  スキンcssファイル (skyblue)
6
6
 
7
7
  Created : 2019/11/04
8
- Revised : 2026/05/17 (v48.0.0)
8
+ Revised : 2026/11/08 (v52.2.0)
9
9
 
10
10
  https://github.com/cwtickle/danoniplus
11
11
  ------------------------------------------ */
@@ -43,6 +43,8 @@
43
43
  --settings-appearance-x: #9900ff;
44
44
  --settings-opacity-x: #cc00ff;
45
45
  --settings-hitPosition-x: #ff00ff;
46
+ --settings-ringSpeed-x: #9900cc;
47
+ --settings-ringGuide-x: #3333ff;
46
48
  --settings-playWindow-x: #000099;
47
49
  --settings-stepArea-x: #000099;
48
50
  --settings-frzReturn-x: #003399;
Binary file