import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; const sourceOrigin = process.argv[2] ?? 'https://dreams.wf-calc.net'; 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 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 singerMetadataOverrides = { m0334: { singerType: 'solo', singerCharacterIds: ['chr-04001'] } }; function singerType(value) { return typeof value === 'string' ? value.split('_').at(-1)?.toLowerCase() ?? null : null; } async function fetchText(url) { const response = await fetch(url); if (!response.ok) throw new Error(`${url} 요청 실패: ${response.status}`); return response.text(); } function requiredMatch(text, pattern, label) { const match = text.match(pattern); if (!match) throw new Error(`${label}을 찾을 수 없습니다.`); return match[1]; } function decodeTemplateLiteral(value) { return value.replaceAll('\\\\', '\\').replaceAll('\\`', '`').replaceAll('\\${', '${'); } function decodeSingleQuotedLiteral(value) { let decoded = ''; for (let index = 0; index < value.length; index += 1) { const character = value[index]; if (character !== '\\') { decoded += character; continue; } const escaped = value[index + 1]; index += 1; if (escaped === '\\' || escaped === "'") decoded += escaped; else if (escaped === 'n') decoded += '\n'; else if (escaped === 'r') decoded += '\r'; else if (escaped === 't') decoded += '\t'; else if (escaped === 'b') decoded += '\b'; else if (escaped === 'f') decoded += '\f'; else if (escaped === 'u') { const code = value.slice(index + 1, index + 5); decoded += String.fromCodePoint(Number.parseInt(code, 16)); index += 4; } else decoded += escaped; } return decoded; } function extractJsonParseArgument(moduleText) { const prefix = "JSON.parse('"; const start = moduleText.indexOf(prefix); if (start < 0) throw new Error('채보 모듈에서 JSON 데이터를 찾을 수 없습니다.'); const valueStart = start + prefix.length; for (let index = valueStart; index < moduleText.length; index += 1) { if (moduleText[index] !== "'") continue; let slashCount = 0; for (let cursor = index - 1; cursor >= valueStart && moduleText[cursor] === '\\'; cursor -= 1) { slashCount += 1; } if (slashCount % 2 === 0) { return decodeSingleQuotedLiteral(moduleText.slice(valueStart, index)); } } throw new Error('채보 JSON 문자열의 끝을 찾을 수 없습니다.'); } function flattenEvents(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 })) .filter((note) => Number.isFinite(note.timeSeconds)) .sort((left, right) => left.timeSeconds - right.timeSeconds || (left.type === 'mid' ? 1 : -1)); } function scoreRatioFromNotes(notes) { const countedMidTimes = new Set(); let weight = 0; for (const note of notes) { if (note.type === 'mid') { if (countedMidTimes.has(note.timeSeconds)) continue; countedMidTimes.add(note.timeSeconds); weight += 0.1; } else { weight += note.isFlick ? 1.05 : 1; } } return weight * chartScoreWeightCoefficient; } const home = await fetchText(sourceOrigin); const mainAsset = requiredMatch(home, /