import { Course, Bar, Note, HitNote } from 'tja-parser'; export namespace Factor { export function filterHitNotes(course: Course) { const notes: HitNote[] = []; course.noteGroups.forEach((g) => { if (g instanceof Bar) { for (const note of g.getNotes()) { if (note instanceof HitNote) { notes.push(note); } } } }); return notes; } export function getAllNotes(course: Course) { const notes: { type: number, time: number }[] = []; for (const group of course.noteGroups) { if (group instanceof Bar) { notes.push(...group.getNotes().map((note) => ({ //@ts-expect-error type: note.type, time: note.getTimingMS() }))); } } return notes.sort((a, b) => a.time - b.time).filter(n => [1, 2, 3, 4].includes(n.type)); } // 밀도 관련 export function getAverageDensity(notes: HitNote[]) { return notes.length / (notes[notes.length - 1].getTimingMS() - notes[0].getTimingMS()) * 1000 } export function getPeakDensity(notes: HitNote[]) { let peakDensity = 0; for (let i = 0; i < notes.length; i++) { let noteCount = 1; for (let j = i + 1; j < notes.length && (notes[j].getTimingMS() - notes[i].getTimingMS() <= 1000); j++) { noteCount++; } if (peakDensity < noteCount) { peakDensity = noteCount; } } return peakDensity; } // BPM 관련 export function getAverageBPM(notes: HitNote[]) { const averageBPM = notes.reduce((p, note) => p + note.getBPM().valueOf(), 0) / notes.length; return averageBPM; } export function getBpmChange(notes: HitNote[]) { let bpmChange = 0; for (let i = 1; i < notes.length; i++) { if (Math.abs(notes[i].getBPM() - notes[i - 1].getBPM()) > 1.5) { bpmChange++; } }; return bpmChange; } // 복잡성 export function getComplexity(notes: HitNote[]) { let complexity = 0; for (let i = 2; i < notes.length; i++) { let localComplexity = 0; // ddk 또는 dkk류면 1, 아니면 0.5 if ( (notes[i].type % 2 === notes[i - 1].type % 2 && notes[i - 1].type % 2 !== notes[i - 2].type % 2) || (notes[i].type % 2 !== notes[i - 1].type % 2 && notes[i - 1].type % 2 === notes[i - 2].type % 2) ) { localComplexity = 1; } else { localComplexity = 0.5 } // 시간 차가 짧을 수록 complexity 증가 localComplexity *= (1 / (notes[i].getTimingMS() - notes[i - 1].getTimingMS()) + 1 / (notes[i - 1].getTimingMS() - notes[i - 2].getTimingMS())) complexity += localComplexity; } return complexity / notes.length; } // 스크롤 변화 export function getScrollChange(notes: HitNote[]) { let bpmChange = 0; for (let i = 1; i < notes.length; i++) { if (Math.abs(notes[i].getBPM() * notes[i].getScroll() - notes[i - 1].getBPM() * notes[i - 1].getScroll()) > 1.5) { bpmChange++; } }; return bpmChange; } } export function factorize(course: Course) { const notes = Factor.filterHitNotes(course) return { note_count: notes.length, density_avg: Factor.getAverageDensity(notes), density_peak: Factor.getPeakDensity(notes), bpm_avg: Factor.getAverageBPM(notes), bpm_change: Factor.getBpmChange(notes), complexity: Factor.getComplexity(notes), scroll_change: Factor.getScrollChange(notes) }; }