feat: release version 1.0.7

This commit is contained in:
2026-08-21 19:06:55 +09:00
parent 5d29b63f3a
commit 71dfe11bc4
406 changed files with 3086 additions and 1172 deletions
+31
View File
@@ -0,0 +1,31 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
const calibrationPath = path.resolve('data/score-calibrations.json');
const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json');
const chartDirectory = path.resolve('static/data/charts');
const calibrations = JSON.parse(await readFile(calibrationPath, 'utf8'));
const catalog = JSON.parse(await readFile(catalogPath, 'utf8'));
const catalogRows = Array.isArray(catalog) ? catalog : catalog.charts;
for (const calibration of calibrations.charts) {
const difficulty = calibration.difficulty.toLowerCase();
const catalogRow = catalogRows.find((row) =>
row.songId === calibration.songId && row.difficulty.toLowerCase() === difficulty
);
// Easy/Normal 실측도 보관하되 현재 생성 대상인 Hard/Expert만 즉시 동기화한다.
if (!catalogRow) continue;
catalogRow.scoreRatio = calibration.scoreRatio;
catalogRow.scoreCalibrationSource = 'measured';
const chartPath = path.join(chartDirectory, `${calibration.songId}.${difficulty}.json`);
const chart = JSON.parse(await readFile(chartPath, 'utf8'));
chart.scoreRatio = calibration.scoreRatio;
chart.scoreCalibrationSource = 'measured';
await writeFile(chartPath, `${JSON.stringify(chart)}\n`);
}
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
+24 -7
View File
@@ -6,8 +6,20 @@ const outputDirectory = path.resolve('static/data/charts');
const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json');
const selectedDifficulties = new Set(['HARD', 'EXPERT']);
const chartScoreWeightCoefficient = 0.3713416563352296;
const scoreCalibrations = JSON.parse(await readFile(path.resolve('data/score-calibrations.json'), 'utf8'));
const scoreCalibrationByKey = new Map(scoreCalibrations.charts.map((row) => [
`${row.songId}::${row.difficulty}`,
row
]));
const musicMaster = JSON.parse(await readFile(path.resolve('data/raw/holodoridb/kor/Music.json'), 'utf8'));
const musicById = new Map(musicMaster.map((row) => [row.data?.id ?? row.id, row.data ?? row]));
const englishMusicText = JSON.parse(await readFile(path.resolve('data/raw/holodoridb/eng/LangMusic_Eng.json'), 'utf8'));
const englishTextById = new Map(englishMusicText.map((row) => [row.data?.id ?? row.id, row.data?.text ?? row.text]));
const musicDifficultyMaster = JSON.parse(await readFile(path.resolve('data/raw/holodoridb/kor/MusicDifficulty.json'), 'utf8'));
const difficultyLevelByKey = new Map(musicDifficultyMaster.map((row) => {
const data = row.data ?? row;
return [`${data.musicId}::${data.difficultyType.split('_').at(-1)?.toLowerCase()}`, data.difficultyLevel];
}));
const singerMetadataOverrides = {
m0334: { singerType: 'solo', singerCharacterIds: ['chr-04001'] }
};
@@ -190,8 +202,10 @@ const importPattern = /charts\/([^"/]+\.json)"[^\n]{0,180}?import\("\.\/([^"/]+\
for (const match of simulatorModule.matchAll(importPattern)) {
const [, chartFile, assetFile] = match;
const [songId, difficultyAndLevel] = chartFile.split('.', 2);
const difficulty = difficultyAndLevel.split('-')[0].toUpperCase();
if (selectedDifficulties.has(difficulty)) imports.push({ songId, difficulty, chartFile, assetFile });
const [difficultyName, difficultyLevelText] = difficultyAndLevel.split('-');
const difficulty = difficultyName.toUpperCase();
const difficultyLevel = Number.parseInt(difficultyLevelText, 10);
if (selectedDifficulties.has(difficulty)) imports.push({ songId, difficulty, difficultyLevel, chartFile, assetFile });
}
if (imports.length === 0) throw new Error('수집할 Hard/Expert 채보 모듈을 찾지 못했습니다.');
@@ -211,15 +225,15 @@ async function worker() {
const music = musicById.get(item.songId);
const singerOverride = singerMetadataOverrides[item.songId];
const notes = flattenEvents(chartData);
const measuredCalibration = scoreCalibrationByKey.get(`${item.songId}::${item.difficulty.toLowerCase()}`);
const output = {
schemaVersion: 1,
songId: item.songId,
difficulty: item.difficulty.toLowerCase(),
durationSeconds: sourceEntry?.durationSeconds ?? Math.ceil(notes.at(-1)?.timeSeconds ?? 0),
// 실측으로 보정된 1노트 기본점수 분모는 기존 채보 가중치 모델을 유지한다.
// 실제 점수 타임라인의 롱노트 분류와는 별개의 보정값이다.
scoreRatio: calibratedScoreRatio(chartData),
scoreCalibrationSource: 'measured-formula-v1',
// 실측이 있으면 우선하고, 없을 때만 채보 가중치와 계산기 고유 상수로 추정한다.
scoreRatio: measuredCalibration?.scoreRatio ?? calibratedScoreRatio(chartData),
scoreCalibrationSource: measuredCalibration ? 'measured' : 'local-estimated-v1',
notes,
specialStartSeconds: (chartData.sp_activation_events ?? [])
.sort((left, right) => left.slot - right.slot)
@@ -236,10 +250,13 @@ async function worker() {
await writeFile(path.join(outputDirectory, fileName), JSON.stringify(output), 'utf8');
normalizedCharts.push({
songId: item.songId,
title: sourceEntry?.title ?? item.songId,
title: englishTextById.get(`la-music_title-${item.songId}`) ?? sourceEntry?.title ?? item.songId,
difficulty: item.difficulty.toLowerCase(),
difficultyLevel: difficultyLevelByKey.get(`${item.songId}::${item.difficulty.toLowerCase()}`) ??
(Number.isFinite(item.difficultyLevel) ? item.difficultyLevel : null),
durationSeconds: output.durationSeconds,
scoreRatio: output.scoreRatio,
scoreCalibrationSource: output.scoreCalibrationSource,
totalNotes: notes.length,
singerType: output.singerType,
singerCharacterIds: output.singerCharacterIds,
+1 -1
View File
@@ -581,7 +581,7 @@ function summarizeSpecialSkillLevels(levels) {
return {
...summary,
condition,
effect: condition ? [baseEffect, conditionalEffect].filter(Boolean).join(' ') : baseEffect
effect: [baseEffect, conditionalEffect].filter(Boolean).join(' ')
};
});
}