feat: apply feedback updates and streamline online delivery
Publish GitHub Pages output / publish (push) Successful in 34s

This commit is contained in:
2026-09-01 22:21:45 +09:00
parent 98d5575463
commit c0f4060dec
36 changed files with 447 additions and 471 deletions
+80
View File
@@ -0,0 +1,80 @@
import { access } from 'node:fs/promises';
import path from 'node:path';
import { finalNoteScore } from '../src/lib/engine/score.ts';
const DIFFICULTIES = new Set(['easy', 'normal', 'hard', 'expert']);
function assertFiniteNumber(value, label, { minimum = -Infinity, maximum = Infinity } = {}) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < minimum || value > maximum) {
throw new Error(`${label} 값이 유효하지 않습니다: ${JSON.stringify(value)}`);
}
}
export async function assertValidScoreCalibrations(calibrations, options = {}) {
const chartDirectory = options.chartDirectory ?? path.resolve('static/data/charts');
if (calibrations?.schemaVersion !== 1 || !Array.isArray(calibrations.charts)) {
throw new Error('score-calibrations.json은 schemaVersion 1과 charts 배열이 필요합니다.');
}
const chartKeys = new Set();
for (const [chartIndex, calibration] of calibrations.charts.entries()) {
const prefix = `charts[${chartIndex}]`;
if (typeof calibration.songId !== 'string' || !/^m\d{4}$/.test(calibration.songId)) {
throw new Error(`${prefix}.songId 형식이 올바르지 않습니다: ${JSON.stringify(calibration.songId)}`);
}
const difficulty = typeof calibration.difficulty === 'string'
? calibration.difficulty.toLowerCase()
: '';
if (!DIFFICULTIES.has(difficulty)) {
throw new Error(`${prefix}.difficulty가 올바르지 않습니다: ${JSON.stringify(calibration.difficulty)}`);
}
const chartKey = `${calibration.songId}.${difficulty}`;
if (chartKeys.has(chartKey)) throw new Error(`중복 실측 보정 항목입니다: ${chartKey}`);
chartKeys.add(chartKey);
assertFiniteNumber(calibration.scoreRatio, `${prefix}.scoreRatio`, { minimum: Number.EPSILON });
if (!Array.isArray(calibration.measurements) || calibration.measurements.length === 0) {
throw new Error(`${prefix}.measurements에는 한 개 이상의 실측값이 필요합니다.`);
}
const measurementKeys = new Set();
for (const [measurementIndex, measurement] of calibration.measurements.entries()) {
const measurementPrefix = `${prefix}.measurements[${measurementIndex}]`;
assertFiniteNumber(measurement.overallPower, `${measurementPrefix}.overallPower`, { minimum: 1 });
assertFiniteNumber(
measurement.directSongScoreBonusPercent,
`${measurementPrefix}.directSongScoreBonusPercent`,
{ minimum: 0, maximum: 100 }
);
assertFiniteNumber(measurement.normalPerfectScore, `${measurementPrefix}.normalPerfectScore`, { minimum: 1 });
const measurementKey = [
measurement.overallPower,
measurement.directSongScoreBonusPercent,
measurement.normalPerfectScore
].join(':');
if (measurementKeys.has(measurementKey)) {
throw new Error(`${chartKey}에 중복 실측값이 있습니다: ${measurementKey}`);
}
measurementKeys.add(measurementKey);
const reproducedScore = finalNoteScore(
measurement.overallPower / calibration.scoreRatio,
measurement.directSongScoreBonusPercent
);
if (reproducedScore !== measurement.normalPerfectScore) {
throw new Error(
`${chartKey} 실측값을 재현하지 못했습니다: 기대 ${measurement.normalPerfectScore}, 계산 ${reproducedScore}`
);
}
}
if (difficulty === 'hard' || difficulty === 'expert') {
await access(path.join(chartDirectory, `${chartKey}.json`));
}
}
}