Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/poor-bga-500ms.md
Original file line number Diff line number Diff line change
@@ -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 `<poorbga>500</poorbga>` 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.
2 changes: 1 addition & 1 deletion docs/player-spec.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` の既定値 `<poorbga>500</poorbga>` に一致)。
- judge/combo 表示を `POOR` に更新する。

### 空打鍵(candidate なし)— LR2 互換 空POOR
Expand Down
2 changes: 1 addition & 1 deletion docs/player-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<poorbga>500</poorbga>` in its `config.xml`).
- Update judge/combo display to `POOR`.

### Blank keystroke (no candidate) — LR2-compatible empty POOR
Expand Down
37 changes: 36 additions & 1 deletion packages/player-tui/src/bga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<poorbga>` 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 {
Expand Down Expand Up @@ -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) ?? []);
Expand Down
2 changes: 1 addition & 1 deletion packages/player-web/src/scene/lr2/gameplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions packages/player/src/core/bga-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<poorbga>500</poorbga>` 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;
Expand Down