UNPKG

koishi-plugin-beatsaber-bot

Version:

一个 用于 BeatSaber 的 koishi Bot 插件

548 lines (547 loc) 20 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.processUnderswings = exports.ColorType = exports.NoteLineLayer = exports.NoteCutDirection = exports.processSliceDetails = exports.processSliceSummary = exports.processAccGraphs = exports.processAccuracySpread = void 0; const bsorDecoder_1 = require("./bsorDecoder"); const beatleader_1 = require("./beatleader"); const NoteScoringType = { Ignore: -1, NoScore: 0, Normal: 1, SliderHead: 2, SliderTail: 3, BurstSliderHead: 4, BurstSliderElement: 5, }; function processAccuracySpread(replay) { if (replay == null) return null; let result = { totalLeftCount: 0.0, leftCount: [], leftTD: [], totalRightCount: 0.0, rightCount: [], rightTD: [], timeDeviation: [], maxCount: 0, maxTD: 0.0, maxTimeDeviation: 0.0, }; if (!replay?.notes?.length) return result; const timings = []; for (let i = 0; i <= 15; i++) { result.leftCount.push(0.0); result.leftTD.push(0.0); result.rightCount.push(0.0); result.rightTD.push(0.0); result.timeDeviation.push(0.0); timings.push([]); } for (let i = 0; i < replay.notes.length; i++) { const note = replay.notes[i]; if (note.eventType !== bsorDecoder_1.NoteEventType.good) continue; const noteData = decodeNoteData(note.noteID); if (noteData.scoringType !== NoteScoringType.Normal) continue; const acc = getAccForDistance(note.noteCutInfo.cutDistanceToCenter); const td = Math.abs(note.noteCutInfo.cutNormal.z); if (note.noteCutInfo.saberType === 0) { result.totalLeftCount += 1.0; result.leftCount[acc] += 1.0; result.leftTD[acc] += td; } else { result.totalRightCount += 1.0; result.rightCount[acc] += 1.0; result.rightTD[acc] += td; } result.timeDeviation[acc] += note.noteCutInfo.timeDeviation; timings[acc].push(note.noteCutInfo.timeDeviation); } for (let i = 0; i <= 15; i++) { //<-- Averages --- const totalCount = result.rightCount[i] + result.leftCount[i]; result.leftTD[i] = result.leftCount[i] > 0 ? result.leftTD[i] / result.leftCount[i] : null; result.rightTD[i] = result.rightCount[i] > 0 ? result.rightTD[i] / result.rightCount[i] : null; result.timeDeviation[i] = totalCount > 0 ? result.timeDeviation[i] / totalCount : null; result.leftCount[i] /= result.totalLeftCount; result.rightCount[i] /= result.totalRightCount; //<-- TimeDeviation --- result.timeDeviation[i] = getStandardDeviation(timings[i], result.timeDeviation[i]); //<-- Min / Max --- if (result.leftCount[i] > result.maxCount) result.maxCount = result.leftCount[i]; if (result.rightCount[i] > result.maxCount) result.maxCount = result.rightCount[i]; if (result.leftTD[i] > result.maxTD) result.maxTD = result.leftTD[i]; if (result.rightTD[i] > result.maxTD) result.maxTD = result.rightTD[i]; if (result.timeDeviation[i] > result.maxTimeDeviation) result.maxTimeDeviation = result.timeDeviation[i]; } return result; } exports.processAccuracySpread = processAccuracySpread; function decodeNoteData(noteId) { let result = {}; result.cutDirection = Math.round(noteId % 10); noteId /= 10; result.colorType = Math.round(noteId % 10); noteId /= 10; result.noteLineLayer = Math.round(noteId % 10); noteId /= 10; result.lineIndex = Math.round(noteId % 10); noteId /= 10; result.scoringType = Math.round((noteId -= 2) < -1 ? noteId + 3 : noteId); return result; } function getAccForDistance(cutDistanceToCenter) { let mul = 1 - cutDistanceToCenter / 0.3; if (mul > 1) mul = 1; if (mul < 0) mul = 0; return Math.round(15.0 * mul); } const SummaryGroup = { Midlanes: 0, Outerlanes: 1, Crossovers: 2, Forehands: 3, Backhands: 4, }; function getStandardDeviation(numArray, mean) { if (numArray.length === 0) return null; let sqrSum = 0.0; numArray.forEach((num) => { sqrSum += Math.pow(num - mean, 2); }); return Math.sqrt(sqrSum / numArray.length); } function getScore(noteCutInfo) { let score = 0.0; score += getAccForDistance(noteCutInfo.cutDistanceToCenter); score += getPreSwingScore(noteCutInfo.beforeCutRating); score += getPostSwingScore(noteCutInfo.afterCutRating); return score; } function getPreSwingScore(preSwingRating) { if (preSwingRating > 1) preSwingRating = 1; if (preSwingRating < 0) preSwingRating = 0; return Math.round(preSwingRating * 70); } function getPostSwingScore(postSwingRating) { if (postSwingRating > 1) postSwingRating = 1; if (postSwingRating < 0) postSwingRating = 0; return Math.round(postSwingRating * 30); } const accGraphResolution = 100; const smoothPeriodPercentage = 0.02; const weightFunctionSteepness = 3.0; function processAccGraphs(replay) { if (replay == null) return null; let result = { times: [], fullSwingBySaber: { red: [], blue: [], total: [] }, realScoreBySaber: { red: [], blue: [], total: [] }, }; if (!replay?.notes?.length) return result; const lastNoteTime = replay.notes[replay.notes.length - 1].eventTime; const distanceWeightFunction = (0, beatleader_1.createDistanceWeightFunction)(lastNoteTime * smoothPeriodPercentage, weightFunctionSteepness); for (let i = 0.0; i < accGraphResolution; i += 1.0) { const time = lastNoteTime * (i / (accGraphResolution - 1)); let sumsBySaber = { red: [0.0, 0.0], blue: [0.0, 0.0], total: [0.0, 0.0] }; let dividerBySaber = { red: 0.0, blue: 0.0, total: 0.0 }; for (let j = 0; j < replay.notes.length; j++) { const note = replay.notes[j]; if (note.eventType !== bsorDecoder_1.NoteEventType.good) continue; const noteData = decodeNoteData(note.noteID); if (noteData.scoringType !== NoteScoringType.Normal) continue; const weight = distanceWeightFunction.getWeight(note.eventTime - time); const acc = getAccForDistance(note.noteCutInfo.cutDistanceToCenter); const pre = getPreSwingScore(note.noteCutInfo.beforeCutRating); const post = getPostSwingScore(note.noteCutInfo.afterCutRating); const fullSwing = acc + 100; const realScore = acc + pre + post; sumsBySaber.total[0] += fullSwing * weight; sumsBySaber.total[1] += realScore * weight; dividerBySaber.total += weight; const saberType = note.noteCutInfo.saberType === 0 ? 'red' : 'blue'; sumsBySaber[saberType][0] += fullSwing * weight; sumsBySaber[saberType][1] += realScore * weight; dividerBySaber[saberType] += weight; } result.times.push(time); ['red', 'blue', 'total'].forEach(saberType => { result.fullSwingBySaber[saberType].push(dividerBySaber[saberType] === 0 ? 0.0 : sumsBySaber[saberType][0] / dividerBySaber[saberType]); result.realScoreBySaber[saberType].push(dividerBySaber[saberType] === 0 ? 0.0 : sumsBySaber[saberType][1] / dividerBySaber[saberType]); }); } return result; } exports.processAccGraphs = processAccGraphs; //endregion //region SliceSummary function processSliceSummary(replay) { if (replay == null) return null; function createEmptySummary(label) { return { label, left: { count: 0, averageScore: 0.0, averageTD: 0.0 }, right: { count: 0, averageScore: 0.0, averageTD: 0.0 }, }; } let result = [ createEmptySummary('Midlanes'), createEmptySummary('Outerlanes'), createEmptySummary('Crossovers'), createEmptySummary('Forehands'), createEmptySummary('Backhands'), ]; if (!replay?.notes?.length) return result; function getLaneSummaryEntry(noteLineIndex, saberType) { let summaryGroup = getLaneSummaryGroup(noteLineIndex, saberType); return saberType === 0 ? result[summaryGroup].left : result[summaryGroup].right; } function getParitySummaryEntry(noteCutInfo, saberType) { let summaryGroup = getParitySummaryGroup(noteCutInfo); return saberType === 0 ? result[summaryGroup].left : result[summaryGroup].right; } function addScore(handSummary, score, td) { handSummary.count += 1; handSummary.averageScore += score; handSummary.averageTD += td; } function applyAverage(handSummary) { if (handSummary.count === 0) return; handSummary.averageScore /= handSummary.count; handSummary.averageTD /= handSummary.count; } for (let i = 0; i < replay.notes.length; i++) { const note = replay.notes[i]; if (note.eventType !== bsorDecoder_1.NoteEventType.good) continue; const noteData = decodeNoteData(note.noteID); if (noteData.scoringType !== NoteScoringType.Normal) continue; let laneSummaryEntry = getLaneSummaryEntry(noteData.lineIndex, note.noteCutInfo.saberType); let paritySummaryEntry = getParitySummaryEntry(note.noteCutInfo, note.noteCutInfo.saberType); const score = getScore(note.noteCutInfo); const td = Math.abs(note.noteCutInfo.cutNormal.z); addScore(laneSummaryEntry, score, td); addScore(paritySummaryEntry, score, td); } result.forEach((summary) => { applyAverage(summary.left); applyAverage(summary.right); }); return result; } exports.processSliceSummary = processSliceSummary; //endregion //region SliceDetails function processSliceDetails(replay) { if (replay == null) return null; let result = { mainGrid: [], summaryGrids: [], }; for (let i = 0; i < 12; i++) { let mainGridCell = { count: 0, averageScore: 0.0, left: [], right: [] }; for (let j = 0; j < 9; j++) { mainGridCell.left.push({ count: 0, averageScore: 0.0 }); mainGridCell.right.push({ count: 0, averageScore: 0.0 }); } result.mainGrid.push(mainGridCell); } for (let summaryGroup = 0; summaryGroup < 5; summaryGroup++) { let summaryGrid = []; for (let i = 0; i < 12; i++) { summaryGrid.push({ count: 0, averageScore: 0.0 }); } result.summaryGrids.push(summaryGrid); } if (!replay?.notes?.length) return result; function addScore(cell, score) { cell.count += 1; cell.averageScore += score; } function applyAverageScore(cell) { if (cell.count === 0) return; cell.averageScore /= cell.count; } for (let i = 0; i < replay.notes.length; i++) { const note = replay.notes[i]; if (note.eventType !== bsorDecoder_1.NoteEventType.good) continue; const noteData = decodeNoteData(note.noteID); if (noteData.scoringType !== NoteScoringType.Normal) continue; let mainGridIndex = getMainGridIndex(noteData.noteLineLayer, noteData.lineIndex); let secondaryGridIndex = getSecondaryGridIndex(noteData.cutDirection); let laneSummaryGroup = getLaneSummaryGroup(noteData.lineIndex, note.noteCutInfo.saberType); let paritySummaryGroup = getParitySummaryGroup(note.noteCutInfo); const mainCell = result.mainGrid[mainGridIndex]; let secondaryCell; if (note.noteCutInfo.saberType === 0) { secondaryCell = mainCell.left[secondaryGridIndex]; } else { secondaryCell = mainCell.right[secondaryGridIndex]; } const laneSummaryCell = result.summaryGrids[laneSummaryGroup][mainGridIndex]; const paritySummaryCell = result.summaryGrids[paritySummaryGroup][mainGridIndex]; const score = getScore(note.noteCutInfo); addScore(mainCell, score); addScore(secondaryCell, score); addScore(laneSummaryCell, score); addScore(paritySummaryCell, score); } for (let i = 0; i < 12; i++) { const mainCell = result.mainGrid[i]; applyAverageScore(mainCell); for (let j = 0; j < 9; j++) { applyAverageScore(mainCell.left[j]); applyAverageScore(mainCell.right[j]); } } for (let summaryGroup = 0; summaryGroup < 5; summaryGroup++) { let summaryGrid = result.summaryGrids[summaryGroup]; for (let i = 0; i < 12; i++) { applyAverageScore(summaryGrid[i]); } } return result; } exports.processSliceDetails = processSliceDetails; exports.NoteCutDirection = { Up: 0, Down: 1, Left: 2, Right: 3, UpLeft: 4, UpRight: 5, DownLeft: 6, DownRight: 7, Any: 8, None: 9, }; exports.NoteLineLayer = { Base: 0, Upper: 1, Top: 2, }; exports.ColorType = { None: -1, ColorA: 0, ColorB: 1, }; function getMainGridIndex(noteLineLayer, noteLineIndex) { switch (noteLineLayer) { case exports.NoteLineLayer.Top: return noteLineIndex; case exports.NoteLineLayer.Upper: return noteLineIndex + 4; case exports.NoteLineLayer.Base: return noteLineIndex + 8; } return -1; } function getSecondaryGridIndex(noteCutDirection) { switch (noteCutDirection) { case exports.NoteCutDirection.UpLeft: return 0; case exports.NoteCutDirection.Up: return 1; case exports.NoteCutDirection.UpRight: return 2; case exports.NoteCutDirection.Left: return 3; case exports.NoteCutDirection.Any: return 4; case exports.NoteCutDirection.Right: return 5; case exports.NoteCutDirection.DownLeft: return 6; case exports.NoteCutDirection.Down: return 7; case exports.NoteCutDirection.DownRight: return 8; } return -1; } //endregion //region Utils function getParitySummaryGroup(noteCutInfo) { const isForehand = noteCutInfo.cutNormal.x < 0; return isForehand ? SummaryGroup.Forehands : SummaryGroup.Backhands; } function getLaneSummaryGroup(noteLineIndex, saberType) { switch (saberType) { case 0: if (noteLineIndex >= 2) return SummaryGroup.Crossovers; break; case 1: if (noteLineIndex <= 1) return SummaryGroup.Crossovers; break; } if (noteLineIndex === 1 || noteLineIndex === 2) return SummaryGroup.Midlanes; return SummaryGroup.Outerlanes; } //endregion const createMultiplierCounter = () => { const MAX_MULTIPLIER = 8; let value = 1; let progress = 1; const inc = () => { if (value >= MAX_MULTIPLIER) return MAX_MULTIPLIER; if (progress + 1 >= value * 2) { value *= 2; progress = 0; } else { progress++; } return value; }; const dec = () => { if (value > 1) { value /= 2; } progress = 1; return value; }; return { get: () => value, inc, dec, }; }; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const getNoteType = (noteId) => { if (!Number.isFinite(noteId) || noteId >= 80000) return null; return Math.floor(noteId / 10000); }; const getNoteScore = (noteData, noteCutInfo) => { let beforeCut = 0, accuracy = 0, afterCut = 0; if (noteData.scoringType === NoteScoringType.SliderTail) { beforeCut = 70; } else if (noteData.scoringType !== NoteScoringType.BurstSliderElement) { beforeCut = Math.round(clamp(noteCutInfo.beforeCutRating * 70, 0, 70)); } if (noteData.scoringType === NoteScoringType.SliderHead) { afterCut = 30; } else if (![NoteScoringType.BurstSliderElement, NoteScoringType.BurstSliderHead].includes(noteData.scoringType)) { afterCut = Math.round(clamp(noteCutInfo.afterCutRating * 30, 0, 30)); } if (noteData.scoringType === NoteScoringType.BurstSliderElement) { accuracy = 20; } else { accuracy = Math.round(15 * (1 - clamp(noteCutInfo.cutDistanceToCenter / 0.3, 0, 1))); } return beforeCut + accuracy + afterCut; }; function processUnderswings(replay) { if (!replay) return null; if (!replay?.notes?.length) return { count: 0, acc: 0, fcAcc: 0, noUnderswingsAcc: 0, noUnderswingsFcAcc: 0, score: 0, fcScore: 0, noUnderswingsScore: 0, noUnderswingsFcScore: 0, maxCutScore: 0, maxSongScore: 0, }; const multiplier = createMultiplierCounter(); const fcMultiplier = createMultiplierCounter(); const result = [...replay.notes, ...(replay.walls ?? [])] .map((e, idx) => ({ ...e, idx, eventTime: e?.eventTime ?? e?.time })) .sort((a, b) => (a.eventTime === b.eventTime ? a.idx - b.idx : a.eventTime - b.eventTime)) .reduce((acc, event) => { if (!Number.isFinite(event?.eventType) || bsorDecoder_1.NoteEventType.bomb === event.eventType) { acc.multiplier.dec(); return acc; } const noteData = decodeNoteData(event.noteID); if (noteData < NoteScoringType.Normal) return acc; const maxNoteScore = getNoteScore(noteData, { cutDistanceToCenter: 0, beforeCutRating: 1, afterCutRating: 1 }); switch (event.eventType) { case bsorDecoder_1.NoteEventType.good: const realScore = getNoteScore(noteData, event.noteCutInfo); const fullSwing = getNoteScore(noteData, { cutDistanceToCenter: event.noteCutInfo.cutDistanceToCenter, beforeCutRating: 1, afterCutRating: 1, }); acc.count++; acc.score += realScore * multiplier.get(); acc.noUnderswingsScore += fullSwing * multiplier.get(); acc.fcScore += realScore * fcMultiplier.get(); acc.noUnderswingsFcScore += fullSwing * fcMultiplier.get(); acc.maxCutScore += maxNoteScore * fcMultiplier.get(); acc.multiplier.inc(); break; case bsorDecoder_1.NoteEventType.bad: case bsorDecoder_1.NoteEventType.miss: acc.multiplier.dec(); break; } acc.maxSongScore += maxNoteScore * fcMultiplier.get(); acc.fcMultiplier.inc(); return acc; }, { count: 0, maxCutScore: 0, maxSongScore: 0, score: 0, fcScore: 0, noUnderswingsScore: 0, noUnderswingsFcScore: 0, multiplier, fcMultiplier, }); const { multiplier: foo, fcMultiplier: bar, ...rest } = result; return { ...rest, maxCutScore: result.maxCutScore, maxSongScore: result.maxSongScore, acc: result.maxSongScore ? (result.score / result.maxSongScore) * 100 : 0, fcAcc: result.maxCutScore ? (result.fcScore / result.maxCutScore) * 100 : 0, noUnderswingsAcc: result.maxSongScore ? (result.noUnderswingsScore / result.maxSongScore) * 100 : 0, noUnderswingsFcAcc: result.maxCutScore ? (result.noUnderswingsFcScore / result.maxCutScore) * 100 : 0, }; } exports.processUnderswings = processUnderswings;