UNPKG

osu-taiko-stable

Version:

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

1,233 lines (1,182 loc) 46.1 kB
'use strict'; var osuClasses = require('osu-classes'); class TaikoNoMod extends osuClasses.NoMod { } class TaikoNoFail extends osuClasses.NoFail { } class TaikoEasy extends osuClasses.Easy { applyToDifficulty(difficulty) { super.applyToDifficulty(difficulty); difficulty.sliderMultiplier *= 0.8; } } class TaikoHidden extends osuClasses.Hidden { } class TaikoHardRock extends osuClasses.HardRock { applyToDifficulty(difficulty) { super.applyToDifficulty(difficulty); const sliderMultiplier = 1.4 * 4 / 3; difficulty.sliderMultiplier *= sliderMultiplier; } } class TaikoSuddenDeath extends osuClasses.SuddenDeath { } class TaikoDoubleTime extends osuClasses.DoubleTime { } class TaikoRelax extends osuClasses.Relax { } class TaikoHalfTime extends osuClasses.HalfTime { } class TaikoNightcore extends osuClasses.Nightcore { } class TaikoFlashlight extends osuClasses.Flashlight { } class TaikoAutoplay extends osuClasses.Autoplay { } class TaikoPerfect extends osuClasses.Perfect { } class TaikoCinema extends osuClasses.Cinema { } class TaikoModCombination extends osuClasses.ModCombination { get mode() { return 1; } get _availableMods() { return [ new TaikoNoMod(), new TaikoNoFail(), new TaikoEasy(), new TaikoHidden(), new TaikoHardRock(), new TaikoSuddenDeath(), new TaikoDoubleTime(), new TaikoRelax(), new TaikoHalfTime(), new TaikoNightcore(), new TaikoFlashlight(), new TaikoAutoplay(), new TaikoPerfect(), new TaikoCinema(), ]; } } class TaikoHitWindows extends osuClasses.HitWindows { isHitResultAllowed(result) { switch (result) { case osuClasses.HitResult.Great: case osuClasses.HitResult.Ok: case osuClasses.HitResult.Miss: return true; } return false; } _getRanges() { return TaikoHitWindows.TAIKO_RANGES; } } TaikoHitWindows.TAIKO_RANGES = [ new osuClasses.DifficultyRange(osuClasses.HitResult.Great, 50, 35, 20), new osuClasses.DifficultyRange(osuClasses.HitResult.Ok, 120, 80, 50), new osuClasses.DifficultyRange(osuClasses.HitResult.Miss, 135, 95, 70), ]; class TaikoHitObject extends osuClasses.HitObject { constructor() { super(...arguments); this.hitWindows = new TaikoHitWindows(); } } TaikoHitObject.DEFAULT_SIZE = Math.fround(0.475); class TaikoStrongHitObject extends TaikoHitObject { get isStrong() { return !!this.samples.find((s) => { return s.hitSound === osuClasses.HitSound[osuClasses.HitSound.Finish]; }); } set isStrong(value) { if (this.samples.length > 0) { this.samples[0].hitSound = osuClasses.HitSound[value ? osuClasses.HitSound.Finish : osuClasses.HitSound.Normal]; } } } TaikoStrongHitObject.STRONG_SCALE = Math.fround(1 / Math.fround(0.65)); TaikoStrongHitObject.DEFAULT_STRONG_SIZE = Math.fround(TaikoHitObject.DEFAULT_SIZE * TaikoStrongHitObject.STRONG_SCALE); class DrumRollTick extends TaikoHitObject { constructor() { super(...arguments); this.firstTick = false; this.tickInterval = 0; this.hitWindows = TaikoHitWindows.empty; } get hitWindow() { return this.tickInterval / 2; } clone() { const cloned = super.clone(); cloned.firstTick = this.firstTick; cloned.tickInterval = this.tickInterval; return cloned; } } class SwellTick extends TaikoHitObject { constructor() { super(...arguments); this.hitWindows = TaikoHitWindows.empty; } } class TaikoEventGenerator extends osuClasses.EventGenerator { static *generateDrumRollTicks(drumRoll) { if (drumRoll.tickInterval === 0) { return; } let firstTick = true; const tickInterval = drumRoll.tickInterval; let time = drumRoll.startTime; const endTime = drumRoll.endTime + tickInterval / 2; while (time < endTime) { const tick = new DrumRollTick(); tick.startTime = time; tick.tickInterval = tickInterval; tick.firstTick = firstTick; tick.hitType = drumRoll.hitType; tick.hitSound = drumRoll.hitSound; tick.samples = drumRoll.samples; firstTick = false; yield tick; time += tickInterval; } } static *generateSwellTicks(swell) { const requiredHits = swell.requiredHits; for (let hit = 0; hit < requiredHits; ++hit) { const tick = new SwellTick(); tick.startTime = swell.startTime; tick.hitType = swell.hitType; tick.hitSound = swell.hitSound; tick.samples = swell.samples; yield tick; } } } class DrumRoll extends TaikoStrongHitObject { constructor() { super(...arguments); this.tickInterval = 100; this.tickRate = 1; this.velocity = 1; this.duration = 0; this.path = new osuClasses.SliderPath(); this.nodeSamples = []; this.repeats = 0; this.hitWindows = TaikoHitWindows.empty; } 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 endTime() { return this.startTime + this.duration; } set endTime(value) { this.duration = value - this.startTime; } applyDefaultsToSelf(controlPoints, difficulty) { super.applyDefaultsToSelf(controlPoints, difficulty); const timingPoint = controlPoints.timingPointAt(this.startTime); const difficultyPoint = controlPoints.difficultyPointAt(this.startTime); const scoringDistance = DrumRoll.BASE_DISTANCE * difficulty.sliderMultiplier * difficultyPoint.sliderVelocity; this.velocity = scoringDistance / timingPoint.beatLength; this.tickInterval = timingPoint.beatLength / this.tickRate; } createNestedHitObjects() { this.nestedHitObjects = []; for (const nested of TaikoEventGenerator.generateDrumRollTicks(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.tickRate = this.tickRate; cloned.tickInterval = this.tickInterval; cloned.duration = this.duration; return cloned; } } DrumRoll.BASE_DISTANCE = 100; class Hit extends TaikoStrongHitObject { get isRim() { return !!this.samples.find((s) => { return s.hitSound === osuClasses.HitSound[osuClasses.HitSound.Clap] || s.hitSound === osuClasses.HitSound[osuClasses.HitSound.Whistle]; }); } } class Swell extends TaikoHitObject { constructor() { super(...arguments); this.requiredHits = 10; this.endTime = 0; this.hitWindows = TaikoHitWindows.empty; } get duration() { return this.endTime - this.startTime; } set duration(value) { this.endTime = this.startTime + value; } createNestedHitObjects() { this.nestedHitObjects = []; for (const nested of TaikoEventGenerator.generateSwellTicks(this)) { this.nestedHitObjects.push(nested); } } clone() { const cloned = super.clone(); cloned.endTime = this.endTime; cloned.requiredHits = this.requiredHits; return cloned; } } class TaikoBeatmap extends osuClasses.RulesetBeatmap { constructor() { super(...arguments); this.mods = new TaikoModCombination(); this.hitObjects = []; } get mode() { return 1; } get maxCombo() { return this.hitObjects.reduce((combo, obj) => { return obj instanceof Hit ? combo + 1 : combo; }, 0); } get hits() { return this.hitObjects.filter((h) => h instanceof Hit); } get drumRolls() { return this.hitObjects.filter((h) => h instanceof DrumRoll); } get swells() { return this.hitObjects.filter((h) => h instanceof Swell); } } class TaikoBeatmapConverter extends osuClasses.BeatmapConverter { constructor() { super(...arguments); this.isForCurrentRuleset = true; this.isForManiaRuleset = false; this.taikoDistance = 0; this.taikoDuration = 0; this.tickDistance = 0; this.tickInterval = 0; } canConvert(_) { return true; } convertBeatmap(original) { this.isForCurrentRuleset = original.originalMode === 1; this.isForManiaRuleset = original.originalMode === 3; const converted = super.convertBeatmap(original); converted.difficulty.sliderMultiplier *= TaikoBeatmapConverter.VELOCITY_MULTIPLIER; if (this.isForManiaRuleset) { const groups = {}; converted.hitObjects.forEach((hitObject) => { const key = hitObject.startTime; if (!groups[key]) groups[key] = []; groups[key].push(hitObject); }); const grouped = Object.values(groups); converted.hitObjects = grouped.map((hitObjects) => { const first = hitObjects[0]; if (hitObjects.length > 1 && first instanceof TaikoStrongHitObject) { first.isStrong = true; } return first; }); } return converted; } *convertHitObjects(beatmap) { const hitObjects = beatmap.hitObjects; for (const hitObject of hitObjects) { if (hitObject instanceof TaikoHitObject) { yield hitObject; continue; } for (const converted of this._convertHitObject(hitObject, beatmap)) { yield converted; } } } _convertHitObject(hitObject, beatmap) { const slidable = hitObject; const spinnable = hitObject; if (slidable.path) { return this._convertSlidableObject(slidable, beatmap); } if (typeof spinnable.endTime === 'number') { return this._convertSpinnableObject(spinnable, beatmap); } return this._convertHittableObject(hitObject); } *_convertHittableObject(hittable) { const converted = new Hit(); converted.startTime = hittable.startTime; converted.hitType = osuClasses.HitType.Normal | (hittable.hitType & osuClasses.HitType.NewCombo); converted.hitSound = hittable.hitSound; converted.samples = hittable.samples; yield converted; } *_convertSlidableObject(slidable, beatmap) { if (this._shouldConvertToHits(slidable, beatmap)) { const allSamples = slidable.nodeSamples; let sampleIndex = 0; let time = slidable.startTime; const endTime = time + this.taikoDuration + this.tickInterval / 8; while (time <= endTime) { const hit = new Hit(); hit.startTime = time; hit.samples = allSamples[sampleIndex]; hit.hitType = osuClasses.HitType.Normal; hit.hitSound = hit.samples.reduce((s, h) => { return s + osuClasses.HitSound[h.hitSound]; }, 0); yield hit; sampleIndex = (sampleIndex + 1) % allSamples.length; if (this.tickInterval < 1e-7) break; time += this.tickInterval; } } else { const sliderTickRate = beatmap.difficulty.sliderTickRate; const drumRoll = new DrumRoll(); drumRoll.duration = this.taikoDuration; drumRoll.tickRate = sliderTickRate === 3 ? 3 : 4; drumRoll.startTime = slidable.startTime; drumRoll.hitType = osuClasses.HitType.Slider | (slidable.hitType & osuClasses.HitType.NewCombo); drumRoll.hitSound = slidable.hitSound; drumRoll.samples = slidable.samples; yield drumRoll; } } *_convertSpinnableObject(spinnable, beatmap) { const baseOD = beatmap.difficulty.overallDifficulty; const difficultyRange = osuClasses.BeatmapDifficultySection.range(baseOD, 3, 5, 7.5); const hitMultiplier = TaikoBeatmapConverter.SWELL_HIT_MULTIPLIER * difficultyRange; const swell = new Swell(); swell.startTime = spinnable.startTime; swell.hitType = osuClasses.HitType.Spinner | (spinnable.hitType & osuClasses.HitType.NewCombo); swell.hitSound = spinnable.hitSound; swell.samples = spinnable.samples; swell.requiredHits = Math.trunc(Math.max(1, (swell.duration / 1000) * hitMultiplier)); yield swell; } _shouldConvertToHits(slidable, beatmap) { const timingPoint = beatmap.controlPoints.timingPointAt(slidable.startTime); const difficultyPoint = beatmap.controlPoints.difficultyPointAt(slidable.startTime); let beatLength = difficultyPoint.isLegacy ? timingPoint.beatLength * difficultyPoint.bpmMultiplier : timingPoint.beatLength / difficultyPoint.sliderVelocity; const sliderMultiplier = beatmap.difficulty.sliderMultiplier * TaikoBeatmapConverter.VELOCITY_MULTIPLIER; const sliderTickRate = beatmap.difficulty.sliderTickRate; const sliderScoringPointDistance = (sliderMultiplier / sliderTickRate) * TaikoBeatmapConverter.BASE_SCORING_DISTANCE; const spans = slidable.repeats + 1 || 1; this.taikoDistance = slidable.distance * spans * TaikoBeatmapConverter.VELOCITY_MULTIPLIER; const taikoVelocity = sliderScoringPointDistance * sliderTickRate; this.taikoDuration = Math.trunc((this.taikoDistance / taikoVelocity) * beatLength); if (this.isForCurrentRuleset) { this.tickInterval = 0; return false; } const osuVelocity = taikoVelocity * 1000 / beatLength; let tickMultiplier = 1; if (beatmap.fileFormat >= 8) { beatLength = timingPoint.beatLength; tickMultiplier = 1 / difficultyPoint.sliderVelocity; } this.tickDistance = (sliderScoringPointDistance / sliderTickRate) * tickMultiplier; this.tickInterval = Math.min(beatLength / sliderTickRate, this.taikoDuration / spans); return (this.tickInterval > 0 && (this.taikoDistance / osuVelocity) * 1000 < 2 * beatLength); } createBeatmap() { return new TaikoBeatmap(); } } TaikoBeatmapConverter.VELOCITY_MULTIPLIER = Math.fround(1.4); TaikoBeatmapConverter.BASE_SCORING_DISTANCE = 100; TaikoBeatmapConverter.SWELL_HIT_MULTIPLIER = Math.fround(1.65); class TaikoBeatmapProcessor extends osuClasses.BeatmapProcessor { } class TaikoDifficultyAttributes extends osuClasses.DifficultyAttributes { constructor() { super(...arguments); this.staminaDifficulty = 0; this.rhythmDifficulty = 0; this.colourDifficulty = 0; this.peakDifficulty = 0; this.greatHitWindow = 0; } } class TaikoPerformanceAttributes extends osuClasses.PerformanceAttributes { constructor(mods, totalPerformance) { super(mods, totalPerformance); this.difficultyPerformance = 0; this.accuracyPerformance = 0; this.effectiveMissCount = 0; this.mods = mods; } } class AlternatingMonoPattern { constructor() { this.monoStreaks = []; this.index = 0; } get firstHitObject() { return this.monoStreaks[0].firstHitObject; } isRepetitionOf(other) { return this.hasIdenticalMonoLength(other) && other.monoStreaks.length === this.monoStreaks.length && other.monoStreaks[0].isRim === this.monoStreaks[0].isRim; } hasIdenticalMonoLength(other) { return other.monoStreaks[0].runLength === this.monoStreaks[0].runLength; } } class MonoStreak { constructor() { this.hitObjects = []; this.index = 0; } get firstHitObject() { return this.hitObjects[0]; } get isRim() { var _a; return (_a = this.hitObjects[0].baseObject) === null || _a === void 0 ? void 0 : _a.isRim; } get runLength() { return this.hitObjects.length; } } class RepeatingHitPatterns { get firstHitObject() { return this.alternatingMonoPatterns[0].firstHitObject; } get repetitionInterval() { return this._repetitionInterval; } set repetitionInterval(value) { this._repetitionInterval = value; } constructor(previous) { this.alternatingMonoPatterns = []; this.previous = null; this._repetitionInterval = RepeatingHitPatterns.MAX_REPETITION_INTERVAL + 1; this.previous = previous; } _isRepetitionOf(other) { if (this.alternatingMonoPatterns.length !== other.alternatingMonoPatterns.length) { return false; } const length = Math.min(this.alternatingMonoPatterns.length, 2); for (let i = 0; i < length; ++i) { if (!this.alternatingMonoPatterns[i].hasIdenticalMonoLength(other.alternatingMonoPatterns[i])) { return false; } } return true; } findRepetitionInterval() { const MAX_REPETITION_INTERVAL = RepeatingHitPatterns.MAX_REPETITION_INTERVAL; if (this.previous === null) { this.repetitionInterval = MAX_REPETITION_INTERVAL + 1; return; } let other = this.previous; let interval = 1; while (interval < MAX_REPETITION_INTERVAL) { if (this._isRepetitionOf(other)) { this.repetitionInterval = Math.min(interval, MAX_REPETITION_INTERVAL); return; } other = other.previous; if (other === null) break; ++interval; } this.repetitionInterval = MAX_REPETITION_INTERVAL + 1; } } RepeatingHitPatterns.MAX_REPETITION_INTERVAL = 16; class TaikoColourDifficultyPreprocessor { static processAndAssign(hitObjects) { const hitPatterns = this._encode(hitObjects); for (const repeatingHitPattern of hitPatterns) { repeatingHitPattern.firstHitObject.colour.repeatingHitPattern = repeatingHitPattern; const length = repeatingHitPattern.alternatingMonoPatterns.length; for (let i = 0; i < length; ++i) { const monoPattern = repeatingHitPattern.alternatingMonoPatterns[i]; monoPattern.parent = repeatingHitPattern; monoPattern.index = i; monoPattern.firstHitObject.colour.alternatingMonoPattern = monoPattern; for (let j = 0; j < monoPattern.monoStreaks.length; ++j) { const monoStreak = monoPattern.monoStreaks[j]; monoStreak.parent = monoPattern; monoStreak.index = j; monoStreak.firstHitObject.colour.monoStreak = monoStreak; } } } } static _encode(data) { const monoStreaks = this._encodeMonoStreak(data); const alternatingMonoPatterns = this._encodeAlternatingMonoPattern(monoStreaks); const repeatingHitPatterns = this._encodeRepeatingHitPattern(alternatingMonoPatterns); return repeatingHitPatterns; } static _encodeMonoStreak(data) { const monoStreaks = []; let currentMonoStreak = null; for (let i = 0; i < data.length; ++i) { const taikoObject = data[i]; const previousObject = taikoObject.previousNote(0); const baseHit = taikoObject.baseObject; const previousHit = previousObject === null || previousObject === void 0 ? void 0 : previousObject.baseObject; if (currentMonoStreak === null || previousObject === null || (baseHit === null || baseHit === void 0 ? void 0 : baseHit.isRim) !== (previousHit === null || previousHit === void 0 ? void 0 : previousHit.isRim)) { currentMonoStreak = new MonoStreak(); monoStreaks.push(currentMonoStreak); } currentMonoStreak.hitObjects.push(taikoObject); } return monoStreaks; } static _encodeAlternatingMonoPattern(data) { const monoPatterns = []; let currentMonoPattern = null; for (let i = 0; i < data.length; ++i) { if (currentMonoPattern === null || data[i].runLength !== data[i - 1].runLength) { currentMonoPattern = new AlternatingMonoPattern(); monoPatterns.push(currentMonoPattern); } currentMonoPattern.monoStreaks.push(data[i]); } return monoPatterns; } static _encodeRepeatingHitPattern(data) { const hitPatterns = []; let currentHitPattern = null; for (let i = 0; i < data.length; ++i) { currentHitPattern = new RepeatingHitPatterns(currentHitPattern); let isCoupled = i < data.length - 2 && data[i].isRepetitionOf(data[i + 2]); if (!isCoupled) { currentHitPattern.alternatingMonoPatterns.push(data[i]); } else { while (isCoupled) { currentHitPattern.alternatingMonoPatterns.push(data[i]); i++; isCoupled = i < data.length - 2 && data[i].isRepetitionOf(data[i + 2]); } currentHitPattern.alternatingMonoPatterns.push(data[i]); currentHitPattern.alternatingMonoPatterns.push(data[i + 1]); i++; } hitPatterns.push(currentHitPattern); } for (let i = 0; i < hitPatterns.length; ++i) { hitPatterns[i].findRepetitionInterval(); } return hitPatterns; } } class TaikoDifficultyHitObjectColour { constructor() { this.monoStreak = null; this.alternatingMonoPattern = null; this.repeatingHitPattern = null; } } class TaikoDifficultyHitObjectRhythm { constructor(numerator, denominator, difficulty) { this.ratio = numerator / denominator; this.difficulty = difficulty; } } class TaikoDifficultyHitObject extends osuClasses.DifficultyHitObject { constructor(hitObject, lastObject, lastLastObject, clockRate, objects, centreHitObjects, rimHitObjects, noteObjects, index) { super(hitObject, lastObject, clockRate, objects, index); this._noteDifficultyHitObjects = noteObjects; this.colour = new TaikoDifficultyHitObjectColour(); this.rhythm = this._getClosestRhythm(lastObject, lastLastObject, clockRate); const taikoHit = hitObject; if ((taikoHit === null || taikoHit === void 0 ? void 0 : taikoHit.isRim) === true) { this.monoIndex = rimHitObjects.length; rimHitObjects.push(this); this._monoDifficultyHitObjects = rimHitObjects; } else if ((taikoHit === null || taikoHit === void 0 ? void 0 : taikoHit.isRim) === false) { this.monoIndex = centreHitObjects.length; centreHitObjects.push(this); this._monoDifficultyHitObjects = centreHitObjects; } else { this.monoIndex = 0; this._monoDifficultyHitObjects = []; } if (hitObject instanceof Hit) { this.noteIndex = noteObjects.length; noteObjects.push(this); } else { this.noteIndex = 0; } } _getClosestRhythm(lastObject, lastLastObject, clockRate) { const prevLength = (lastObject.startTime - lastLastObject.startTime) / clockRate; const ratio = this.deltaTime / prevLength; return TaikoDifficultyHitObject.COMMON_RHYTHMS.slice() .sort((a, b) => Math.abs(a.ratio - ratio) - Math.abs(b.ratio - ratio))[0]; } previousMono(backwardsIndex) { var _a, _b; const index = this.monoIndex - (backwardsIndex + 1); return (_b = (_a = this._monoDifficultyHitObjects) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : null; } nextMono(forwardsIndex) { var _a, _b; const index = this.monoIndex + (forwardsIndex + 1); return (_b = (_a = this._monoDifficultyHitObjects) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : null; } previousNote(backwardsIndex) { var _a; const index = this.noteIndex - (backwardsIndex + 1); return (_a = this._noteDifficultyHitObjects[index]) !== null && _a !== void 0 ? _a : null; } nextNote(forwardsIndex) { var _a; const index = this.noteIndex + (forwardsIndex + 1); return (_a = this._noteDifficultyHitObjects[index]) !== null && _a !== void 0 ? _a : null; } } TaikoDifficultyHitObject.COMMON_RHYTHMS = [ new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), new TaikoDifficultyHitObjectRhythm(4, 5, 0.7), ]; class ColourEvaluator { static _sigmoid(val, center, width, middle, height) { const sigmoid = Math.tanh(Math.E * -(val - center) / width); return sigmoid * (height / 2) + middle; } static evaluateDifficultyOfMono(monoStreak) { return this._sigmoid(monoStreak.index, 2, 2, 0.5, 1) * this.evaluateDifficultyOfAlternate(monoStreak.parent) * 0.5; } static evaluateDifficultyOfAlternate(alternatingMonoPattern) { return this._sigmoid(alternatingMonoPattern.index, 2, 2, 0.5, 1) * this.evaluateDifficultyOfRepeat(alternatingMonoPattern.parent); } static evaluateDifficultyOfRepeat(repeatingHitPattern) { return 2 * (1 - this._sigmoid(repeatingHitPattern.repetitionInterval, 2, 2, 0.5, 1)); } static evaluateDifficultyOf(hitObject) { const colour = hitObject.colour; let difficulty = 0; if ((colour === null || colour === void 0 ? void 0 : colour.monoStreak) !== null) { difficulty += this.evaluateDifficultyOfMono(colour.monoStreak); } if ((colour === null || colour === void 0 ? void 0 : colour.alternatingMonoPattern) !== null) { difficulty += this.evaluateDifficultyOfAlternate(colour.alternatingMonoPattern); } if ((colour === null || colour === void 0 ? void 0 : colour.repeatingHitPattern) !== null) { difficulty += this.evaluateDifficultyOfRepeat(colour.repeatingHitPattern); } return difficulty; } } class StaminaEvaluator { static _speedBonus(interval) { interval = Math.max(interval, 50); return 30 / interval; } static evaluateDifficultyOf(current) { if (!(current.baseObject instanceof Hit)) return 0; const taikoCurrent = current; const keyPrevious = taikoCurrent.previousMono(1); if (keyPrevious === null) return 0; let objectStrain = 0.5; objectStrain += this._speedBonus(taikoCurrent.startTime - keyPrevious.startTime); return objectStrain; } } class Colour extends osuClasses.StrainDecaySkill { constructor() { super(...arguments); this._skillMultiplier = 0.12; this._strainDecayBase = 0.8; } _strainValueOf(current) { return ColourEvaluator.evaluateDifficultyOf(current); } } class Rhythm extends osuClasses.StrainDecaySkill { constructor() { super(...arguments); this._skillMultiplier = 10; this._strainDecayBase = 0; this._rhythmHistory = new osuClasses.LimitedCapacityQueue(Rhythm.RHYTHM_HISTORY_MAX_LENGTH); this._currentRhythmStrain = 0; this._notesSinceRhythmChange = 0; } _strainValueOf(current) { if (!(current.baseObject instanceof Hit)) { this._resetRhythmAndStrain(); return 0; } this._currentRhythmStrain *= Rhythm.STRAIN_DECAY; const hitObject = current; this._notesSinceRhythmChange += 1; if (hitObject.rhythm.difficulty === 0) { return 0; } let objectStrain = hitObject.rhythm.difficulty; objectStrain *= this._repetitionPenalties(hitObject); objectStrain *= Rhythm._patternLengthPenalty(this._notesSinceRhythmChange); objectStrain *= this._speedPenalty(hitObject.deltaTime); this._notesSinceRhythmChange = 0; this._currentRhythmStrain += objectStrain; return this._currentRhythmStrain; } _repetitionPenalties(hitObject) { let penalty = 1; this._rhythmHistory.enqueue(hitObject); for (let mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= Rhythm.RHYTHM_HISTORY_MAX_LENGTH / 2; mostRecentPatternsToCompare++) { const startIndex = this._rhythmHistory.count - mostRecentPatternsToCompare - 1; for (let start = startIndex; start >= 0; start--) { if (!this._samePattern(start, mostRecentPatternsToCompare)) { continue; } const notesSince = hitObject.index - this._rhythmHistory.get(start).index; penalty *= Rhythm._repetitionPenalty(notesSince); break; } } return penalty; } _samePattern(start, mostRecentPatternsToCompare) { for (let i = 0; i < mostRecentPatternsToCompare; i++) { const firstRhythm = this._rhythmHistory.get(start + i).rhythm; const secondRhythm = this._rhythmHistory.get(this._rhythmHistory.count - mostRecentPatternsToCompare + i).rhythm; if (firstRhythm !== secondRhythm) return false; } return true; } static _repetitionPenalty(notesSince) { return Math.min(1.0, 0.032 * notesSince); } static _patternLengthPenalty(patternLength) { const shortPatternPenalty = Math.min(0.15 * patternLength, 1.0); const longPatternPenalty = osuClasses.MathUtils.clamp01(2.5 - 0.15 * patternLength); return Math.min(shortPatternPenalty, longPatternPenalty); } _speedPenalty(deltaTime) { if (deltaTime < 80) return 1; if (deltaTime < 210) return Math.max(0, 1.4 - 0.005 * deltaTime); this._resetRhythmAndStrain(); return 0; } _resetRhythmAndStrain() { this._currentRhythmStrain = 0.0; this._notesSinceRhythmChange = 0; } } Rhythm.STRAIN_DECAY = 0.96; Rhythm.RHYTHM_HISTORY_MAX_LENGTH = 8; class Stamina extends osuClasses.StrainDecaySkill { constructor() { super(...arguments); this._skillMultiplier = 1.1; this._strainDecayBase = 0.4; } _strainValueOf(current) { return StaminaEvaluator.evaluateDifficultyOf(current); } } var _a; class Peaks extends osuClasses.Skill { constructor(mods) { super(mods); this.rhythm = new Rhythm(mods); this.colour = new Colour(mods); this.stamina = new Stamina(mods); } get colourDifficultyValue() { return this.colour.difficultyValue * _a.COLOUR_SKILL_MULTIPLIER; } get rhythmDifficultyValue() { return this.rhythm.difficultyValue * _a.RHYTHM_SKILL_MULTIPLIER; } get staminaDifficultyValue() { return this.stamina.difficultyValue * _a.STAMINA_SKILL_MULTIPLIER; } _norm(p, ...values) { const sum = values.reduce((sum, x) => sum + Math.pow(x, p), 0); return Math.pow(sum, 1 / p); } process(current) { this.rhythm.process(current); this.colour.process(current); this.stamina.process(current); } get difficultyValue() { const peaks = []; const colourPeaks = [...this.colour.getCurrentStrainPeaks()]; const rhythmPeaks = [...this.rhythm.getCurrentStrainPeaks()]; const staminaPeaks = [...this.stamina.getCurrentStrainPeaks()]; for (let i = 0; i < colourPeaks.length; ++i) { const colourPeak = colourPeaks[i] * _a.COLOUR_SKILL_MULTIPLIER; const rhythmPeak = rhythmPeaks[i] * _a.RHYTHM_SKILL_MULTIPLIER; const staminaPeak = staminaPeaks[i] * _a.STAMINA_SKILL_MULTIPLIER; let peak = this._norm(1.5, colourPeak, staminaPeak); peak = this._norm(2, peak, rhythmPeak); if (peak > 0) peaks.push(peak); } let difficulty = 0; let weight = 1; for (const strain of peaks.sort((a, b) => b - a)) { difficulty += strain * weight; weight *= 0.9; } return difficulty; } } _a = Peaks; Peaks.FINAL_MULTIPLIER = 0.0625; Peaks.RHYTHM_SKILL_MULTIPLIER = 0.2 * _a.FINAL_MULTIPLIER; Peaks.COLOUR_SKILL_MULTIPLIER = 0.375 * _a.FINAL_MULTIPLIER; Peaks.STAMINA_SKILL_MULTIPLIER = 0.375 * _a.FINAL_MULTIPLIER; class TaikoDifficultyCalculator extends osuClasses.DifficultyCalculator { _createSkills(_, mods) { return [ new Peaks(mods), ]; } get difficultyMods() { return [ new TaikoDoubleTime(), new TaikoHalfTime(), new TaikoEasy(), new TaikoHardRock(), ]; } _createDifficultyHitObjects(beatmap, clockRate) { const difficultyObjects = []; const centreObjects = []; const rimObjects = []; const noteObjects = []; for (let i = 2; i < beatmap.hitObjects.length; ++i) { const hitObject = beatmap.hitObjects[i]; const lastObject = beatmap.hitObjects[i - 1]; const lastLastObject = beatmap.hitObjects[i - 2]; const difficultyObject = new TaikoDifficultyHitObject(hitObject, lastObject, lastLastObject, clockRate, difficultyObjects, centreObjects, rimObjects, noteObjects, difficultyObjects.length); difficultyObjects.push(difficultyObject); } TaikoColourDifficultyPreprocessor.processAndAssign(difficultyObjects); return difficultyObjects; } _createDifficultyAttributes(beatmap, mods, skills, clockRate) { if (beatmap.hitObjects.length === 0) { return new TaikoDifficultyAttributes(mods, 0); } const combined = skills[0]; const multiplier = TaikoDifficultyCalculator.DIFFICULTY_MULTIPLIER; const colourRating = combined.colourDifficultyValue * multiplier; const rhythmRating = combined.rhythmDifficultyValue * multiplier; const staminaRating = combined.staminaDifficultyValue * multiplier; const combinedRating = combined.difficultyValue * multiplier; let starRating = this._rescale(combinedRating * 1.4); if (beatmap.originalMode === 0) { starRating *= 0.925; if (colourRating < 2 && staminaRating > 8) { starRating *= 0.8; } } const hitWindows = new TaikoHitWindows(); hitWindows.setDifficulty(beatmap.difficulty.overallDifficulty); const attributes = new TaikoDifficultyAttributes(mods, starRating); attributes.staminaDifficulty = staminaRating; attributes.rhythmDifficulty = rhythmRating; attributes.colourDifficulty = colourRating; attributes.peakDifficulty = combinedRating; attributes.greatHitWindow = hitWindows.windowFor(osuClasses.HitResult.Great) / clockRate; attributes.maxCombo = beatmap.hitObjects.reduce((s, c) => s + (c instanceof Hit ? 1 : 0), 0); return attributes; } static _simpleColourPenalty(staminaDifficulty, colorDifficulty) { if (colorDifficulty <= 0) return 0.79 - 0.25; return 0.79 - Math.atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } _rescale(sr) { if (sr < 0) return sr; return 10.43 * Math.log(sr / 8 + 1); } } TaikoDifficultyCalculator.DIFFICULTY_MULTIPLIER = 1.35; class TaikoPerformanceCalculator extends osuClasses.PerformanceCalculator { constructor(ruleset, attributes, score) { super(ruleset, attributes, score); this._mods = new TaikoModCombination(); this._countGreat = 0; this._countOk = 0; this._countMeh = 0; this._countMiss = 0; this._accuracy = 1; this._effectiveMissCount = 0; this.attributes = attributes; this._addParams(attributes, score); } calculateAttributes(attributes, score) { this._addParams(attributes, score); if (!this.attributes) { return new TaikoPerformanceAttributes(this._mods, 0); } let multiplier = 1.13; if (this._mods.any(TaikoHidden)) { multiplier *= 1.075; } if (this._mods.any(TaikoEasy)) { multiplier *= 0.975; } const difficultyValue = this._computeDifficultyValue(); const accuracyValue = this._computeAccuracyValue(); const totalValue = Math.pow(Math.pow(difficultyValue, 1.1) + Math.pow(accuracyValue, 1.1), 1 / 1.1) * multiplier; const performance = new TaikoPerformanceAttributes(this._mods, totalValue); performance.difficultyPerformance = difficultyValue; performance.accuracyPerformance = accuracyValue; return performance; } _computeDifficultyValue() { const max = Math.max(1, this.attributes.starRating / 0.115); let difficultyValue = Math.pow(5 * max - 4, 2.25) / 1150; const lengthBonus = 1 + 0.1 * Math.min(1, this._totalHits / 1500); difficultyValue *= lengthBonus; difficultyValue *= Math.pow(0.986, this._effectiveMissCount); if (this._mods.any(TaikoEasy)) { difficultyValue *= 0.985; } if (this._mods.any(TaikoHidden)) { difficultyValue *= 1.025; } if (this._mods.any(TaikoHardRock)) { difficultyValue *= 1.05; } if (this._mods.any(TaikoFlashlight)) { difficultyValue *= 1.05 * lengthBonus; } return difficultyValue * Math.pow(this._accuracy, 2); } _computeAccuracyValue() { if (this.attributes.greatHitWindow <= 0) { return 0; } let accuracyValue = Math.pow(60 / this.attributes.greatHitWindow, 1.1) * Math.pow(this._accuracy, 8) * Math.pow(this.attributes.starRating, 0.4) * 27; const lengthBonus = Math.min(1.15, Math.pow(this._totalHits / 1500, 0.3)); accuracyValue *= lengthBonus; if (this._mods.any(TaikoFlashlight) && this._mods.any(TaikoHidden)) { accuracyValue *= Math.max(1.05, 1.075 * lengthBonus); } return accuracyValue; } _addParams(attributes, score) { var _a, _b, _c, _d, _e, _f; if (score) { this._mods = (_a = score === null || score === void 0 ? void 0 : score.mods) !== null && _a !== void 0 ? _a : new TaikoModCombination(); this._countGreat = (_b = score.statistics.get(osuClasses.HitResult.Great)) !== null && _b !== void 0 ? _b : this._countGreat; this._countOk = (_c = score.statistics.get(osuClasses.HitResult.Ok)) !== null && _c !== void 0 ? _c : this._countOk; this._countMeh = (_d = score.statistics.get(osuClasses.HitResult.Meh)) !== null && _d !== void 0 ? _d : this._countMeh; this._countMiss = (_e = score.statistics.get(osuClasses.HitResult.Miss)) !== null && _e !== void 0 ? _e : this._countMiss; this._accuracy = (_f = score.accuracy) !== null && _f !== void 0 ? _f : this._accuracy; } if (this._totalSuccessfulHits > 0) { this._effectiveMissCount = Math.max(1, 1000 / this._totalSuccessfulHits) * this._countMiss; } if (attributes) { this.attributes = attributes; } } get _totalHits() { return this._countGreat + this._countOk + this._countMeh + this._countMiss; } get _totalSuccessfulHits() { return this._countGreat + this._countOk + this._countMeh; } } exports.TaikoAction = void 0; (function (TaikoAction) { TaikoAction[TaikoAction["LeftRim"] = 0] = "LeftRim"; TaikoAction[TaikoAction["LeftCentre"] = 1] = "LeftCentre"; TaikoAction[TaikoAction["RightCentre"] = 2] = "RightCentre"; TaikoAction[TaikoAction["RightRim"] = 3] = "RightRim"; })(exports.TaikoAction || (exports.TaikoAction = {})); class TaikoReplayFrame extends osuClasses.ReplayFrame { constructor() { super(...arguments); this.actions = new Set(); } fromLegacy(currentFrame) { this.startTime = currentFrame.startTime; this.interval = currentFrame.interval; if (currentFrame.mouseRight1) { this.actions.add(exports.TaikoAction.LeftRim); } if (currentFrame.mouseRight2) { this.actions.add(exports.TaikoAction.RightRim); } if (currentFrame.mouseLeft1) { this.actions.add(exports.TaikoAction.LeftCentre); } if (currentFrame.mouseLeft2) { this.actions.add(exports.TaikoAction.RightCentre); } return this; } toLegacy() { let state = osuClasses.ReplayButtonState.None; if (this.actions.has(exports.TaikoAction.LeftRim)) { state |= osuClasses.ReplayButtonState.Right1; } if (this.actions.has(exports.TaikoAction.RightRim)) { state |= osuClasses.ReplayButtonState.Right2; } if (this.actions.has(exports.TaikoAction.LeftCentre)) { state |= osuClasses.ReplayButtonState.Left1; } if (this.actions.has(exports.TaikoAction.RightCentre)) { state |= osuClasses.ReplayButtonState.Left2; } return new osuClasses.LegacyReplayFrame(this.startTime, this.interval, new osuClasses.Vector2(0, 0), state); } } class TaikoReplayConverter extends osuClasses.ReplayConverter { _createConvertibleReplayFrame() { return new TaikoReplayFrame(); } _isConvertedReplayFrame(frame) { return frame instanceof TaikoReplayFrame; } } class TaikoRuleset extends osuClasses.Ruleset { get id() { return 1; } applyToBeatmap(beatmap) { return super.applyToBeatmap(beatmap); } applyToBeatmapWithMods(beatmap, mods) { return super.applyToBeatmapWithMods(beatmap, mods); } resetMods(beatmap) { return super.resetMods(beatmap); } createModCombination(input) { return new TaikoModCombination(input); } _createBeatmapProcessor() { return new TaikoBeatmapProcessor(); } _createBeatmapConverter() { return new TaikoBeatmapConverter(); } _createReplayConverter() { return new TaikoReplayConverter(); } createDifficultyCalculator(beatmap) { return new TaikoDifficultyCalculator(beatmap, this); } createPerformanceCalculator(attributes, score) { return new TaikoPerformanceCalculator(this, attributes, score); } } exports.AlternatingMonoPattern = AlternatingMonoPattern; exports.Colour = Colour; exports.DrumRoll = DrumRoll; exports.DrumRollTick = DrumRollTick; exports.Hit = Hit; exports.MonoStreak = MonoStreak; exports.Peaks = Peaks; exports.RepeatingHitPatterns = RepeatingHitPatterns; exports.Rhythm = Rhythm; exports.Stamina = Stamina; exports.Swell = Swell; exports.SwellTick = SwellTick; exports.TaikoAutoplay = TaikoAutoplay; exports.TaikoBeatmap = TaikoBeatmap; exports.TaikoBeatmapConverter = TaikoBeatmapConverter; exports.TaikoBeatmapProcessor = TaikoBeatmapProcessor; exports.TaikoCinema = TaikoCinema; exports.TaikoColourDifficultyPreprocessor = TaikoColourDifficultyPreprocessor; exports.TaikoDifficultyAttributes = TaikoDifficultyAttributes; exports.TaikoDifficultyCalculator = TaikoDifficultyCalculator; exports.TaikoDifficultyHitObject = TaikoDifficultyHitObject; exports.TaikoDifficultyHitObjectColour = TaikoDifficultyHitObjectColour; exports.TaikoDifficultyHitObjectRhythm = TaikoDifficultyHitObjectRhythm; exports.TaikoDoubleTime = TaikoDoubleTime; exports.TaikoEasy = TaikoEasy; exports.TaikoEventGenerator = TaikoEventGenerator; exports.TaikoFlashlight = TaikoFlashlight; exports.TaikoHalfTime = TaikoHalfTime; exports.TaikoHardRock = TaikoHardRock; exports.TaikoHidden = TaikoHidden; exports.TaikoHitObject = TaikoHitObject; exports.TaikoHitWindows = TaikoHitWindows; exports.TaikoModCombination = TaikoModCombination; exports.TaikoNightcore = TaikoNightcore; exports.TaikoNoFail = TaikoNoFail; exports.TaikoNoMod = TaikoNoMod; exports.TaikoPerfect = TaikoPerfect; exports.TaikoPerformanceAttributes = TaikoPerformanceAttributes; exports.TaikoPerformanceCalculator = TaikoPerformanceCalculator; exports.TaikoRelax = TaikoRelax; exports.TaikoReplayConverter = TaikoReplayConverter; exports.TaikoReplayFrame = TaikoReplayFrame; exports.TaikoRuleset = TaikoRuleset; exports.TaikoStrongHitObject = TaikoStrongHitObject; exports.TaikoSuddenDeath = TaikoSuddenDeath;