fix: correct chart scoring weights for v1.0.3

This commit is contained in:
2026-08-19 16:57:54 +09:00
parent e259f82b69
commit bd9d5a6ba6
374 changed files with 1551 additions and 1485 deletions
+68 -6
View File
@@ -79,12 +79,56 @@ function extractJsonParseArgument(moduleText) {
function flattenEvents(chartData) {
const noteEvents = chartData.note_events ?? {};
const green = (noteEvents.green_blocks ?? noteEvents.green ?? [])
.flatMap((block) => Array.isArray(block) ? block : [block]);
const greenBlocks = (noteEvents.green_blocks ?? noteEvents.green ?? [])
.map((rawBlock) => Array.isArray(rawBlock) ? rawBlock : [rawBlock]);
const pink = noteEvents.pink ?? [];
const internalGreenKeys = new Set(greenBlocks.flatMap((block) => block.slice(1, -1)
.map((note) => `${note.time_seconds}|${note.lane}`)));
const beatGap = (end, flick) => flick.beat - end.beat;
const sameShape = (end, flick) =>
end.lane_center === flick.lane_center && end.lane_half_width === flick.lane_half_width;
const shapeOverlapRatio = (end, flick) => {
const left = Math.max(end.lane_center - end.lane_half_width, flick.lane_center - flick.lane_half_width);
const right = Math.min(end.lane_center + end.lane_half_width, flick.lane_center + flick.lane_half_width);
const unionLeft = Math.min(end.lane_center - end.lane_half_width, flick.lane_center - flick.lane_half_width);
const unionRight = Math.max(end.lane_center + end.lane_half_width, flick.lane_center + flick.lane_half_width);
return Math.max(0, right - left) / Math.max(Number.EPSILON, unionRight - unionLeft);
};
const isDirectContinuation = (end, flick) =>
beatGap(end, flick) > 0 && beatGap(end, flick) <= 0.25 + 1e-9 && sameShape(end, flick);
const continuesIntoFlick = (note) => pink.some((flick) => isDirectContinuation(note, flick));
// 롱노트 끝 플릭은 색상이 아니라 직전 green block과의 연결 관계로 구분한다.
// 끝이 mid인 블록은 다음 플릭이 끝 판정을 대신하며, 아주 짧은 동일 형상 연결도 같은 경우다.
const isHoldEndFlick = (flick) => greenBlocks.some((block) => {
const end = block.at(-1);
if (!end) return false;
const gap = beatGap(end, flick);
return isDirectContinuation(end, flick) ||
(end.note_type === 'mid' && gap > 0 && gap <= 0.5 + 1e-9 && shapeOverlapRatio(end, flick) >= 0.8);
});
const green = greenBlocks
.flatMap((rawBlock) => {
const block = Array.isArray(rawBlock) ? rawBlock : [rawBlock];
return block.map((note, index) => ({
...note,
// 롱노트 안쪽 판정은 점 모양이어도 0.1배다. 시작·끝의 일반 판정만 1배다.
type: (index === 0 || index === block.length - 1) &&
note.note_type !== 'mid' &&
!internalGreenKeys.has(`${note.time_seconds}|${note.lane}`) &&
!(index === block.length - 1 && continuesIntoFlick(note))
? 'regular'
: 'mid'
}));
});
return [
...(noteEvents.blue ?? []).map((note) => ({ ...note, type: note.note_type ?? 'regular' })),
...green.map((note) => ({ ...note, type: note.note_type ?? 'regular' })),
...(noteEvents.pink ?? []).map((note) => ({ ...note, type: note.note_type ?? 'regular', is_flick: true }))
...green,
// 롱노트 끝에 붙은 플릭은 판정 모양과 달리 1.0배, 독립 플릭만 1.05배다.
...pink.map((note) => ({
...note,
type: note.note_type ?? 'regular',
is_flick: !isHoldEndFlick(note)
}))
]
.map((note) => ({
timeSeconds: note.time_seconds,
@@ -95,7 +139,23 @@ function flattenEvents(chartData) {
.sort((left, right) => left.timeSeconds - right.timeSeconds || (left.type === 'mid' ? 1 : -1));
}
function scoreRatioFromNotes(notes) {
function legacyCalibrationNotes(chartData) {
const noteEvents = chartData.note_events ?? {};
const green = (noteEvents.green_blocks ?? noteEvents.green ?? [])
.flatMap((block) => Array.isArray(block) ? block : [block]);
return [
...(noteEvents.blue ?? []).map((note) => ({ ...note, type: note.note_type ?? 'regular' })),
...green.map((note) => ({ ...note, type: note.note_type ?? 'regular' })),
...(noteEvents.pink ?? []).map((note) => ({ ...note, type: note.note_type ?? 'regular', is_flick: true }))
].map((note) => ({
timeSeconds: note.time_seconds,
type: note.type === 'mid' ? 'mid' : 'regular',
isFlick: note.is_flick === true
}));
}
function calibratedScoreRatio(chartData) {
const notes = legacyCalibrationNotes(chartData);
const countedMidTimes = new Set();
let weight = 0;
for (const note of notes) {
@@ -156,7 +216,9 @@ async function worker() {
songId: item.songId,
difficulty: item.difficulty.toLowerCase(),
durationSeconds: sourceEntry?.durationSeconds ?? Math.ceil(notes.at(-1)?.timeSeconds ?? 0),
scoreRatio: scoreRatioFromNotes(notes),
// 실측으로 보정된 1노트 기본점수 분모는 기존 채보 가중치 모델을 유지한다.
// 실제 점수 타임라인의 롱노트 분류와는 별개의 보정값이다.
scoreRatio: calibratedScoreRatio(chartData),
scoreCalibrationSource: 'measured-formula-v1',
notes,
specialStartSeconds: (chartData.sp_activation_events ?? [])