UNPKG

osu-mania-stable

Version:

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

1,344 lines (1,301 loc) 76.8 kB
import { HitObject, EventGenerator, HitType, Autoplay, Cinema, DoubleTime, ModBitwise, ModType, Easy, Flashlight, HalfTime, HardRock, Hidden, NoMod, NoFail, SuddenDeath, Nightcore, Perfect, ModCombination, RulesetBeatmap, RoundHelper, HitSound, Vector2, BeatmapConverter, FastRandom, BeatmapProcessor, DifficultyAttributes, PerformanceAttributes, DifficultyHitObject, StrainDecaySkill, DifficultyCalculator, SortHelper, PerformanceCalculator, HitResult, ReplayFrame, LegacyReplayFrame, ReplayButtonState, ReplayConverter, HitWindows, Ruleset } from 'osu-classes'; class ManiaHitObject extends HitObject { constructor() { super(...arguments); this._originalColumn = 0; this._column = 0; } get originalColumn() { return this._originalColumn; } set originalColumn(value) { this._originalColumn = value; this._column = value; } get column() { return this._column; } set column(value) { this._column = value; } get startX() { return this._column; } get endX() { return this.startX; } clone() { const cloned = super.clone(); cloned.originalColumn = this.originalColumn; cloned.column = this.column; return cloned; } } class Note extends ManiaHitObject { } class HoldHead extends Note { } class HoldTail extends Note { } class HoldTick extends ManiaHitObject { } class ManiaEventGenerator extends EventGenerator { static *generateHoldTicks(hold) { if (hold.tickInterval === 0) { return; } const head = new HoldHead(); head.startTime = hold.startTime; head.column = hold.column; yield head; const tickInterval = hold.tickInterval; let time = hold.startTime + tickInterval; const endTime = hold.endTime - tickInterval; while (time <= endTime) { const tick = new HoldTick(); tick.startTime = time; yield tick; time += tickInterval; } const tail = new HoldTail(); tail.startTime = hold.endTime; tail.column = hold.column; yield tail; } } class Hold extends ManiaHitObject { constructor() { super(...arguments); this.tickInterval = 50; this.nodeSamples = []; this.endTime = 0; this.hitType = HitType.Hold; } get duration() { return this.endTime - this.startTime; } get head() { const head = this.nestedHitObjects.find((n) => n instanceof HoldHead); if (head) { head.startTime = this.startTime; } return head || null; } get tail() { const tail = this.nestedHitObjects.find((n) => n instanceof HoldTail); if (tail) { tail.startTime = this.endTime; } return tail || null; } get column() { return this._column; } set column(value) { this._column = value; if (this.head) { this.head.column = value; } if (this.tail) { this.tail.column = value; } } applyDefaultsToSelf(controlPoints, difficulty) { super.applyDefaultsToSelf(controlPoints, difficulty); const timingPoint = controlPoints.timingPointAt(this.startTime); this.tickInterval = timingPoint.beatLength / difficulty.sliderTickRate; } createNestedHitObjects() { this.nestedHitObjects = []; for (const nested of ManiaEventGenerator.generateHoldTicks(this)) { this.nestedHitObjects.push(nested); } } clone() { const cloned = super.clone(); cloned.endTime = this.endTime; cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone())); cloned.tickInterval = this.tickInterval; return cloned; } } class ManiaAutoplay extends Autoplay { } class ManiaCinema extends Cinema { } class ManiaDoubleTime extends DoubleTime { constructor() { super(...arguments); this.multiplier = 1; } } class ManiaDualStages { constructor() { this.name = 'Dual Stages'; this.acronym = 'DS'; this.bitwise = ModBitwise.KeyCoop; this.type = ModType.Conversion; this.multiplier = 1; this.isRanked = false; this.incompatibles = ModBitwise.None; } applyToConverter(converter) { converter.isDual = true; } } class ManiaEasy extends Easy { } class ManiaFadeIn { constructor() { this.name = 'Fade In'; this.acronym = 'FI'; this.bitwise = ModBitwise.FadeIn; this.type = ModType.DifficultyIncrease; this.multiplier = 1; this.isRanked = true; this.incompatibles = ModBitwise.Hidden | ModBitwise.Flashlight; } } class ManiaFlashlight extends Flashlight { constructor() { super(...arguments); this.multiplier = 1; this.incompatibles = ModBitwise.Hidden | ModBitwise.FadeIn; } } class ManiaHalfTime extends HalfTime { constructor() { super(...arguments); this.multiplier = 0.5; } } class ManiaHardRock extends HardRock { constructor() { super(...arguments); this.multiplier = 1; } } class ManiaHidden extends Hidden { constructor() { super(...arguments); this.multiplier = 1; this.incompatibles = ModBitwise.FadeIn | ModBitwise.Flashlight; } } class ManiaKeyMod { constructor() { this.type = ModType.Conversion; this.multiplier = 1; this.isRanked = true; this.incompatibles = ModBitwise.KeyMod; } applyToConverter(converter) { converter.targetColumns = this.keyCount; } } class ManiaKey1 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'One Key'; this.acronym = '1K'; this.bitwise = ModBitwise.Key1; this.keyCount = 1; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key1; } } class ManiaKey2 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Two Keys'; this.acronym = '2K'; this.bitwise = ModBitwise.Key2; this.keyCount = 2; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key2; } } class ManiaKey3 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Three Keys'; this.acronym = '3K'; this.bitwise = ModBitwise.Key3; this.keyCount = 3; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key3; } } class ManiaKey4 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Four Keys'; this.acronym = '4K'; this.bitwise = ModBitwise.Key4; this.keyCount = 4; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key4; } } class ManiaKey5 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Five Keys'; this.acronym = '5K'; this.bitwise = ModBitwise.Key5; this.keyCount = 5; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key5; } } class ManiaKey6 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Six Keys'; this.acronym = '6K'; this.bitwise = ModBitwise.Key6; this.keyCount = 6; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key6; } } class ManiaKey7 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Seven Keys'; this.acronym = '7K'; this.bitwise = ModBitwise.Key7; this.keyCount = 7; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key7; } } class ManiaKey8 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Eight Keys'; this.acronym = '8K'; this.bitwise = ModBitwise.Key8; this.keyCount = 8; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key8; } } class ManiaKey9 extends ManiaKeyMod { constructor() { super(...arguments); this.name = 'Nine Keys'; this.acronym = '9K'; this.bitwise = ModBitwise.Key9; this.keyCount = 9; this.incompatibles = ModBitwise.KeyMod ^ ModBitwise.Key9; } } class ManiaMirror { constructor() { this.name = 'Mirror'; this.acronym = 'MR'; this.bitwise = ModBitwise.Mirror; this.type = ModType.Conversion; this.multiplier = 1; this.isRanked = true; this.incompatibles = ModBitwise.None; } applyToBeatmap(beatmap) { beatmap.hitObjects.forEach((h) => { h.column = beatmap.totalColumns - 1 - h.column; }); } } class ManiaNoMod extends NoMod { } class ManiaNoFail extends NoFail { } class ManiaSuddenDeath extends SuddenDeath { } class ManiaNightcore extends Nightcore { } class ManiaPerfect extends Perfect { } class ManiaRandom { constructor() { this.name = 'Random'; this.acronym = 'RD'; this.bitwise = ModBitwise.Random; this.type = ModType.Conversion; this.multiplier = 1; this.isRanked = false; this.incompatibles = ModBitwise.None; } applyToBeatmap(beatmap) { const random = []; const shuffled = []; for (let i = 0; i < beatmap.totalColumns; ++i) { random.push(Math.random()); shuffled.push(i); } shuffled.sort((a, b) => random[shuffled.indexOf(a)] - random[shuffled.indexOf(b)]); beatmap.hitObjects.forEach((h) => (h.column = shuffled[h.column])); } } class ManiaModCombination extends ModCombination { get mode() { return 3; } get _availableMods() { return [ new ManiaNoMod(), new ManiaNoFail(), new ManiaEasy(), new ManiaHidden(), new ManiaFadeIn(), new ManiaHardRock(), new ManiaSuddenDeath(), new ManiaDoubleTime(), new ManiaHalfTime(), new ManiaNightcore(), new ManiaFlashlight(), new ManiaAutoplay(), new ManiaPerfect(), new ManiaCinema(), new ManiaKey1(), new ManiaKey2(), new ManiaKey3(), new ManiaKey4(), new ManiaKey5(), new ManiaKey6(), new ManiaKey7(), new ManiaKey8(), new ManiaKey9(), new ManiaRandom(), new ManiaDualStages(), new ManiaMirror(), ]; } } class ManiaBeatmap extends RulesetBeatmap { constructor(stage, columns) { super(); this.mods = new ManiaModCombination(); this.stages = []; this.originalTotalColumns = 0; this.hitObjects = []; this.stages.push(stage); this.originalTotalColumns = columns || stage.columns; } get mode() { return 3; } get totalColumns() { return this.stages.reduce((c, s) => c + s.columns, 0); } get maxCombo() { return this.hitObjects.reduce((combo, obj) => { if (obj instanceof Note) { return combo + 1; } if (obj instanceof Hold) { return combo + 1 + Math.trunc((obj.endTime - obj.startTime) / 100); } return combo; }, 0); } get notes() { return this.hitObjects.filter((h) => h instanceof Note); } get holds() { return this.hitObjects.filter((h) => h instanceof Hold); } } class PatternGenerator { constructor(hitObject, beatmap, originalBeatmap, previousPattern, rng) { this._conversionDiff = null; this.hitObject = hitObject; this.beatmap = beatmap; this.originalBeatmap = originalBeatmap; this.previousPattern = previousPattern; this.totalColumns = beatmap.totalColumns; this.randomStart = beatmap.totalColumns === 8 ? 1 : 0; this.rng = rng; } getColumn(position, allowSpecial = false) { if (allowSpecial && this.totalColumns === 8) { const divisor = Math.round(512 / 7 * 100000) / 100000; const x = Math.floor(position / divisor); return Math.max(0, Math.min(x, 6)) + 1; } const divisor = Math.round(512 / this.totalColumns * 100000) / 100000; const x = Math.floor(position / divisor); return Math.max(0, Math.min(x, this.totalColumns - 1)); } getRandomNoteCount(p2, p3, p4 = 0, p5 = 0, p6 = 0) { if (p2 < 0 || p2 > 1) { throw new Error('p2 is not in range 0-1'); } if (p3 < 0 || p3 > 1) { throw new Error('p3 is not in range 0-1'); } if (p4 < 0 || p4 > 1) { throw new Error('p4 is not in range 0-1'); } if (p5 < 0 || p5 > 1) { throw new Error('p5 is not in range 0-1'); } if (p6 < 0 || p6 > 1) { throw new Error('p6 is not in range 0-1'); } const value = this.rng.nextDouble(); if (value >= 1 - p6) return 6; if (value >= 1 - p5) return 5; if (value >= 1 - p4) return 4; if (value >= 1 - p3) return 3; return value >= 1 - p2 ? 2 : 1; } get conversionDifficulty() { if (this._conversionDiff !== null) { return this._conversionDiff; } const hitObjects = this.originalBeatmap.hitObjects; const firstObject = hitObjects[0]; const lastObject = hitObjects[hitObjects.length - 1]; const firstStartTime = firstObject.startTime || 0; const lastStartTime = lastObject.startTime || 0; const drain = lastStartTime - firstStartTime - this.originalBeatmap.totalBreakTime; let drainTime = Math.trunc(drain / 1000); if (drainTime === 0) drainTime = 10000; const difficulty = this.originalBeatmap.difficulty; this._conversionDiff = Math.max(4, Math.min(difficulty.approachRate, 7)); this._conversionDiff += difficulty.drainRate; this._conversionDiff /= 1.5; this._conversionDiff += (hitObjects.length / drainTime) * Math.fround(9); this._conversionDiff = Math.fround((this._conversionDiff / 38) * 5) / 1.15; this._conversionDiff = Math.min(this._conversionDiff, 12); return this._conversionDiff; } findAvailableColumn(column, options) { const patterns = options.patterns || []; const lowerBound = options.lowerBound || this.randomStart; const upperBound = options.upperBound || this.totalColumns; const nextColumn = options.nextColumn || (() => { return this.getRandomColumn(lowerBound, upperBound); }); const validate = options.validate || (() => true); const isValid = (c) => { return validate(c) !== false && !patterns.find((p) => p.columnHasObject(c)); }; if (isValid(column)) return column; let hasValidColumns = false; for (let i = lowerBound; i < upperBound; ++i) { hasValidColumns = isValid(i); if (hasValidColumns) break; } if (!hasValidColumns) { throw new Error('There were not enough columns to complete conversion.'); } do { column = nextColumn(column); } while (!isValid(column)); return column; } getRandomColumn(lowerBound, upperBound) { lowerBound = lowerBound || this.randomStart; upperBound = upperBound || this.totalColumns; return this.rng.nextInt(lowerBound, upperBound); } } var PatternType; (function (PatternType) { PatternType[PatternType["None"] = 0] = "None"; PatternType[PatternType["ForceStack"] = 1] = "ForceStack"; PatternType[PatternType["ForceNotStack"] = 2] = "ForceNotStack"; PatternType[PatternType["KeepSingle"] = 4] = "KeepSingle"; PatternType[PatternType["LowProbability"] = 8] = "LowProbability"; PatternType[PatternType["Alternate"] = 16] = "Alternate"; PatternType[PatternType["ForceSigSlider"] = 32] = "ForceSigSlider"; PatternType[PatternType["ForceNotSlider"] = 64] = "ForceNotSlider"; PatternType[PatternType["Gathered"] = 128] = "Gathered"; PatternType[PatternType["Mirror"] = 256] = "Mirror"; PatternType[PatternType["Reverse"] = 512] = "Reverse"; PatternType[PatternType["Cycle"] = 1024] = "Cycle"; PatternType[PatternType["Stair"] = 2048] = "Stair"; PatternType[PatternType["ReverseStair"] = 4096] = "ReverseStair"; })(PatternType || (PatternType = {})); class Pattern { constructor() { this.hitObjects = []; this.containedColumns = new Set(); } columnHasObject(column) { return this.containedColumns.has(column); } get columnsWithObjects() { return this.containedColumns.size; } addHitObject(hitObject) { this.hitObjects.push(hitObject); this.containedColumns.add(hitObject.column); } addPatternHitObjects(other) { this.hitObjects.push(...other.hitObjects); other.hitObjects.forEach((h) => { this.containedColumns.add(h.column); }); } clear() { this.hitObjects.length = 0; this.containedColumns.clear(); } } class DistanceObjectPatternGenerator extends PatternGenerator { constructor(hitObject, beatmap, originalBeatmap, previousPattern, rng) { super(hitObject, beatmap, originalBeatmap, previousPattern, rng); this.convertType = PatternType.None; if (!beatmap.controlPoints.effectPointAt(hitObject.startTime).kiai) { this.convertType = PatternType.LowProbability; } const slider = hitObject; const timingPoint = beatmap.controlPoints .timingPointAt(hitObject.startTime); const difficultyPoint = beatmap.controlPoints.difficultyPointAt(hitObject.startTime); const beatLength = timingPoint.beatLength * difficultyPoint.bpmMultiplier; this.spanCount = slider.repeats + 1 || 1; this.startTime = RoundHelper.round(hitObject.startTime); const sliderMultiplier = beatmap.difficulty.sliderMultiplier; this.endTime = ((slider.path.distance || 0) * beatLength * this.spanCount * 0.01) / sliderMultiplier; this.endTime = Math.trunc(Math.floor(this.startTime + this.endTime)); const duration = this.endTime - this.startTime; this.segmentDuration = (duration / this.spanCount) >> 0; } *generate() { const originalPattern = this.generatePattern(); if (originalPattern.hitObjects.length === 1) { return yield originalPattern; } const intermediatePattern = new Pattern(); const endTimePattern = new Pattern(); for (const hitObject of originalPattern.hitObjects) { let endTime = hitObject.endTime; endTime = endTime || hitObject.startTime; if (this.endTime !== RoundHelper.round(endTime)) { intermediatePattern.addHitObject(hitObject); } else { endTimePattern.addHitObject(hitObject); } } yield intermediatePattern; yield endTimePattern; } generatePattern() { if (this.totalColumns === 1) { const pattern = new Pattern(); this.addToPattern(pattern, 0, this.startTime, this.endTime); return pattern; } if (this.spanCount > 1) { if (this.segmentDuration <= 90) { return this.generateRandomHoldNotes(this.startTime, 1); } if (this.segmentDuration <= 120) { this.convertType |= PatternType.ForceNotStack; return this.generateRandomNotes(this.startTime, this.spanCount + 1); } if (this.segmentDuration <= 160) { return this.generateStair(this.startTime); } if (this.segmentDuration <= 200 && this.conversionDifficulty > 3) { return this.generateRandomMultipleNotes(this.startTime); } if (this.endTime - this.startTime >= 4000) { return this.generateNRandomNotes(this.startTime, 0.23, 0, 0); } const columns = this.totalColumns - 1 - this.randomStart; if (this.segmentDuration > 400 && this.spanCount < columns) { return this.generateTiledHoldNotes(this.startTime); } return this.generateHoldAndNormalNotes(this.startTime); } if (this.segmentDuration <= 110) { if (this.previousPattern.columnsWithObjects < this.totalColumns) { this.convertType |= PatternType.ForceNotStack; } else { this.convertType &= ~PatternType.ForceNotStack; } const noteCount = this.segmentDuration < 80 ? 1 : 2; return this.generateRandomNotes(this.startTime, noteCount); } if (this.conversionDifficulty > 6.5) { if (this.convertType & PatternType.LowProbability) { return this.generateNRandomNotes(this.startTime, 0.78, 0.3, 0); } return this.generateNRandomNotes(this.startTime, 0.85, 0.36, 0.03); } if (this.conversionDifficulty > 4) { if (this.convertType & PatternType.LowProbability) { return this.generateNRandomNotes(this.startTime, 0.43, 0.08, 0); } return this.generateNRandomNotes(this.startTime, 0.56, 0.18, 0); } if (this.conversionDifficulty > 2.5) { if (this.convertType & PatternType.LowProbability) { return this.generateNRandomNotes(this.startTime, 0.3, 0, 0); } return this.generateNRandomNotes(this.startTime, 0.37, 0.08, 0); } if (this.convertType & PatternType.LowProbability) { return this.generateNRandomNotes(this.startTime, 0.17, 0, 0); } return this.generateNRandomNotes(this.startTime, 0.27, 0, 0); } generateRandomHoldNotes(startTime, noteCount) { const pattern = new Pattern(); const usableColumns = this.totalColumns - this.randomStart - this.previousPattern.columnsWithObjects; let column = this.getRandomColumn(); for (let i = 0, len = Math.min(usableColumns, noteCount); i < len; ++i) { const options = { patterns: [pattern, this.previousPattern], }; column = this.findAvailableColumn(column, options); this.addToPattern(pattern, column, startTime, this.endTime); } for (let i = 0, len = noteCount - usableColumns; i < len; ++i) { const options = { patterns: [pattern], }; column = this.findAvailableColumn(column, options); this.addToPattern(pattern, column, startTime, this.endTime); } return pattern; } generateRandomNotes(startTime, noteCount) { const pattern = new Pattern(); const startX = this.hitObject.startX; let column = this.getColumn(startX || 0, true); const isForceNotStack = !!(this.convertType & PatternType.ForceNotStack); const lessThanTotalColumns = this.previousPattern.columnsWithObjects < this.totalColumns; if (isForceNotStack && lessThanTotalColumns) { const options = { patterns: [this.previousPattern], }; column = this.findAvailableColumn(column, options); } let lastColumn = column; for (let i = 0; i < noteCount; ++i) { this.addToPattern(pattern, column, startTime, startTime); const options = { validate: (c) => c !== lastColumn, }; startTime += this.segmentDuration; column = this.findAvailableColumn(column, options); lastColumn = column; } return pattern; } generateStair(startTime) { const pattern = new Pattern(); const startX = this.hitObject.startX; let column = this.getColumn(startX || 0, true); let increasing = this.rng.nextDouble() > 0.5; for (let i = 0; i <= this.spanCount; ++i) { this.addToPattern(pattern, column, startTime, startTime); startTime += this.segmentDuration; if (increasing) { if (column >= this.totalColumns - 1) { increasing = false; --column; } else { ++column; } } else { if (column <= this.randomStart) { increasing = true; ++column; } else { --column; } } } return pattern; } generateRandomMultipleNotes(startTime) { const pattern = new Pattern(); const legacy = this.totalColumns >= 4 && this.totalColumns <= 8; const interval = this.rng.nextInt(1, this.totalColumns - (legacy ? 1 : 0)); const startX = this.hitObject.startX; let column = this.getColumn(startX || 0, true); for (let i = 0; i <= this.spanCount; ++i) { this.addToPattern(pattern, column, startTime, startTime); column += interval; if (column >= this.totalColumns - this.randomStart) { column = column - this.totalColumns - this.randomStart + (legacy ? 1 : 0); } column += this.randomStart; if (this.totalColumns > 2) { this.addToPattern(pattern, column, startTime, startTime); } column = this.getRandomColumn(); startTime += this.segmentDuration; } return pattern; } generateNRandomNotes(startTime, p2, p3, p4) { switch (this.totalColumns) { case 2: p2 = 0; p3 = 0; p4 = 0; break; case 3: p2 = Math.min(p2, 0.1); p3 = 0; p4 = 0; break; case 4: p2 = Math.min(p2, 0.3); p3 = Math.min(p3, 0.04); p4 = 0; break; case 5: p2 = Math.min(p2, 0.34); p3 = Math.min(p3, 0.1); p4 = Math.min(p4, 0.03); break; } const isDoubleAtObject = !!this.hitObject.samples.find(findDoubleSample); const isDoubleAtTime = !!this.hitSamplesAt(this.startTime).find(findDoubleSample); const isLowProbability = this.convertType & PatternType.LowProbability; const canGenerateTwoNotes = !isLowProbability && (isDoubleAtObject || isDoubleAtTime); if (canGenerateTwoNotes) p2 = 1; const notes = this.getRandomNoteCount(p2, p3, p4); return this.generateRandomHoldNotes(startTime, notes); function findDoubleSample(sample) { return (sample.hitSound === HitSound[HitSound.Clap] || sample.hitSound === HitSound[HitSound.Finish]); } } generateTiledHoldNotes(startTime) { const pattern = new Pattern(); const columnRepeat = Math.min(this.spanCount, this.totalColumns); const endTime = startTime + this.segmentDuration * this.spanCount; const startX = this.hitObject.startX; let column = this.getColumn(startX || 0, true); const isForceNotStack = !!(this.convertType & PatternType.ForceNotStack); const lessThanTotalColumns = this.previousPattern.columnsWithObjects < this.totalColumns; if (isForceNotStack && lessThanTotalColumns) { const options = { patterns: [this.previousPattern], }; column = this.findAvailableColumn(column, options); } for (let i = 0; i < columnRepeat; ++i) { const options = { patterns: [pattern], }; column = this.findAvailableColumn(column, options); this.addToPattern(pattern, column, startTime, endTime); startTime += this.segmentDuration; } return pattern; } generateHoldAndNormalNotes(startTime) { const pattern = new Pattern(); const startX = this.hitObject.startX; let holdColumn = this.getColumn(startX || 0, true); const isForceNotStack = !!(this.convertType & PatternType.ForceNotStack); const lessThanTotalColumns = this.previousPattern.columnsWithObjects < this.totalColumns; if (isForceNotStack && lessThanTotalColumns) { const options = { patterns: [this.previousPattern], }; holdColumn = this.findAvailableColumn(holdColumn, options); } this.addToPattern(pattern, holdColumn, startTime, this.endTime); let column = this.getRandomColumn(); let noteCount = 0; if (this.conversionDifficulty > 6.5) { noteCount = this.getRandomNoteCount(0.63, 0); } else if (this.conversionDifficulty > 4) { const p2 = this.totalColumns < 6 ? 0.12 : 0.45; noteCount = this.getRandomNoteCount(p2, 0); } else if (this.conversionDifficulty > 2.5) { const p2 = this.totalColumns < 6 ? 0 : 0.24; noteCount = this.getRandomNoteCount(p2, 0); } noteCount = Math.min(this.totalColumns - 1, noteCount); const ignoreHead = !this.hitSamplesAt(startTime).find((sample) => { return (sample.hitSound === HitSound[HitSound.Whistle] || sample.hitSound === HitSound[HitSound.Finish] || sample.hitSound === HitSound[HitSound.Clap]); }); const rowPattern = new Pattern(); for (let i = 0; i <= this.spanCount; ++i) { if (!(ignoreHead && startTime === this.startTime)) { for (let j = 0; j < noteCount; ++j) { const options = { validate: (c) => c !== holdColumn, patterns: [rowPattern], }; column = this.findAvailableColumn(column, options); this.addToPattern(rowPattern, column, startTime, startTime); } } pattern.addPatternHitObjects(rowPattern); rowPattern.clear(); startTime += this.segmentDuration; } return pattern; } hitSamplesAt(time) { const nodeSamples = this.nodeSamplesAt(time); return nodeSamples[0] || this.hitObject.samples; } nodeSamplesAt(time) { if (!(this.hitObject.hitType & HitType.Slider)) { return []; } const slider = this.hitObject; let index = 0; if (this.segmentDuration) { index = (time - this.startTime) / this.segmentDuration; } return index ? slider.nodeSamples.slice(index) : slider.nodeSamples; } addToPattern(pattern, column, startTime, endTime) { var _a, _b; if (startTime === endTime) { const note = new Note(); const posData = this.hitObject; note.startTime = startTime; note.originalColumn = column; note.hitType = HitType.Normal | (this.hitObject.hitType & HitType.NewCombo); note.samples = this.hitSamplesAt(startTime); note.startPosition = (_a = posData === null || posData === void 0 ? void 0 : posData.startPosition) !== null && _a !== void 0 ? _a : new Vector2(256, 192); pattern.addHitObject(note); } else { const hold = new Hold(); const posData = this.hitObject; hold.startTime = startTime; hold.endTime = endTime; hold.originalColumn = column; hold.hitType = HitType.Hold | (this.hitObject.hitType & HitType.NewCombo); hold.samples = this.hitSamplesAt(startTime); hold.nodeSamples = this.nodeSamplesAt(startTime); hold.startPosition = (_b = posData === null || posData === void 0 ? void 0 : posData.startPosition) !== null && _b !== void 0 ? _b : new Vector2(256, 192); pattern.addHitObject(hold); } } } DistanceObjectPatternGenerator.BASE_SCORING_DISTANCE = 100; class EndTimeObjectPatternGenerator extends PatternGenerator { constructor(hitObject, beatmap, originalBeatmap, previousPattern, rng) { super(hitObject, beatmap, originalBeatmap, previousPattern, rng); this.endTime = Math.trunc(hitObject.endTime || 0); this.convertType = previousPattern.columnsWithObjects === this.totalColumns ? PatternType.None : PatternType.ForceNotStack; } *generate() { const pattern = new Pattern(); const shouldGenerateHold = this.endTime - this.hitObject.startTime >= 100; switch (this.totalColumns) { case 8: { const findFinish = (sample) => { return sample.hitSound === HitSound[HitSound.Finish]; }; const hasFinish = !!this.hitObject.samples.find(findFinish); if (hasFinish && this.endTime - this.hitObject.startTime < 1000) { this.addToPattern(pattern, 0, shouldGenerateHold); break; } this.addToPattern(pattern, this.getRandomColumn(), shouldGenerateHold); break; } default: this.addToPattern(pattern, this.getRandomColumn(0), shouldGenerateHold); break; } yield pattern; } getRandomColumn(lowerBound) { const column = super.getRandomColumn(lowerBound); if (this.convertType & PatternType.ForceNotStack) { const options = { lowerBound, patterns: [this.previousPattern], }; return this.findAvailableColumn(column, options); } return this.findAvailableColumn(column, { lowerBound }); } addToPattern(pattern, column, isHoldNote) { var _a, _b, _c; if (isHoldNote) { const hold = new Hold(); const posData = this.hitObject; hold.startTime = this.hitObject.startTime; hold.endTime = this.endTime; hold.originalColumn = column; hold.hitType = HitType.Hold | (this.hitObject.hitType & HitType.NewCombo); hold.samples = this.hitObject.samples; hold.nodeSamples = (_a = this.hitObject.nodeSamples) !== null && _a !== void 0 ? _a : []; hold.startPosition = (_b = posData === null || posData === void 0 ? void 0 : posData.startPosition) !== null && _b !== void 0 ? _b : new Vector2(256, 192); pattern.addHitObject(hold); } else { const note = new Note(); const posData = this.hitObject; note.startTime = this.hitObject.startTime; note.originalColumn = column; note.hitType = HitType.Normal | (this.hitObject.hitType & HitType.NewCombo); note.samples = this.hitObject.samples; note.startPosition = (_c = posData === null || posData === void 0 ? void 0 : posData.startPosition) !== null && _c !== void 0 ? _c : new Vector2(256, 192); pattern.addHitObject(note); } } } class HitObjectPatternGenerator extends PatternGenerator { constructor(hitObject, beatmap, originalBeatmap, previousPattern, rng, previousTime, previousPosition, density, lastStair) { super(hitObject, beatmap, originalBeatmap, previousPattern, rng); this.stairType = PatternType.None; this.convertType = PatternType.None; this.stairType = lastStair; const timingPoint = beatmap.controlPoints .timingPointAt(hitObject.startTime); const effectPoint = beatmap.controlPoints .effectPointAt(hitObject.startTime); const startPosition = hitObject.startPosition; const posSeparation = (startPosition || new Vector2(0, 0)) .fsubtract(previousPosition) .flength(); const timeSeparation = hitObject.startTime - previousTime; if (timeSeparation <= 80) { this.convertType |= PatternType.ForceNotStack | PatternType.KeepSingle; } else if (timeSeparation <= 95) { this.convertType |= PatternType.ForceNotStack | PatternType.KeepSingle | lastStair; } else if (timeSeparation <= 105) { this.convertType |= PatternType.ForceNotStack | PatternType.LowProbability; } else if (timeSeparation <= 125) { this.convertType |= PatternType.ForceNotStack; } else if (timeSeparation <= 135 && posSeparation < 20) { this.convertType |= PatternType.Cycle | PatternType.KeepSingle; } else if (timeSeparation <= 150 && posSeparation < 20) { this.convertType |= PatternType.ForceStack | PatternType.LowProbability; } else if (posSeparation < 20 && density >= timingPoint.beatLength / 2.5) { this.convertType |= PatternType.Reverse | PatternType.LowProbability; } else if (density < timingPoint.beatLength / 2.5 || effectPoint.kiai) ; else { this.convertType |= PatternType.LowProbability; } if (!(this.convertType & PatternType.KeepSingle)) { const isFinish = !!hitObject.samples.find((sample) => { return sample.hitSound === HitSound[HitSound.Finish]; }); const isClap = !!hitObject.samples.find((sample) => { return sample.hitSound === HitSound[HitSound.Clap]; }); if (isFinish && this.totalColumns !== 8) { this.convertType |= PatternType.Mirror; } else if (isClap) { this.convertType |= PatternType.Gathered; } } } *generate() { const p = this._generateCore(); const isStair = !!(this.convertType & PatternType.Stair); const isReverseStair = !!(this.convertType & PatternType.ReverseStair); for (const hitObject of p.hitObjects) { if (isStair && hitObject.column === this.totalColumns - 1) { this.stairType = PatternType.ReverseStair; } if (isReverseStair && hitObject.column === this.randomStart) { this.stairType = PatternType.Stair; } } yield p; } _generateCore() { const pattern = new Pattern(); if (this.totalColumns === 1) { this.addToPattern(pattern, 0); return pattern; } const lastColumn = this.previousPattern.hitObjects.length ? this.previousPattern.hitObjects[0].column : 0; const isReverse = !!(this.convertType & PatternType.Reverse); if (isReverse && this.previousPattern.hitObjects.length) { for (let i = this.randomStart; i < this.totalColumns; ++i) { if (this.previousPattern.columnHasObject(i)) { const column = this.randomStart + this.totalColumns - i - 1; this.addToPattern(pattern, column); } } return pattern; } const isCycle = !!(this.convertType & PatternType.Cycle); const isSingleObject = this.previousPattern.hitObjects.length === 1; const is7KPlus1 = this.totalColumns !== 8 || lastColumn !== 0; const isNotCenter = this.totalColumns % 2 === 0 || lastColumn !== this.totalColumns / 2; if (isCycle && isSingleObject && is7KPlus1 && isNotCenter) { const column = this.randomStart + this.totalColumns - lastColumn - 1; this.addToPattern(pattern, column); return pattern; } const isForceStack = !!(this.convertType & PatternType.ForceStack); if (isForceStack && this.previousPattern.hitObjects.length) { for (let i = this.randomStart; i < this.totalColumns; ++i) { if (this.previousPattern.columnHasObject(i)) { this.addToPattern(pattern, i); } } return pattern; } if (this.previousPattern.hitObjects.length === 1) { if (this.convertType & PatternType.Stair) { let targetColumn = lastColumn + 1; if (targetColumn === this.totalColumns) { targetColumn = this.randomStart; } this.addToPattern(pattern, targetColumn); return pattern; } if (this.convertType & PatternType.ReverseStair) { let targetColumn = lastColumn - 1; if (targetColumn === this.randomStart - 1) { targetColumn = this.totalColumns - 1; } this.addToPattern(pattern, targetColumn); return pattern; } } if (this.convertType & PatternType.KeepSingle) { return this.generateRandomNotes(1); } if (this.convertType & PatternType.Mirror) { if (this.conversionDifficulty > 6.5) { return this.generateRandomPatternWithMirrored(0.12, 0.38, 0.12); } if (this.conversionDifficulty > 4) { return this.generateRandomPatternWithMirrored(0.12, 0.17, 0); } return this.generateRandomPatternWithMirrored(0.12, 0, 0); } if (this.conversionDifficulty > 6.5) { if (this.convertType & PatternType.LowProbability) { return this.generateRandomPattern(0.78, 0.42, 0, 0); } return this.generateRandomPattern(1, 0.62, 0, 0); } if (this.conversionDifficulty > 4) { if (this.convertType & PatternType.LowProbability) { return this.generateRandomPattern(0.35, 0.08, 0, 0); } return this.generateRandomPattern(0.52, 0.15, 0, 0); } if (this.conversionDifficulty > 2) { if (this.convertType & PatternType.LowProbability) { return this.generateRandomPattern(0.18, 0, 0, 0); } return this.generateRandomPattern(0.45, 0, 0, 0); } return this.generateRandomPattern(0, 0, 0, 0); } generateRandomNotes(noteCount) { const getNextColumn = (last) => { if (this.convertType & PatternType.Gathered) { if (++last === this.totalColumns) { last = this.randomStart; } } else { last = this.getRandomColumn(); } return last; }; const pattern = new Pattern(); const allowStacking = !(this.convertType & PatternType.ForceNotStack); if (!allowStacking) { const count = this.totalColumns - this.randomStart - this.previousPattern.columnsWithObjects; noteCount = Math.min(noteCount, count); } const startX = this.hitObject.startX; let column = this.getColumn(startX || 0, true); for (let i = 0; i < noteCount; ++i) { if (allowStacking) { const options = { nextColumn: getNextColumn, patterns: [pattern], }; column = this.findAvailableColumn(column, options); } else { const options = { nextColumn: getNextColumn, patterns: [pattern, this.previousPattern], }; column = this.findAvailableColumn(column, options); } this.addToPattern(pattern, column); } return pattern; } hasSpecialColumn() { const findClap = (sample) => sample.hitSound === HitSound[HitSound.Clap]; const findFinish = (sample) => sample.hitSound === HitSound[HitSound.Finish]; return (!!this.hitObject.samples.find(findClap) && !!this.hitObject.samples.find(findFinish)); } generateRandomPattern(p2, p3, p4, p5) { const pattern = new Pattern(); const noteCount = this.getRandomNoteCount(p2, p3, p4, p5); const randomNotes = this.generateRandomNotes(noteCount); pattern.addPatternHitObjects(randomNotes); if (this.randomStart > 0 && this.hasSpecialColumn()) { this.addToPattern(pattern, 0); } return pattern; } generateRandomPatternWithMirrored(centreProbability, p2, p3) { if (this.convertType & PatternType.ForceNotStack) { return this.generateRandomPattern(1 / Math.fround(2) + p2 / 2, p2, (p2 + p3) / 2, p3); } const pattern = new Pattern(); const [noteCount, addToCentre] = this.getRandomNoteCountMirrored(centreProbability, p2, p3); const columnLimit = Math.trunc((this.totalColumns % 2 ? this.totalColumns - 1 : this.totalColumns) / 2); let column = this.getRandomColumn(this.randomStart, columnLimit); const options = { upperBound: columnLimit, patterns: [pattern], }; for (let i = 0; i < noteCount; ++i) { column = this.findAvailableColumn(column, options); const mirroredColumn = this.randomStart + this.totalColumns - column - 1; this.addToPattern(pattern, column); this.addToPattern(pattern, mirroredColumn); } if (addToCentre) { this.addToPattern(pattern, Math.trunc(this.totalColumns / 2)); } if (this.randomStart > 0 && this.hasSpecialColumn()) { this.addToPattern(pattern, 0); } return pattern; } getRandomNoteCount(p2, p3, p4, p5) { switch (this.totalColumns) { case 2: p2 = 0; p3 = 0; p4 = 0; p5 = 0; break; case 3: p2 = Math.min(p2, 0.1); p3 = 0; p4 = 0; p5 = 0; break; case 4: p2 = Math.min(p2, 0.23); p3 = Math.min(p3, 0.04); p4 = 0; p5 = 0; break; case 5: p3 = Math.min(p3, 0.15); p4 = Math.min(p4, 0.03); p5 = 0; break; } const findClap = (sample) => sample.hitSound === HitSound[HitSound.Clap]; if (this.hitObject.samples.find(findClap)) { p2 = 1; } return super.getRandomNoteCount(p2, p3, p4, p5); } getRandomNoteCountMirrored(centreProbability, p2, p3) { switch (this.totalColumns) { case 2: centreProbability = 0; p2 = 0; p3 = 0; break; case 3: centreProbability = Math.min(centreProbability, 0.03); p2 = 0; p3 = 0; break; case 4: centreProbability = 0; p2 = 1 - Math.max((1 - p2) * 2, 0.8); p3 = 0; break; case 5: centreProbability = Math.min(centreProbability, 0.03); p3 = 0; break; case 6: centreProbability = 0; p2 = 1 - Math.max((1 - p2) * 2, 0.5); p3 = 1 - Math.max((1 - p3) * 2, 0.85); break; } p2 = Math.max(0, Math.min(p2, 1)); p3 = Math.max(0, Math.min(p3, 1)); const centreVal = this.rng.nextDouble(); const noteCount = super.getRandomNoteCount(p2, p3); const addToCentre = this.totalColumns % 2 === 1 && noteCount !== 3 && centreVal > 1 - centreProbability; return [noteCount, addToCentre]; } addToPattern(pattern, column) { var _a; const note = new Note(); const posData = this.hitObject; note.startTime = this.hitObject.startTime; note.originalColumn = column; note.hitType = HitType.Normal | (this.hitObject.hitType & HitType.NewCombo); note.samples = this.hitObject.samples; note.startPosition = (_a = posData === null || posData === void 0 ? void 0 : posData.startPosition) !== null && _a !== void 0 ? _a : new Vector2(256, 192); pattern.addHitObject(note); } } class SpecificBeatmapPatternGenerator extends PatternGenerator { *generate() { var _a, _b, _c; const startX = this.hitObject.startX; const column = this.getColumn(startX || 0); const pattern = new Pattern(); const defaultNodeSamples = [this.hitObject.samples, []]; if (this.hitObject.hitType & HitType.Hold) { const hold = new Hold(); const posData = this.hitObject; hold.startTime = this.hitObject.startTime; hold.endTime = (_a = this.hitObject.endTime) !== null && _a !== void 0 ? _a : hold.startTime; hold.originalColumn = colum