Skip to content

Commit

Permalink
Full taiko support
Browse files Browse the repository at this point in the history
Added star rating calculator, allowing HR, DT, HT, EZ
  • Loading branch information
acrylic-style committed Mar 24, 2021
1 parent 1f9fe77 commit e13f62f
Show file tree
Hide file tree
Showing 18 changed files with 1,048 additions and 11 deletions.
204 changes: 200 additions & 4 deletions popup/calculators/taiko.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,220 @@
import ojsama from 'ojsama'
import TaikoDifficultyAttributes from '../objects/taiko/taikoDifficultyAttributes'
import TaikoDifficultyHitObject from '../objects/taiko/taikoDifficultyHitObject'
import TaikoObject from '../objects/taiko/taikoObject'
import Skill from '../skills/skill'
import StaminaCheeseDetector from '../skills/taiko/staminaCheeseDetector'
import Colour from '../skills/taiko/colour'
import Rhythm from '../skills/taiko/rhythm'
import Stamina from '../skills/taiko/stamina'

export const GREAT_MIN = 50
export const GREAT_MID = 35
export const GREAT_MAX = 20

export const COLOUR_SKILL_MULTIPLIER = 0.01
export const RHYTHM_SKILL_MULTIPLIER = 0.014
export const STAMINA_SKILL_MULTIPLIER = 0.02

export const difficltyRange = (difficulty, min, mid, max) => {
if (difficulty > 5) return mid + ((max - mid) * (difficulty - 5)) / 5
if (difficulty < 5) return mid - ((mid - min) * (5 - difficulty)) / 5
return mid
}

/**
* @param {ojsama.beatmap} map
* @param {{ time: number, type: number, hitSounds: number, hitType: number }[]} taikoObjects
*/
export const createDifficultyHitObjects = (map, taikoObjects, clockRate) => {
const objects = []
for (let i = 2; i < map.objects.length; i++) {
objects.push(
new TaikoDifficultyHitObject(
new TaikoObject(map.objects[i], taikoObjects[i].hitType),
new TaikoObject(map.objects[i - 1], taikoObjects[i - 1].hitType),
new TaikoObject(map.objects[i - 2], taikoObjects[i - 2].hitType),
clockRate,
i
)
)
}
new StaminaCheeseDetector(objects).findCheese() // this method name makes me hungry...
return objects
}

/**
* @param {ojsama.beatmap} map
* @param {ojsama.modbits | number} mods
*/
export const calculate = (map, mods, taikoObjects) => {
const originalOverallDifficulty = map.od
let clockRate = 1
if (mods & ojsama.modbits.dt) clockRate = 1.5
if (mods & ojsama.modbits.ht) clockRate = 0.75
if (mods & ojsama.modbits.hr) {
const ratio = 1.4
map.od = Math.min(map.od * ratio, 10.0)
}
if (mods & ojsama.modbits.ez) {
const ratio = 0.5
map.od *= ratio
}

const skills = [
new Colour(mods),
new Rhythm(mods),
new Stamina(mods, true),
new Stamina(mods, false),
]
if (map.objects.length === 0)
return createDifficultyAttributes(map, mods, skills, clockRate)

const difficultyHitObjects = createDifficultyHitObjects(
map,
taikoObjects,
clockRate
)
const sectionLength = 400 * clockRate
let currentSectionEnd =
Math.ceil(map.objects[0].time / sectionLength) * sectionLength

difficultyHitObjects.forEach((h) => {
while (h.baseObject.time > currentSectionEnd) {
skills.forEach((s) => {
s.saveCurrentPeak()
s.startNewSectionFrom(currentSectionEnd)
})

currentSectionEnd += sectionLength
}

skills.forEach((s) => s.process(h))
})

// The peak strain will not be saved for the last section in the above loop
skills.forEach((s) => s.saveCurrentPeak())

const attr = createDifficultyAttributes(map, mods, skills, clockRate)
map.od = originalOverallDifficulty
return attr
}

/**
* @param {ojsama.beatmap} map
* @param {ojsama.modbits | number} mods
* @param {Skill[]} skills
* @param {number} clockRate
*/
export const createDifficultyAttributes = (map, mods, skills, clockRate) => {
if (map.objects.length === 0) {
return new TaikoDifficultyAttributes(0, mods, 0, 0, 0, 0, 0, skills)
}
const colour = skills[0]
const rhythm = skills[1]
const staminaRight = skills[2]
const staminaLeft = skills[3]

const colourRating = colour.getDifficultyValue() * COLOUR_SKILL_MULTIPLIER
const rhythmRating = rhythm.getDifficultyValue() * RHYTHM_SKILL_MULTIPLIER
let staminaRating =
(staminaRight.getDifficultyValue() + staminaLeft.getDifficultyValue()) *
STAMINA_SKILL_MULTIPLIER

const staminaPenalty = simpleColourPenalty(staminaRating, colourRating)
staminaRating *= staminaPenalty

const combinedRating = locallyCombinedDifficulty(
colour,
rhythm,
staminaRight,
staminaLeft,
staminaPenalty
)
const separatedRating = norm(1.5, [colourRating, rhythmRating, staminaRating])
let starRating = 1.4 * separatedRating + 0.5 * combinedRating
starRating = rescale(starRating)

const greatHitWindow =
difficltyRange(map.od, GREAT_MIN, GREAT_MID, GREAT_MAX) | 0

return new TaikoDifficultyAttributes(
starRating,
mods,
staminaRating,
rhythmRating,
colourRating,
greatHitWindow / clockRate,
map.objects.filter((obj) => (obj.type & 1) !== 0).length, // max combo
skills
)
}

export const simpleColourPenalty = (staminaDifficulty, colorDifficulty) => {
if (colorDifficulty <= 0) return 0.79 - 0.25
return (
0.79 - Math.atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2
)
}

/**
* @param {number} p
* @param {number[]} values
*/
export const norm = (p, values) => {
let e = 0
values.forEach((n) => {
e += Math.pow(n, p)
})
return Math.pow(e, 1 / p)
}

/**
* @param {Skill} colour
* @param {Skill} rhythm
* @param {Skill} staminaRight
* @param {Skill} staminaLeft
* @param {number} staminaPenalty
*/
export const locallyCombinedDifficulty = (
colour,
rhythm,
staminaRight,
staminaLeft,
staminaPenalty
) => {
const peaks = []
for (let i = 0; i < colour.strainPeaks.length; i++) {
const colourPeak = colour.strainPeaks[i] * COLOUR_SKILL_MULTIPLIER
const rhythmPeak = rhythm.strainPeaks[i] * RHYTHM_SKILL_MULTIPLIER
const staminaPeak =
(staminaRight.strainPeaks[i] + staminaLeft.strainPeaks[i]) *
STAMINA_SKILL_MULTIPLIER *
staminaPenalty
peaks.push(norm(2, [colourPeak, rhythmPeak, staminaPeak]))
}
let difficulty = 0
let weight = 1
peaks
.sort((a, b) => b - a)
.forEach((strain) => {
difficulty += strain * weight
weight *= 0.9
})
return difficulty
}

export const rescale = (sr) => (sr < 0 ? sr : 10.43 * Math.log(sr / 8 + 1))

// javascript implementation of osu!lazer's pp calculator implementation: https://github.com/ppy/osu/blob/master/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
/**
* @param {ojsama.beatmap} map
* @param {{ total: number }} stars
* @param {TaikoDifficultyAttributes} attr
* @param {ojsama.modbits} mods
*/
export const calculatePerformance = (
map,
stars,
attr,
mods,
combo,
misses,
Expand All @@ -28,14 +224,14 @@ export const calculatePerformance = (
if (mods & ojsama.modbits.nf) multiplier *= 0.9
if (mods & ojsama.modbits.hd) multiplier *= 1.1
const strainValue = calculateStrainPerformance(
stars,
attr.starRating,
mods,
misses,
accuracy / 100,
combo
)
const accuracyValue = calculateAccuracyPerformance(
difficltyRange(map.od, GREAT_MIN, GREAT_MID, GREAT_MAX) | 0,
attr.greatHitWindow,
accuracy / 100,
combo
) // todo: clockRate, see DifficultyCalculator.cs:47, TaikoDifficultyCalculator.cs:65
Expand Down
13 changes: 10 additions & 3 deletions popup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BEATMAP_URL_REGEX } from '../common/constants'
import { loadAnalytics } from './analytics'
import * as taiko from './calculators/taiko'
import * as std from './calculators/standard'
import * as taikoReader from './objects/taiko/taikoReader'

require('./notifications')

Expand Down Expand Up @@ -37,6 +38,10 @@ versionElement.innerText = `ezpp! v${manifest.version}`
// Set after the extension initializes, used for additional error information.
let currentUrl = null
let cleanBeatmap = null
/**
* @type {{ time: number, type: number, hitSounds: number, hitType: number }[]}
*/
let taikoObjects = null
let pageInfo = {
isOldSite: null,
beatmapSetId: null,
Expand Down Expand Up @@ -232,11 +237,11 @@ const calculate = () => {

case MODE_TAIKO:
document.documentElement.classList.add('mode-taiko')
// TOOD: implement star rating calculator
stars = { total: pageInfo.beatmap.difficulty_rating }
const attr = taiko.calculate(cleanBeatmap, modifiers, taikoObjects)
stars = { total: attr.starRating }
pp = taiko.calculatePerformance(
cleanBeatmap,
stars.total,
attr,
modifiers,
combo,
misses,
Expand Down Expand Up @@ -268,6 +273,7 @@ const calculate = () => {
setResultText(Math.round(pp.total))
} catch (error) {
displayError(error)
console.error('Error in popup: ' + (error.stack || error))
}
}

Expand Down Expand Up @@ -367,6 +373,7 @@ const attemptToFetchBeatmap = (id, attempts) =>

const processBeatmap = (rawBeatmap) => {
const { map } = new ojsama.parser().feed(rawBeatmap)
taikoObjects = taikoReader.feed(rawBeatmap)

cleanBeatmap = map

Expand Down
14 changes: 14 additions & 0 deletions popup/objects/difficultyHitObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { hitobject } from 'ojsama'

export default class DifficultyHitObject {
/**
* @param {hitobject} baseObject
* @param {hitobject} lastObject
* @param {number} clockRate The rate at which the gameplay clock is run at.
*/
constructor(baseObject, lastObject, clockRate) {
this.baseObject = baseObject
this.lastObject = lastObject
this.deltaTime = (baseObject.time - lastObject.time) / clockRate
}
}
10 changes: 10 additions & 0 deletions popup/objects/taiko/hitType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const HitType = {
/**
* A hit that can be hit by the centre portion of the drum.
*/
Centre: 0,
/**
* A hit that can be hit by rim portion of the drum.
*/
Rim: 1,
}
12 changes: 12 additions & 0 deletions popup/objects/taiko/objectType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const ObjectType = {
Hit: 1,
Slider: 2,
Spinner: 12,
}

export const fromNumber = (num) => {
if (num === 1) return Hit
if (num === 2) return Slider
if (num === 12) return Spinner
return -1
}
34 changes: 34 additions & 0 deletions popup/objects/taiko/taikoDifficultyAttributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { modbits } from 'ojsama'
import Skill from '../../skills/skill'

export default class TaikoDifficultyAttributes {
/**
* @param {number} starRating
* @param {modbits} mods
* @param {number} staminaStrain
* @param {number} rhythmStrain
* @param {number} colourStrain
* @param {number} greatHitWindow
* @param {number} maxCombo
* @param {Skill[]} skills
*/
constructor(
starRating,
mods,
staminaStrain,
rhythmStrain,
colourStrain,
greatHitWindow,
maxCombo,
skills
) {
this.starRating = starRating
this.mods = mods
this.staminaStrain = staminaStrain
this.rhythmStrain = rhythmStrain
this.colourStrain = colourStrain
this.greatHitWindow = greatHitWindow
this.maxCombo = maxCombo
this.skills = skills
}
}
Loading

0 comments on commit e13f62f

Please sign in to comment.