UNPKG

@osbjs/hitobjects-tiny-osbjs

Version:

Hit objects parser for tiny-osbjs.

329 lines (307 loc) 14.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var fs = require('fs'); var tinyOsbjs = require('@osbjs/tiny-osbjs'); const PlayfieldToStoryboardOffset = [64, 56]; function parseCircles(rawHitObjs) { const circles = []; const match = rawHitObjs.match(/-*\d+,-*\d+,-*\d+,\d+,\d+,\d+:\d+:\d+:\d+:\w*/g); if (match) { for (const cL of match) { const cAttr = cL.split(','); const playfieldPosition = [parseInt(cAttr[0]), parseInt(cAttr[1])]; circles.push({ position: tinyOsbjs.addVec(playfieldPosition, PlayfieldToStoryboardOffset), time: parseInt(cAttr[2]), }); } } return circles; } function parseSliderMultiplier(raw) { const sliderMultiplier = parseFloat(raw.match(/(?<=SliderMultiplier:)\d+\.?\d*/)[0]); return sliderMultiplier; } function calculateTravelDuration(beatLength, beatmapMultiplier, currentMultiplier, visualLength) { return Math.round((beatLength * ((visualLength * currentMultiplier) / beatmapMultiplier)) / 100); } function createBezierSegments(points, visualLength) { const startPoint = points[0], endPoint = points[points.length - 1]; const segments = []; const segmentCount = visualLength; let distance = 0; let prevPosition = startPoint; for (let i = 1; i <= segmentCount; i++) { let delta = i / segmentCount; let nextPosition = getPositionAtDelta$1(delta, points); distance += tinyOsbjs.lengthVec(tinyOsbjs.subVec(nextPosition, prevPosition)); segments.push({ distance, position: nextPosition }); prevPosition = nextPosition; } distance += tinyOsbjs.lengthVec(tinyOsbjs.subVec(endPoint, prevPosition)); segments.push({ distance, position: endPoint }); return segments; } // de_casteljau algorithm function getPositionAtDelta$1(delta, points) { let intermediatePoints = points.map((point) => tinyOsbjs.cloneVec(point)); for (let i = 1; i < points.length; i++) for (let j = 0; j < points.length - i; j++) { intermediatePoints[j] = [ intermediatePoints[j][0] * (1 - delta) + intermediatePoints[j + 1][0] * delta, intermediatePoints[j][1] * (1 - delta) + intermediatePoints[j + 1][1] * delta, ]; } return intermediatePoints[0]; } function createBezierSegmentGroup(points, visualLength) { const curves = []; const startPoint = points[0]; let _points = [startPoint]; let prevPoint = startPoint; points.forEach((currentPoint) => { if (tinyOsbjs.areEqualVecs(currentPoint, prevPoint)) { if (_points.length > 1) curves.push(createBezierSegments(_points, visualLength)); _points = []; } _points.push(tinyOsbjs.cloneVec(currentPoint)); prevPoint = currentPoint; }); if (_points.length > 1) curves.push(createBezierSegments(_points, visualLength)); return curves; } function createCatmulSegments(points, visualLength) { const startPoint = points[0], endPoint = points[points.length - 1]; const segmentCountEachGroup = visualLength / points.length; const segments = []; let distance = 0.0; let prevPosition = startPoint; for (let lineIndex = 0; lineIndex < points.length - 1; lineIndex++) { for (let i = 1; i <= segmentCountEachGroup; i++) { let delta = i / (segmentCountEachGroup + 1); const p1 = lineIndex > 0 ? points[lineIndex - 1] : points[lineIndex], p2 = points[lineIndex], p3 = points[lineIndex + 1], p4 = lineIndex < points.length - 2 ? points[lineIndex + 2] : p3; let nextPosition = getPositionAtDelta(delta, p1, p2, p3, p4); distance += tinyOsbjs.lengthVec(tinyOsbjs.subVec(nextPosition, prevPosition)); segments.push({ distance, position: nextPosition }); prevPosition = nextPosition; } } distance += tinyOsbjs.lengthVec(tinyOsbjs.subVec(endPoint, prevPosition)); segments.push({ distance, position: endPoint }); return segments; } function getPointPositionAtDelta(delta, p1, p2, p3, p4) { return (0.5 * ((-p1 + 3 * p2 - 3 * p3 + p4) * delta * delta * delta + (2 * p1 - 5 * p2 + 4 * p3 - p4) * delta * delta + (-p1 + p3) * delta + 2 * p2)); } function getPositionAtDelta(delta, p1, p2, p3, p4) { return [getPointPositionAtDelta(p1[0], p2[0], p3[0], p4[0], delta), getPointPositionAtDelta(p1[1], p2[1], p3[1], p4[1], delta)]; } function createCircleSegments(points) { const startPoint = points[0], midPoint = points[1], endPoint = points[2]; const d = 2 * (startPoint[0] * (midPoint[1] - endPoint[1]) + midPoint[0] * (endPoint[1] - startPoint[1]) + endPoint[0] * (startPoint[1] - midPoint[1])); const center = [ (tinyOsbjs.lengthSqrVec(startPoint) * (midPoint[1] - endPoint[1]) + tinyOsbjs.lengthSqrVec(midPoint) * (endPoint[1] - startPoint[1]) + tinyOsbjs.lengthSqrVec(endPoint) * (startPoint[1] - midPoint[1])) / d, (tinyOsbjs.lengthSqrVec(startPoint) * (endPoint[0] - midPoint[0]) + tinyOsbjs.lengthSqrVec(midPoint) * (startPoint[0] - endPoint[0]) + tinyOsbjs.lengthSqrVec(endPoint) * (midPoint[0] - startPoint[0])) / d, ]; const radius = tinyOsbjs.lengthVec(tinyOsbjs.subVec(startPoint, center)); let startAngle = Math.atan2(startPoint[1] - center[1], startPoint[0] - center[0]); let midAngle = Math.atan2(midPoint[1] - center[1], midPoint[0] - center[0]); let endAngle = Math.atan2(endPoint[1] - center[1], endPoint[0] - center[0]); while (midAngle < startAngle) midAngle += 2 * Math.PI; while (endAngle < startAngle) endAngle += 2 * Math.PI; if (midAngle > endAngle) endAngle -= 2 * Math.PI; const length = Math.abs((endAngle - startAngle) * radius); const segmentCount = Math.floor(length / 8); const segments = []; for (let i = 1; i < segmentCount; i++) { const progress = i / segmentCount; const angle = endAngle * progress + startAngle * (1 - progress); const position = [Math.cos(angle) * radius + center[0], Math.sin(angle) * radius + center[1]]; segments.push({ distance: progress * length, position }); } segments.push({ distance: length, position: endPoint }); return segments; } function createLineSegments(points, visualLength) { const startPoint = points[0], endPoint = points[1]; return [ { distance: 0, position: startPoint }, { distance: visualLength, position: endPoint }, ]; } function getPositionAtDistance(distance, segments) { console.log(distance, segments[segments.length - 1].distance); if (distance >= segments[segments.length - 1].distance) return segments[segments.length - 1].position; if (distance <= segments[0].distance) return segments[0].position; const nextSegmentIndex = segments.findIndex((segment) => segment.distance > distance); const { distance: nextDistance, position: nextPosition } = segments[nextSegmentIndex]; const { distance: prevDistance, position: prevPosition } = segments[nextSegmentIndex - 1]; // linearly interpolate const alpha = (distance - prevDistance) / (nextDistance - prevDistance); const positionAtDistance = tinyOsbjs.interpolateVec(prevPosition, nextPosition, alpha); return positionAtDistance; } function getBezierGroupPositionAtDistance(distance, curves) { let _distance = distance; for (let i = 0; i < curves.length; i++) { const segments = curves[i]; if (_distance < segments[segments.length - 1].distance) return getPositionAtDistance(_distance, segments); _distance -= segments[segments.length - 1].distance; } return getPositionAtDistance(_distance, curves[curves.length - 1]); } var CurveType; (function (CurveType) { CurveType["Bezier"] = "B"; CurveType["Catmull"] = "C"; CurveType["Linear"] = "L"; CurveType["Perfect"] = "P"; })(CurveType || (CurveType = {})); function createSlider(startTime, curveType, points, repeatCount, visualLength, beatLength, beatMultiplier, currentMultiplier) { const travelDuration = calculateTravelDuration(beatLength, beatMultiplier, currentMultiplier, visualLength); const startPoint = points[0], endPoint = points[points.length - 1]; const endTime = startTime + travelDuration * repeatCount; const segments = curveType == CurveType.Bezier ? createBezierSegmentGroup(points, visualLength) : curveType == CurveType.Catmull ? createCatmulSegments(points, visualLength) : curveType == CurveType.Perfect ? createCircleSegments(points) : createLineSegments(points, visualLength); return { startTime, endTime, positionAtTime: (time) => { if (time <= startTime) return startPoint; if (endTime <= time) return repeatCount % 2 == 0 ? startPoint : endPoint; let elapsedSinceStart = time - startTime; let repeatAtTime = 1; let progressDuration = elapsedSinceStart; while (progressDuration > travelDuration) { progressDuration -= travelDuration; repeatAtTime++; } const progress = repeatAtTime % 2 != 0 ? progressDuration / travelDuration : 1 - progressDuration / travelDuration; if (curveType == CurveType.Bezier) { return getBezierGroupPositionAtDistance(visualLength * progress, segments); } else { return getPositionAtDistance(visualLength * progress, segments); } }, }; } function parseSliders(rawHitObjs, beatMultiplier, timingPoints) { const sliders = []; const match = rawHitObjs.match(/-*\d+,-*\d+,-*\d+,\d+,\d+,(L|B|C|P)(\|-*\d+:-*\d+)+,\d+,\d+\.*\d*(,\d+(\|\d*)*,\d+:\d+(\|\d+:\d+)*,\d+:\d+:\d+:\d+:\w*)*/g); if (match) { for (const sL of match) { const sAttr = sL.split(','); const startTime = parseInt(sAttr[2]); const curves = sAttr[5].split('|'); const curveType = curves[0]; const curvePoints = curves .slice(1) .map((cp) => tinyOsbjs.addVec([parseInt(cp.split(':')[0]), parseInt(cp.split(':')[1])], PlayfieldToStoryboardOffset)); const points = [tinyOsbjs.addVec([parseInt(sAttr[0]), parseInt(sAttr[1])], PlayfieldToStoryboardOffset), ...curvePoints]; const repeatCount = parseInt(sAttr[6]); const visualLength = parseInt(sAttr[7]); const currentTimingPoint = timingPoints .filter((tP) => tP.startTime <= startTime && tP.uninherited) .sort((t1, t2) => t2.startTime - t1.startTime)[0]; const currentMultipliers = timingPoints .filter((tP) => tP.startTime <= startTime && !tP.uninherited && tP.startTime >= currentTimingPoint.startTime) .sort((t1, t2) => t2.startTime - t1.startTime); const currentMultiplier = currentMultipliers.length ? -currentMultipliers[0].beatLength / 100 : 1; sliders.push(createSlider(startTime, curveType, points, repeatCount, visualLength, currentTimingPoint.beatLength, beatMultiplier, currentMultiplier)); } } return sliders; } function parseTimingPoints(raw) { let match = raw .match(/(?<=\[TimingPoints]\r?\n)(.+\r?\n?)+/g) ?.toString() .match(/-?\d+,-?\d+\.?\d*,\d+,\d+,\d+,\d+,\d+,\d+/g); const timingPoints = []; if (match) { for (const timingPointLine of match) { const timingPointAttr = timingPointLine.split(','); timingPoints.push({ startTime: parseInt(timingPointAttr[0]), beatLength: parseFloat(timingPointAttr[1]), uninherited: timingPointAttr[6] === '1', }); } } return timingPoints; } /** * Get all beatmap hitobjects. * @param filepath Full path to osu file. */ function loadBeatmapHitobjects(filepath) { const raw = fs.readFileSync(filepath, 'utf-8'); const rawHitObjs = raw .match(/(?<=\[HitObjects]\r?\n)(.+\r?\n?)+/g) ?.toString() .trim(); if (!rawHitObjs) return { sliders: [], circles: [] }; const timingPoints = parseTimingPoints(raw); const beatMultiplier = parseSliderMultiplier(raw); const sliders = parseSliders(rawHitObjs, beatMultiplier, timingPoints); const circles = parseCircles(rawHitObjs); return { sliders, circles }; } /** * Find the first circle with a specific timestamp. * @param time Time in millisecond * @param circles Array of circles * @param maxAcceptableOffset Accept result in range [time - maxAcceptableOffset, time + maxAcceptableOffset] */ function findCircleAtTime(time, circles, maxAcceptableOffset = 5) { return circles.find((circle) => circle.time <= time + maxAcceptableOffset && circle.time >= time - maxAcceptableOffset); } /** * Find the first slider with a specific timestamp. * @param time Time in millisecond * @param sliders Array of sliders * @param maxAcceptableOffset Accept result in range [time - maxAcceptableOffset, time + maxAcceptableOffset] */ function findSliderAtTime(time, sliders, maxAcceptableOffset = 5) { return sliders.find((slider) => slider.startTime <= time + maxAcceptableOffset && slider.startTime >= time - maxAcceptableOffset); } /** * Returns hitobjects in a specific period. * @param startTime Start time in millisecond * @param endTime End time in millisecond * @param hitobjects Array of hit objects * @param maxAcceptableOffset Accept result in range [startTime - maxAcceptableOffset, endTime + maxAcceptableOffset] */ function filterHitObjectsInPeriod(startTime, endTime, hitobjects, maxAcceptableOffset = 5) { const { circles, sliders } = hitobjects; const filteredCircles = circles.filter((circle) => circle.time >= startTime - maxAcceptableOffset && circle.time <= endTime + maxAcceptableOffset); const filteredSliders = sliders.filter((slider) => slider.startTime >= startTime - maxAcceptableOffset && slider.startTime <= endTime + maxAcceptableOffset); return { circles: filteredCircles, sliders: filteredSliders }; } exports.filterHitObjectsInPeriod = filterHitObjectsInPeriod; exports.findCircleAtTime = findCircleAtTime; exports.findSliderAtTime = findSliderAtTime; exports.loadBeatmapHitobjects = loadBeatmapHitobjects;