UNPKG

osu-catch-stable

Version:

osu!stable version of osu!catch ruleset based on osu!lazer source code.

957 lines (915 loc) 37.1 kB
'use strict'; var osuClasses = require('osu-classes'); class CatchHitObject extends osuClasses.HitObject { constructor() { super(...arguments); this.currentComboIndex = 0; this.comboIndex = 0; this.comboIndexWithOffsets = 0; this.comboOffset = 0; this.lastInCombo = false; this.isNewCombo = false; this.timePreempt = 1000; this.scale = 0.5; this.offsetX = 0; this._originalX = this.startX; } get startX() { return this.startPosition.x; } set startX(value) { this.startPosition.x = value; this._originalX = value; } get endX() { return this.effectiveX; } get originalX() { return Math.fround(this._originalX); } get effectiveX() { return Math.fround(this._originalX + this.offsetX); } get randomSeed() { return Math.trunc(this.startTime); } applyDefaultsToSelf(controlPoints, difficulty) { super.applyDefaultsToSelf(controlPoints, difficulty); this.timePreempt = Math.fround(osuClasses.BeatmapDifficultySection.range(difficulty.approachRate, 1800, 1200, 450)); const scale = Math.fround(Math.fround(0.7) * Math.fround(difficulty.circleSize - 5)); this.scale = Math.fround(Math.fround(1 - Math.fround(scale / 5)) / 2); } clone() { const cloned = super.clone(); cloned.timePreempt = this.timePreempt; cloned.scale = this.scale; cloned.offsetX = this.offsetX; cloned.currentComboIndex = this.currentComboIndex; cloned.comboIndex = this.comboIndex; cloned.comboIndexWithOffsets = this.comboIndexWithOffsets; cloned.comboOffset = this.comboOffset; cloned.lastInCombo = this.lastInCombo; return cloned; } } CatchHitObject.OBJECT_RADIUS = 64; class PalpableHitObject extends CatchHitObject { constructor() { super(...arguments); this.hyperDashTarget = null; this.distanceToHyperDash = 0; } get hasHyperDash() { return this.hyperDashTarget !== null; } clone() { const cloned = super.clone(); cloned.hyperDashTarget = this.hyperDashTarget; cloned.distanceToHyperDash = this.distanceToHyperDash; return cloned; } } class Banana extends PalpableHitObject { constructor() { super(...arguments); this.index = 0; } clone() { const cloned = super.clone(); cloned.index = this.index; return cloned; } } class Fruit extends PalpableHitObject { } class JuiceFruit extends Fruit { } class JuiceDroplet extends PalpableHitObject { } class JuiceTinyDroplet extends JuiceDroplet { } class CatchPlayfield { static calculateCatcherWidth(difficulty) { let scale = Math.fround(Math.fround(0.7) * Math.fround(difficulty.circleSize - 5)); scale = Math.fround(1 - Math.fround(scale / 5)); const catcherWidth = Math.fround(this.BASE_CATCHER_SIZE * Math.abs(scale)); return Math.fround(catcherWidth * this.ALLOWED_CATCH_RANGE); } static clampToPlayfield(value) { return osuClasses.MathUtils.clamp(value, 0, this.PLAYFIELD_WIDTH); } } CatchPlayfield.BASE_CATCHER_SIZE = 106.75; CatchPlayfield.ALLOWED_CATCH_RANGE = Math.fround(0.8); CatchPlayfield.PLAYFIELD_WIDTH = 512; class CatchEventGenerator extends osuClasses.EventGenerator { static *generateDroplets(stream) { let lastEvent = null; for (const event of osuClasses.EventGenerator.generate(stream)) { if (lastEvent !== null) { const sinceLastTick = event.startTime - lastEvent.startTime; if (sinceLastTick > 80) { let timeBetweenTiny = sinceLastTick; while (timeBetweenTiny > 100) { timeBetweenTiny /= 2; } let time = timeBetweenTiny; const endTime = sinceLastTick; while (time < endTime) { const startTime = time + lastEvent.startTime; const offset = lastEvent.progress + (time / sinceLastTick) * (event.progress - lastEvent.progress); const droplet = new JuiceTinyDroplet(); droplet.startTime = startTime; droplet.startX = CatchPlayfield.clampToPlayfield(stream.effectiveX + stream.path.positionAt(offset).x); yield droplet; time += timeBetweenTiny; } } } lastEvent = event; let nested; switch (event.eventType) { case osuClasses.SliderEventType.Head: case osuClasses.SliderEventType.Repeat: case osuClasses.SliderEventType.Tail: nested = new JuiceFruit(); nested.startTime = event.startTime; nested.startX = CatchPlayfield.clampToPlayfield(stream.effectiveX + stream.path.positionAt(event.progress).x); yield nested; break; case osuClasses.SliderEventType.Tick: nested = new JuiceDroplet(); nested.startTime = event.startTime; nested.startX = CatchPlayfield.clampToPlayfield(stream.effectiveX + stream.path.positionAt(event.progress).x); yield nested; } } } static *generateBananas(shower) { let tickInterval = shower.duration; while (tickInterval > 100) { tickInterval /= 2; } if (tickInterval <= 0) { return; } const endTime = shower.endTime; let time = shower.startTime; let index = -1; while (time <= endTime) { const banana = new Banana(); banana.startTime = time; banana.index = ++index; yield banana; time += tickInterval; } } } class BananaShower extends CatchHitObject { constructor() { super(...arguments); this.endTime = 0; } get duration() { return this.endTime - this.startTime; } set duration(value) { this.endTime = this.startTime + value; } createNestedHitObjects() { this.nestedHitObjects = []; for (const nested of CatchEventGenerator.generateBananas(this)) { this.nestedHitObjects.push(nested); } } clone() { const cloned = super.clone(); cloned.endTime = this.endTime; return cloned; } } class JuiceStream extends CatchHitObject { constructor() { super(...arguments); this.tickDistance = 100.0; this.velocity = 1.0; this.path = new osuClasses.SliderPath(); this.nodeSamples = []; this.repeats = 0; } get distance() { return this.path.distance; } set distance(value) { this.path.distance = value; } get spans() { return this.repeats + 1; } get spanDuration() { return this.duration / this.spans; } get duration() { return (this.spans * this.path.distance) / this.velocity; } set duration(value) { this.velocity = (this.spans * this.path.distance) / value; } get endTime() { return this.startTime + this.duration; } get endX() { return CatchPlayfield.clampToPlayfield(this.effectiveX + this.path.curvePositionAt(1, this.spans).x); } applyDefaultsToSelf(controlPoints, difficulty) { super.applyDefaultsToSelf(controlPoints, difficulty); const timingPoint = controlPoints.timingPointAt(this.startTime); const difficultyPoint = controlPoints.difficultyPointAt(this.startTime); const scoringDistance = JuiceStream.BASE_DISTANCE * difficulty.sliderMultiplier; const tickDistanceFactor = scoringDistance / difficulty.sliderTickRate; const velocityFactor = scoringDistance / timingPoint.beatLength; this.tickDistance = tickDistanceFactor * difficultyPoint.sliderVelocity; this.velocity = velocityFactor * difficultyPoint.sliderVelocity; } createNestedHitObjects() { this.nestedHitObjects = []; for (const nested of CatchEventGenerator.generateDroplets(this)) { this.nestedHitObjects.push(nested); } } clone() { const cloned = super.clone(); cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone())); cloned.velocity = this.velocity; cloned.repeats = this.repeats; cloned.path = this.path.clone(); cloned.tickDistance = this.tickDistance; cloned.legacyLastTickOffset = this.legacyLastTickOffset; return cloned; } } JuiceStream.BASE_DISTANCE = 100; class CatchNoMod extends osuClasses.NoMod { } class CatchNoFail extends osuClasses.NoFail { } class CatchEasy extends osuClasses.Easy { } class CatchHidden extends osuClasses.Hidden { } class CatchHardRock extends osuClasses.HardRock { constructor() { super(...arguments); this.multiplier = 1.12; } } class CatchSuddenDeath extends osuClasses.SuddenDeath { } class CatchDoubleTime extends osuClasses.DoubleTime { constructor() { super(...arguments); this.multiplier = 1.06; } } class CatchRelax extends osuClasses.Relax { } class CatchHalfTime extends osuClasses.HalfTime { } class CatchNightcore extends osuClasses.Nightcore { } class CatchFlashlight extends osuClasses.Flashlight { } class CatchAutoplay extends osuClasses.Autoplay { } class CatchPerfect extends osuClasses.Perfect { } class CatchCinema extends osuClasses.Cinema { } class CatchModCombination extends osuClasses.ModCombination { get mode() { return 2; } get _availableMods() { return [ new CatchNoMod(), new CatchNoFail(), new CatchEasy(), new CatchHidden(), new CatchHardRock(), new CatchSuddenDeath(), new CatchDoubleTime(), new CatchRelax(), new CatchHalfTime(), new CatchNightcore(), new CatchFlashlight(), new CatchAutoplay(), new CatchPerfect(), new CatchCinema(), ]; } } class CatchBeatmap extends osuClasses.RulesetBeatmap { constructor() { super(...arguments); this.mods = new CatchModCombination(); this.hitObjects = []; } get mode() { return 2; } get maxCombo() { return this.hitObjects.reduce((c, h) => { if (h instanceof Fruit) return c + 1; if (h instanceof BananaShower) return c; return c + h.nestedHitObjects.reduce((c, n) => { return c + (n instanceof JuiceTinyDroplet ? 0 : 1); }, 0); }, 0); } get fruits() { return this.hitObjects.filter((h) => h instanceof Fruit); } get juiceStreams() { return this.hitObjects.filter((h) => h instanceof JuiceStream); } get bananaShowers() { return this.hitObjects.filter((h) => h instanceof BananaShower); } } class CatchBeatmapConverter extends osuClasses.BeatmapConverter { canConvert(beatmap) { return beatmap.hitObjects.every((h) => { return Number.isFinite(h.startX); }); } *convertHitObjects(beatmap) { const hitObjects = beatmap.hitObjects; for (const hitObject of hitObjects) { if (hitObject instanceof CatchHitObject) { yield hitObject; continue; } yield this._convertHitObject(hitObject); } } _convertHitObject(hitObject) { const slidable = hitObject; const spinnable = hitObject; if (slidable.path) { return this._convertSlidableObject(slidable); } if (typeof spinnable.endTime === 'number') { return this._convertSpinnableObject(spinnable); } return this._convertHittableObject(hitObject); } _convertSlidableObject(slidable) { var _a; const converted = new JuiceStream(); this._copyBaseProperties(converted, slidable); converted.hitType = osuClasses.HitType.Slider | (slidable.hitType & osuClasses.HitType.NewCombo); converted.repeats = slidable.repeats; converted.nodeSamples = slidable.nodeSamples; converted.path = slidable.path; converted.legacyLastTickOffset = (_a = slidable === null || slidable === void 0 ? void 0 : slidable.legacyLastTickOffset) !== null && _a !== void 0 ? _a : 0; return converted; } _convertSpinnableObject(spinnable) { const converted = new BananaShower(); this._copyBaseProperties(converted, spinnable); converted.hitType = osuClasses.HitType.Spinner | (spinnable.hitType & osuClasses.HitType.NewCombo); converted.endTime = spinnable.endTime; return converted; } _convertHittableObject(hittable) { const converted = new Fruit(); this._copyBaseProperties(converted, hittable); converted.hitType |= hittable.hitType & osuClasses.HitType.NewCombo; return converted; } _copyBaseProperties(converted, obj) { var _a, _b, _c, _d; const posObj = obj; const comboObj = obj; converted.startX = (_a = posObj === null || posObj === void 0 ? void 0 : posObj.startX) !== null && _a !== void 0 ? _a : 0; converted.startY = (_b = posObj === null || posObj === void 0 ? void 0 : posObj.startY) !== null && _b !== void 0 ? _b : 192; converted.startTime = obj.startTime; converted.hitSound = obj.hitSound; converted.samples = obj.samples; converted.comboOffset = (_c = comboObj === null || comboObj === void 0 ? void 0 : comboObj.comboOffset) !== null && _c !== void 0 ? _c : 0; converted.isNewCombo = (_d = comboObj === null || comboObj === void 0 ? void 0 : comboObj.isNewCombo) !== null && _d !== void 0 ? _d : false; } createBeatmap() { return new CatchBeatmap(); } } class CatchBeatmapProcessor extends osuClasses.BeatmapProcessor { constructor() { super(...arguments); this._lastX = null; this._lastStartTime = 0; this._rng = new osuClasses.FastRandom(CatchBeatmapProcessor.RNG_SEED); } postProcess(beatmap) { super.postProcess(beatmap); this._applyXOffsets(beatmap); return beatmap; } _applyXOffsets(beatmap) { const shouldApplyHR = beatmap.mods.has(osuClasses.ModBitwise.HardRock); const hitObjects = beatmap.hitObjects; hitObjects.forEach((hitObject) => { hitObject.offsetX = 0; if (hitObject.hitType & osuClasses.HitType.Normal) { const fruit = hitObject; if (shouldApplyHR) { this._applyHardRockOffset(fruit); } } if (hitObject.hitType & osuClasses.HitType.Slider) { const juiceStream = hitObject; const controlPoints = juiceStream.path.controlPoints; this._lastX = Math.fround(juiceStream.originalX + controlPoints[controlPoints.length - 1].position.x); this._lastStartTime = juiceStream.startTime; const nestedHitObjects = juiceStream.nestedHitObjects; nestedHitObjects.forEach((nested) => { const juice = nested; juice.offsetX = 0; if (nested instanceof JuiceTinyDroplet) { juice.offsetX = osuClasses.MathUtils.clamp(this._rng.nextInt(-20, 20), -juice.originalX, Math.fround(CatchPlayfield.PLAYFIELD_WIDTH - juice.originalX)); } else if (nested instanceof JuiceDroplet) { this._rng.next(); } }); } if (hitObject.hitType & osuClasses.HitType.Spinner) { const bananaShower = hitObject; const bananas = bananaShower.nestedHitObjects; bananas.forEach((banana) => { banana.offsetX = Math.fround(this._rng.nextDouble() * CatchPlayfield.PLAYFIELD_WIDTH); this._rng.next(); this._rng.next(); this._rng.next(); }); } }); this._initialiseHyperDash(beatmap); } _applyHardRockOffset(hitObject) { let offsetPosition = hitObject.originalX; const startTime = hitObject.startTime; if (this._lastX === null) { this._lastX = offsetPosition; this._lastStartTime = startTime; return; } const xDiff = Math.fround(offsetPosition - this._lastX); const timeDiff = Math.trunc(startTime - this._lastStartTime); if (timeDiff > 1000) { this._lastX = offsetPosition; this._lastStartTime = startTime; return; } if (xDiff === 0) { offsetPosition = this._applyRandomOffset(offsetPosition, timeDiff / 4); hitObject.offsetX = Math.fround(offsetPosition - hitObject.originalX); return; } if (Math.abs(xDiff) < Math.trunc(timeDiff / 3)) { offsetPosition = this._applyOffset(offsetPosition, xDiff); } hitObject.offsetX = Math.fround(offsetPosition - hitObject.originalX); this._lastX = offsetPosition; this._lastStartTime = startTime; } _applyRandomOffset(position, maxOffset) { const left = !this._rng.nextBool(); const rand = Math.min(20, Math.fround(this._rng.nextInt(0, Math.max(0, maxOffset)))); const positionPlusRand = Math.fround(position + rand); const positionMinusRand = Math.fround(position - rand); if (left) { return positionMinusRand >= 0 ? positionMinusRand : positionPlusRand; } return positionPlusRand <= CatchPlayfield.PLAYFIELD_WIDTH ? positionPlusRand : positionMinusRand; } _applyOffset(position, amount) { const positionPlusAmount = Math.fround(position + amount); if (amount > 0) { if (positionPlusAmount < CatchPlayfield.PLAYFIELD_WIDTH) { return positionPlusAmount; } } else if (positionPlusAmount > 0) { return positionPlusAmount; } return position; } _initialiseHyperDash(beatmap) { const palpableObjects = []; const hitObjects = beatmap.hitObjects; hitObjects.forEach((hitObject) => { if (hitObject.hitType & osuClasses.HitType.Normal) { palpableObjects.push(hitObject); } else if (hitObject.hitType & osuClasses.HitType.Slider) { const juiceStream = hitObject; const nestedHitObjects = juiceStream.nestedHitObjects; nestedHitObjects.forEach((nested) => { if (!(nested instanceof JuiceTinyDroplet)) { palpableObjects.push(nested); } }); } }); osuClasses.SortHelper.introSort(palpableObjects, (a, b) => a.startTime - b.startTime); let halfCatcherWidth = CatchPlayfield.calculateCatcherWidth(beatmap.difficulty) / 2; halfCatcherWidth /= CatchPlayfield.ALLOWED_CATCH_RANGE; let lastDirection = 0; let lastExcess = halfCatcherWidth; for (let i = 0, len = palpableObjects.length - 1; i < len; ++i) { const current = palpableObjects[i]; const next = palpableObjects[i + 1]; current.hyperDashTarget = null; current.distanceToHyperDash = 0; const thisDirection = next.effectiveX > current.effectiveX ? 1 : -1; const timeToNext = next.startTime - current.startTime - Math.fround(Math.fround(1000 / 60) / 4); const distanceToNext = Math.abs(Math.fround(next.effectiveX - current.effectiveX)) - (lastDirection === thisDirection ? lastExcess : halfCatcherWidth); const distanceToHyper = Math.fround(timeToNext * CatchBeatmapProcessor.BASE_SPEED - distanceToNext); if (distanceToHyper < 0) { current.hyperDashTarget = next; lastExcess = halfCatcherWidth; } else { current.distanceToHyperDash = distanceToHyper; lastExcess = Math.max(0, Math.min(distanceToHyper, halfCatcherWidth)); } lastDirection = thisDirection; } } } CatchBeatmapProcessor.RNG_SEED = 1337; CatchBeatmapProcessor.BASE_SPEED = 1; class CatchDifficultyAttributes extends osuClasses.DifficultyAttributes { constructor() { super(...arguments); this.approachRate = 0; } } class CatchPerformanceAttributes extends osuClasses.PerformanceAttributes { constructor(mods, totalPerformance) { super(mods, totalPerformance); this.mods = mods; } } class CatchDifficultyHitObject extends osuClasses.DifficultyHitObject { constructor(hitObject, lastObject, clockRate, halfCatcherWidth, objects, index) { super(hitObject, lastObject, clockRate, objects, index); this.baseObject = hitObject; this.lastObject = lastObject; const normalizedRadius = CatchDifficultyHitObject._NORMALIZED_HITOBJECT_RADIUS; const scalingFactor = Math.fround(normalizedRadius / halfCatcherWidth); this.normalizedPosition = Math.fround(this.baseObject.effectiveX * scalingFactor); this.lastNormalizedPosition = Math.fround(this.lastObject.effectiveX * scalingFactor); this.strainTime = Math.max(40, this.deltaTime); } } CatchDifficultyHitObject._NORMALIZED_HITOBJECT_RADIUS = 41; class Movement extends osuClasses.StrainDecaySkill { constructor(mods, halfCatcherWidth, clockRate) { super(mods); this._skillMultiplier = 900; this._strainDecayBase = 0.2; this._decayWeight = 0.94; this._sectionLength = 750; this._lastDistanceMoved = 0; this._lastStrainTime = 0; this._halfCatcherWidth = halfCatcherWidth; this._catcherSpeedMultiplier = clockRate; } _strainValueOf(current) { var _a; const catchCurrent = current; (_a = this._lastPlayerPosition) !== null && _a !== void 0 ? _a : (this._lastPlayerPosition = catchCurrent.lastNormalizedPosition); const normalizedRaidus = Math.fround(Movement._NORMALIZED_HITOBJECT_RADIUS); const positioningError = Math.fround(Movement._ABSOLUTE_PLAYER_POSITIONING_ERROR); const directionBonus = Math.fround(Movement._DIRECTION_CHANGE_BONUS); const offset = Math.fround(normalizedRaidus - positioningError); let playerPosition = osuClasses.MathUtils.clamp(this._lastPlayerPosition, Math.fround(catchCurrent.normalizedPosition - offset), Math.fround(catchCurrent.normalizedPosition + offset)); const distanceMoved = Math.fround(playerPosition - this._lastPlayerPosition); const weightedStrainTime = catchCurrent.strainTime + 13 + (3 / this._catcherSpeedMultiplier); const sqrtStrain = Math.sqrt(weightedStrainTime); let distanceAddition = (Math.pow(Math.abs(distanceMoved), 1.3) / 510); let edgeDashBonus = 0; if (Math.abs(distanceMoved) > 0.1) { const signDiff = Math.sign(distanceMoved) !== Math.sign(this._lastDistanceMoved); if (Math.abs(this._lastDistanceMoved) > 0.1 && signDiff) { const bonusFactor = Math.fround(Math.min(50, Math.abs(distanceMoved)) / 50); const antiflowFactor = Math.max(Math.fround(Math.min(70, Math.abs(this._lastDistanceMoved)) / 70), 0.38); const sqrt = Math.sqrt(this._lastStrainTime + 16); const max = Math.max(1 - Math.pow(weightedStrainTime / 1000, 3), 0); distanceAddition += directionBonus / sqrt * bonusFactor * antiflowFactor * max; } const min = Math.min(Math.abs(distanceMoved), normalizedRaidus * 2); distanceAddition += 12.5 * min / (normalizedRaidus * 6) / sqrtStrain; } if (catchCurrent.lastObject.distanceToHyperDash <= 20) { if (!catchCurrent.lastObject.hasHyperDash) { edgeDashBonus += 5.7; } else { playerPosition = catchCurrent.normalizedPosition; } const hyperDash = Math.fround(Math.fround(20 - catchCurrent.lastObject.distanceToHyperDash) / 20); const min = Math.min(catchCurrent.strainTime * this._catcherSpeedMultiplier, 265); const pow = Math.pow((min / 265), 1.5); distanceAddition *= 1 + edgeDashBonus * hyperDash * pow; } this._lastPlayerPosition = playerPosition; this._lastDistanceMoved = distanceMoved; this._lastStrainTime = catchCurrent.strainTime; return distanceAddition / weightedStrainTime; } } Movement._ABSOLUTE_PLAYER_POSITIONING_ERROR = 16; Movement._NORMALIZED_HITOBJECT_RADIUS = 41; Movement._DIRECTION_CHANGE_BONUS = 21; class CatchDifficultyCalculator extends osuClasses.DifficultyCalculator { constructor() { super(...arguments); this._halfCatcherWidth = 0; } _createDifficultyAttributes(beatmap, mods, skills, clockRate) { if (beatmap.hitObjects.length === 0) { return new CatchDifficultyAttributes(mods, 0); } const approachRate = beatmap.difficulty.approachRate; const starMultiplier = CatchDifficultyCalculator._STAR_SCALING_FACTOR; const preempt = osuClasses.DifficultyRange.map(approachRate, 1800, 1200, 450) / clockRate; const starRating = Math.sqrt(skills[0].difficultyValue) * starMultiplier; const attributes = new CatchDifficultyAttributes(mods, starRating); attributes.approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0; const fruits = beatmap.hitObjects.reduce((c, h) => { return c + (h instanceof Fruit ? 1 : 0); }, 0); attributes.maxCombo = beatmap.hitObjects.reduce((c, h) => { if (!(h instanceof JuiceStream)) return c; return c + h.nestedHitObjects.reduce((c, n) => { return c + (n instanceof JuiceTinyDroplet ? 0 : 1); }, 0); }, fruits); return attributes; } _createDifficultyHitObjects(beatmap, clockRate) { let lastObject = null; const difficultyObjects = []; const hitObjects = beatmap.hitObjects .flatMap((obj) => obj instanceof JuiceStream ? obj.nestedHitObjects : obj) .sort((a, b) => a.startTime - b.startTime); for (const hitObject of hitObjects) { if (hitObject instanceof BananaShower) continue; if (hitObject instanceof JuiceTinyDroplet) continue; if (lastObject !== null) { const object = new CatchDifficultyHitObject(hitObject, lastObject, clockRate, this._halfCatcherWidth, difficultyObjects, difficultyObjects.length); difficultyObjects.push(object); } lastObject = hitObject; } return difficultyObjects; } _createSkills(beatmap, mods, clockRate) { const difficulty = beatmap.difficulty; const catcherWidth = CatchPlayfield.calculateCatcherWidth(difficulty); const catcherWidthScale = Math.fround(Math.max(0, Math.fround(difficulty.circleSize - 5.5)) * 0.0625); this._halfCatcherWidth = Math.fround(catcherWidth / 2); this._halfCatcherWidth = Math.fround(this._halfCatcherWidth * (1 - catcherWidthScale)); return [ new Movement(mods, this._halfCatcherWidth, clockRate), ]; } get difficultyMods() { return [ new CatchDoubleTime(), new CatchHalfTime(), new CatchHardRock(), new CatchEasy(), ]; } } CatchDifficultyCalculator._STAR_SCALING_FACTOR = 0.153; class CatchPerformanceCalculator extends osuClasses.PerformanceCalculator { constructor(ruleset, attributes, score) { super(ruleset, attributes, score); this._scoreMaxCombo = 0; this._mods = new CatchModCombination(); this._fruitsHit = 0; this._ticksHit = 0; this._tinyTicksHit = 0; this._tinyTicksMissed = 0; this._misses = 0; this.attributes = attributes; this._addParams(attributes, score); } calculateAttributes(attributes, score) { this._addParams(attributes, score); if (!this.attributes) { return new CatchPerformanceAttributes(this._mods, 0); } const max = Math.max(1.0, this.attributes.starRating / 0.0049); let totalValue = Math.pow(5.0 * max - 4.0, 2.0) / 100000.0; const lengthBonus = 0.95 + 0.3 * Math.min(1.0, this._totalComboHits / 2500.0) + (this._totalComboHits > 2500 ? Math.log10(this._totalComboHits / 2500.0) * 0.475 : 0.0); totalValue *= lengthBonus; totalValue *= Math.pow(0.97, this._misses); if (this.attributes.maxCombo > 0) { const scoreMaxCombo = Math.pow(this._scoreMaxCombo, 0.8); const maxCombo = Math.pow(this.attributes.maxCombo, 0.8); totalValue *= Math.min(scoreMaxCombo / maxCombo, 1.0); } const approachRate = this.attributes.approachRate; let approachRateFactor = 1; if (approachRate > 9) { approachRateFactor += 0.1 * (approachRate - 9); } if (approachRate > 10) { approachRateFactor += 0.1 * (approachRate - 10); } else if (approachRate < 8) { approachRateFactor += 0.025 * (8 - approachRate); } totalValue *= approachRateFactor; if (this._mods.has(osuClasses.ModBitwise.Hidden)) { if (approachRate <= 10) { totalValue *= 1.05 + 0.075 * (10 - approachRate); } else if (approachRate > 10) { totalValue *= 1.01 + 0.04 * (11 - Math.min(11, approachRate)); } } if (this._mods.has(osuClasses.ModBitwise.Flashlight)) { totalValue *= 1.35 * lengthBonus; } totalValue *= Math.pow(this._accuracy, 5.5); if (this._mods.has(osuClasses.ModBitwise.NoFail)) { totalValue *= 0.9; } return new CatchPerformanceAttributes(this._mods, totalValue); } _addParams(attributes, score) { var _a, _b, _c, _d, _e, _f, _g; if (score) { this._scoreMaxCombo = (_a = score.maxCombo) !== null && _a !== void 0 ? _a : this._scoreMaxCombo; this._mods = (_b = score === null || score === void 0 ? void 0 : score.mods) !== null && _b !== void 0 ? _b : this._mods; this._fruitsHit = (_c = score.statistics.get(osuClasses.HitResult.Great)) !== null && _c !== void 0 ? _c : this._fruitsHit; this._ticksHit = (_d = score.statistics.get(osuClasses.HitResult.LargeTickHit)) !== null && _d !== void 0 ? _d : this._ticksHit; this._tinyTicksHit = (_e = score.statistics.get(osuClasses.HitResult.SmallTickHit)) !== null && _e !== void 0 ? _e : this._tinyTicksHit; this._tinyTicksMissed = (_f = score.statistics.get(osuClasses.HitResult.SmallTickMiss)) !== null && _f !== void 0 ? _f : this._tinyTicksMissed; this._misses = (_g = score.statistics.get(osuClasses.HitResult.Miss)) !== null && _g !== void 0 ? _g : this._misses; } if (attributes) { this.attributes = attributes; } } get _accuracy() { return this._totalHits === 0 ? 0 : Math.min(Math.max(this._totalSuccessfulHits / this._totalHits, 0), 1); } get _totalHits() { return this._tinyTicksHit + this._ticksHit + this._fruitsHit + this._misses + this._tinyTicksMissed; } get _totalSuccessfulHits() { return this._tinyTicksHit + this._ticksHit + this._fruitsHit; } get _totalComboHits() { return this._misses + this._ticksHit + this._fruitsHit; } } exports.CatchAction = void 0; (function (CatchAction) { CatchAction[CatchAction["MoveLeft"] = 0] = "MoveLeft"; CatchAction[CatchAction["MoveRight"] = 1] = "MoveRight"; CatchAction[CatchAction["Dash"] = 2] = "Dash"; })(exports.CatchAction || (exports.CatchAction = {})); class CatchReplayFrame extends osuClasses.ReplayFrame { constructor() { super(...arguments); this.position = 0; this.isDashing = false; this.actions = new Set(); } fromLegacy(currentFrame, lastFrame) { this.startTime = currentFrame.startTime; this.interval = currentFrame.interval; this.position = currentFrame.position.x; this.isDashing = currentFrame.buttonState === osuClasses.ReplayButtonState.Left1; if (this.isDashing) { this.actions.add(exports.CatchAction.Dash); } if (lastFrame instanceof CatchReplayFrame) { if (this.position > lastFrame.position) { lastFrame.actions.add(exports.CatchAction.MoveRight); } if (this.position < lastFrame.position) { lastFrame.actions.add(exports.CatchAction.MoveLeft); } } return this; } toLegacy() { let state = osuClasses.ReplayButtonState.None; if (this.actions.has(exports.CatchAction.Dash)) { state |= osuClasses.ReplayButtonState.Left1; } return new osuClasses.LegacyReplayFrame(this.startTime, this.interval, new osuClasses.Vector2(this.position, 0), state); } } class CatchReplayConverter extends osuClasses.ReplayConverter { _createConvertibleReplayFrame() { return new CatchReplayFrame(); } _isConvertedReplayFrame(frame) { return frame instanceof CatchReplayFrame; } } class CatchRuleset extends osuClasses.Ruleset { get id() { return 2; } applyToBeatmap(beatmap) { return super.applyToBeatmap(beatmap); } applyToBeatmapWithMods(beatmap, mods) { return super.applyToBeatmapWithMods(beatmap, mods); } resetMods(beatmap) { return super.resetMods(beatmap); } createModCombination(input) { return new CatchModCombination(input); } _createBeatmapProcessor() { return new CatchBeatmapProcessor(); } _createBeatmapConverter() { return new CatchBeatmapConverter(); } _createReplayConverter() { return new CatchReplayConverter(); } createDifficultyCalculator(beatmap) { return new CatchDifficultyCalculator(beatmap, this); } createPerformanceCalculator(attributes, score) { return new CatchPerformanceCalculator(this, attributes, score); } } exports.Banana = Banana; exports.BananaShower = BananaShower; exports.CatchAutoplay = CatchAutoplay; exports.CatchBeatmap = CatchBeatmap; exports.CatchBeatmapConverter = CatchBeatmapConverter; exports.CatchBeatmapProcessor = CatchBeatmapProcessor; exports.CatchCinema = CatchCinema; exports.CatchDifficultyAttributes = CatchDifficultyAttributes; exports.CatchDifficultyCalculator = CatchDifficultyCalculator; exports.CatchDifficultyHitObject = CatchDifficultyHitObject; exports.CatchDoubleTime = CatchDoubleTime; exports.CatchEasy = CatchEasy; exports.CatchEventGenerator = CatchEventGenerator; exports.CatchFlashlight = CatchFlashlight; exports.CatchHalfTime = CatchHalfTime; exports.CatchHardRock = CatchHardRock; exports.CatchHidden = CatchHidden; exports.CatchHitObject = CatchHitObject; exports.CatchModCombination = CatchModCombination; exports.CatchNightcore = CatchNightcore; exports.CatchNoFail = CatchNoFail; exports.CatchNoMod = CatchNoMod; exports.CatchPerfect = CatchPerfect; exports.CatchPerformanceAttributes = CatchPerformanceAttributes; exports.CatchPerformanceCalculator = CatchPerformanceCalculator; exports.CatchRelax = CatchRelax; exports.CatchReplayConverter = CatchReplayConverter; exports.CatchReplayFrame = CatchReplayFrame; exports.CatchRuleset = CatchRuleset; exports.CatchSuddenDeath = CatchSuddenDeath; exports.Fruit = Fruit; exports.JuiceDroplet = JuiceDroplet; exports.JuiceFruit = JuiceFruit; exports.JuiceStream = JuiceStream; exports.JuiceTinyDroplet = JuiceTinyDroplet; exports.Movement = Movement; exports.PalpableHitObject = PalpableHitObject;