fix: correct chart scoring weights for v1.0.3
This commit is contained in:
@@ -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 ?? [])
|
||||
|
||||
Reference in New Issue
Block a user