From b3fa63509b8d022f58ad5b36b2cae874cfb3747c Mon Sep 17 00:00:00 2001 From: Seiya KONNO Date: Wed, 19 Aug 2026 20:05:20 +0900 Subject: [PATCH] fix(player): shorten the default POOR BGA display window to 500ms (LR2) --- .changeset/poor-bga-500ms.md | 12 ++++++ docs/player-spec.ja.md | 2 +- docs/player-spec.md | 2 +- packages/player-tui/src/bga.test.ts | 37 ++++++++++++++++++- packages/player-web/src/scene/lr2/gameplay.ts | 2 +- packages/player/src/core/bga-timeline.ts | 8 ++-- 6 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .changeset/poor-bga-500ms.md diff --git a/.changeset/poor-bga-500ms.md b/.changeset/poor-bga-500ms.md new file mode 100644 index 00000000..300a7643 --- /dev/null +++ b/.changeset/poor-bga-500ms.md @@ -0,0 +1,12 @@ +--- +'@be-music/player': patch +'@be-music/player-tui': patch +'@be-music/player-web': patch +--- + +Shorten the default POOR / miss BGA display window from 2000 ms to 500 ms, matching real LR2. + +LR2 ships `500` in its `config.xml` and its changelog documents 500 ms as the +miss-BGA default, so the previous 2-second window held the miss layer four times longer than LR2. +`DEFAULT_POOR_BGA_DISPLAY_SECONDS` is shared by the TUI compositor and the web LR2 scene, so both +runtimes pick up the corrected timing. diff --git a/docs/player-spec.ja.md b/docs/player-spec.ja.md index 826e04d4..dfdb7494 100644 --- a/docs/player-spec.ja.md +++ b/docs/player-spec.ja.md @@ -324,7 +324,7 @@ bmson の `judgeRank` は `#DEFEXRANK` と同じ「`100` = `NORMAL`」基準の - `summary.poor` を加算する。 - combo を 0 に戻す。 - groove gauge を `-6` する。 -- POOR BGA を発火する。 +- POOR BGA を発火する。miss レイヤーの表示時間は 500ms(LR2 同梱 `config.xml` の既定値 `500` に一致)。 - judge/combo 表示を `POOR` に更新する。 ### 空打鍵(candidate なし)— LR2 互換 空POOR diff --git a/docs/player-spec.md b/docs/player-spec.md index 10533390..0b27992f 100644 --- a/docs/player-spec.md +++ b/docs/player-spec.md @@ -315,7 +315,7 @@ When `POOR` occurs, do the following: - Add `summary.poor`. - Set combo back to 0. - Set groove gauge to `-6`. -- Fire POOR BGA. +- Fire POOR BGA. The miss layer stays visible for 500 ms — LR2's shipped default (`500` in its `config.xml`). - Update judge/combo display to `POOR`. ### Blank keystroke (no candidate) — LR2-compatible empty POOR diff --git a/packages/player-tui/src/bga.test.ts b/packages/player-tui/src/bga.test.ts index c4acf258..ae7c125f 100644 --- a/packages/player-tui/src/bga.test.ts +++ b/packages/player-tui/src/bga.test.ts @@ -995,6 +995,41 @@ describe('player bga', () => { } }); + test('player bga: POOR overlay expires exactly at the 500 ms LR2 default window', async () => { + const baseDir = await mkdtemp(join(tmpdir(), 'be-music-bga-poor-window-')); + try { + await writePng(join(baseDir, 'base.png'), 256, 256, () => ({ r: 255, g: 0, b: 0, a: 255 })); + await writePng(join(baseDir, 'poor.png'), 256, 256, () => ({ r: 0, g: 255, b: 0, a: 255 })); + + const json = createEmptyJson('bms'); + json.metadata.bpm = 120; + json.resources.bmp['01'] = 'base.png'; + json.resources.bmp['02'] = 'poor.png'; + json.events = [ + { measure: 0, channel: '04', position: [0, 1], value: '01' }, + { measure: 0, channel: '06', position: [0, 1], value: '02' }, + ]; + + const renderer = await createBgaAnsiRenderer(json, { + baseDir, + width: 40, + height: 20, + }); + expect(renderer).toBeDefined(); + + renderer?.triggerPoor(0); + // The window is DEFAULT_POOR_BGA_DISPLAY_SECONDS (0.5 s, LR2's `` default) with a strict `<` + // expiry: the overlay is still visible just before the boundary and gone exactly at it. + const justBefore = parseAnsiPixels(renderer?.getAnsiLines(0.49) ?? []); + expect(justBefore[10]?.[20]).toEqual({ r: 0, g: 255, b: 0 }); + + const atBoundary = parseAnsiPixels(renderer?.getAnsiLines(0.5) ?? []); + expect(atBoundary[10]?.[20]).toEqual({ r: 255, g: 0, b: 0 }); + } finally { + await rm(baseDir, { recursive: true, force: true }); + } + }); + test('player bga: prioritizes POOR over 04/07/0A while active', async () => { const baseDir = await mkdtemp(join(tmpdir(), 'be-music-bga-poor-priority-')); try { @@ -1027,7 +1062,7 @@ describe('player bga', () => { expect(beforePoor[10]?.[20]).toEqual({ r: 0, g: 0, b: 255 }); renderer?.triggerPoor(0); - const duringPoor = parseAnsiPixels(renderer?.getAnsiLines(0.5) ?? []); + const duringPoor = parseAnsiPixels(renderer?.getAnsiLines(0.3) ?? []); expect(duringPoor[10]?.[20]).toEqual({ r: 255, g: 255, b: 0 }); const afterPoor = parseAnsiPixels(renderer?.getAnsiLines(2.1) ?? []); diff --git a/packages/player-web/src/scene/lr2/gameplay.ts b/packages/player-web/src/scene/lr2/gameplay.ts index 3d9b4231..9ed51222 100644 --- a/packages/player-web/src/scene/lr2/gameplay.ts +++ b/packages/player-web/src/scene/lr2/gameplay.ts @@ -3955,7 +3955,7 @@ export class PixiGameplayView { * Composites the chart's BGA into the active skin's `#DST_BGA` rectangles, or the default-family BGA rectangle when * no external skin is loaded. Three layers stack from back to front: base (channel 04 / bmson `bga.events`), layer * (channel 07 / 0A / bmson `layerEvents`), and a POOR override (channel 06 / `poorEvents`) that briefly replaces the - * base while the player is in a 2-second POOR-judgement window. + * base while the player is inside the POOR-judgement window (`DEFAULT_POOR_BGA_DISPLAY_SECONDS`). * * The renderer is idempotent per frame — it tears down any existing sprites and rebuilds from the active cues, so cue * switches show up the next frame without explicit dirty tracking. diff --git a/packages/player/src/core/bga-timeline.ts b/packages/player/src/core/bga-timeline.ts index fc45d426..3579dad8 100644 --- a/packages/player/src/core/bga-timeline.ts +++ b/packages/player/src/core/bga-timeline.ts @@ -9,10 +9,12 @@ import { /** * How long a POOR / miss BGA layer stays visible after a miss before the normal base/layer composite returns. - * Shared by every renderer (TUI compositor, web LR2 scene) so the miss-layer feel is identical across runtimes — - * change it here, not per frontend. + * 500 ms is LR2's shipped default (`500` in its `config.xml`; LR2 exposes it as a per-user + * setting, this implementation does not yet). Shared by the renderers that time the miss layer out (TUI + * compositor, web LR2 scene) so the miss-layer feel is identical across them — change it here, not per frontend. + * The beatoraja web scene instead holds its miss layer until the next non-POOR judge, as beatoraja skins expect. */ -export const DEFAULT_POOR_BGA_DISPLAY_SECONDS = 2; +export const DEFAULT_POOR_BGA_DISPLAY_SECONDS = 0.5; export interface BgaCue { seconds: number;