osu-catch-stable
Version:
osu!stable version of osu!catch ruleset based on osu!lazer source code.
957 lines (915 loc) • 37.1 kB
JavaScript
'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;