// ==UserScript==
// @name chuni-net - display overpower
// @namespace esterTion
// @license MIT
// @match https://chunithm-net-eng.com/mobile/record/music*
// @match https://chunithm.wahlap.com/mobile/record/music*
// @match https://new.chunithm-net.com/chuni-mobile/html/mobile/record/music*
// @match https://chunithm-net-eng.com/mobile/home/playerData/rating*
// @match https://chunithm.wahlap.com/mobile/home/playerData/rating*
// @match https://new.chunithm-net.com/chuni-mobile/html/mobile/home/playerData/rating*
// @version 1.1.1
// @author esterTion
// @description Display song overpower and rating on chunithm-net
// @run-at document-end
// ==/UserScript==
(async function () {
const host = location.hostname
const server = host === 'new.chunithm-net.com' ? 'jp' : host === 'chunithm-net-eng.com' ? 'ex' : host === 'chunithm.wahlap.com' ? 'cn' : ''
if (!server) throw new Error('unknown server')
const disabledSongs = {
ex: [],
cn: [],
jp: []
}
// createElement
function _(e,t,i){var a=null;if("text"===e)return document.createTextNode(t);a=document.createElement(e);for(var n in t)if("style"===n)for(var o in t.style)a.style[o]=t.style[o];else if("className"===n)a.className=t[n];else if("event"===n)for(var o in t.event)a.addEventListener(o,t.event[o]);else a.setAttribute(n,t[n]);if(i)if("string"==typeof i)a.innerHTML=i;else if(Array.isArray(i))for(var l=0;l<i.length;l++)null!=i[l]&&a.appendChild(i[l]);return a}
const localStorageTimeKey = 'CNDL_music_level_info_time'
const localStorageDataKey = 'CNDL_music_level_info'
let musicLevelInfo = {}
function loadLocalInfo() {
if (!localStorage[localStorageDataKey]) return
musicLevelInfo = JSON.parse(localStorage[localStorageDataKey])
}
let resultCount = {
sssp: 0,
sss: 0,
ssp: 0,
ss: 0,
sp: 0,
s: 0,
other: 0,
max: 0,
aj: 0,
fc: 0,
nonfc: 0,
}
const diffToggles = {
bas: true,
adv: true,
exp: true,
mas: true,
ult: true,
other: true,
}
const diffTogglesDisplay = {
bas: false,
adv: false,
exp: false,
mas: false,
ult: false,
other: false,
}
let musicOnPage
let sortForm
function initPageOverPower() {
musicOnPage = [...document.querySelectorAll('.musiclist_box .music_title')].map(addOverPowerToList)
musicOnPage.forEach(i => {
if (diffTogglesDisplay[i.dif] !== undefined) {
diffTogglesDisplay[i.dif] = true
} else {
diffTogglesDisplay.other = true
}
})
const scoreList = document.querySelector('#scoreList_result')
if (!scoreList) return
scoreList.appendChild(_('div', {className: 'box01 w420 CNDO-sort-box'}, [
_('div', {}, [ sortForm = _('form', { event: { change: applySort }}, [
_('select', { style: { width: '160px', margin: '10px' }, name: 'key' }, [
_('option', {value: 'initial'}, [_('text', '初始顺序')]),
_('option', {value: 'level'}, [_('text', '难度')]),
_('option', {value: 'score'}, [_('text', '分数')]),
_('option', {value: 'aj'}, [_('text', 'FC/AJ')]),
_('option', {value: 'op_percent'}, [_('text', 'op %')]),
_('option', {value: 'op_remain'}, [_('text', 'op 剩余')]),
]),
_('select', { style: { width: '96px', margin: '10px' }, name: 'desc' }, [
_('option', {value: 0}, [_('text', '↑')]),
_('option', {value: 1}, [_('text', '↓')]),
]),
])]),
]))
addOverPowerToPage()
}
function addOverPowerToPage() {
const musics = musicOnPage.filter(i => {
if (i.disabled) return false
let on = diffToggles[i.dif]
on ??= diffToggles.other
return on
})
document.querySelector('.CNDO-info-box')?.remove()
let resultCount = {
sssp: 0,
sss: 0,
ssp: 0,
ss: 0,
sp: 0,
s: 0,
other: 0,
max: 0,
aj: 0,
fc: 0,
nonfc: 0,
}
const total = musics.reduce((a, i) => {
resultCount.other++
resultCount.nonfc++
const obj = i.parsed
if (obj.scoreRank >=8) { resultCount.other--; }
if (obj.scoreRank >=8) { resultCount.s++; }
if (obj.scoreRank >=9) { resultCount.sp++; }
if (obj.scoreRank >=10) { resultCount.ss++; }
if (obj.scoreRank >=11) { resultCount.ssp++; }
if (obj.scoreRank >=12) { resultCount.sss++; }
if (obj.scoreRank >=13) { resultCount.sssp++; }
if (obj.theoryCount) { resultCount.max++; }
if (obj.isAllJustice) { resultCount.aj++; }
if (obj.isFullCombo) { resultCount.fc++; resultCount.nonfc--; }
return {rate: a.rate+(i.rate), max: a.max+i.max, playedmax: a.playedmax+(i.rate>0?i.max:0)}
}, {rate: 0, max: 0, playedmax: 0})
total.rate /= 100
total.max /= 100
total.playedmax /= 100
total.max = Math.max(total.max, 1)
total.playedmax = Math.max(total.playedmax, 1)
// console.log(total, total.rate/total.max*100)
const countedSize = musics.filter(i => !i.disabled).length
const scoreList = document.querySelector('#scoreList_result')
if (!scoreList) return
let diffToggleForm
scoreList.appendChild(_('div', {className: 'box01 w420 CNDO-info-box'}, [
_('style', {}, `
.overpower-graph-base {
position:relative;
width:100%;
height:22px;
background:#6e6e6e;
outline:#dddddd 1px solid
}
.overpower-graph-base .bg-gold,
.overpower-graph-base .bg-platinum,
.overpower-graph-base .bg-silver,
.overpower-graph-base .bg-white {
position:absolute;
left:0;
top:0;
bottom:0
}
.overpower-graph-base .bg-white {
z-index:12;
background:linear-gradient(180deg,#fff,#fff 50%,#d0d6da 50%,#e4edf3 75%,#fff)!important
}
.overpower-graph-base .bg-platinum {
z-index:10;
background:linear-gradient(180deg,#fff1ba,#ffeb9c 50%,#fc0 50%,#ffeca4 75%,#fff)!important
}
.overpower-graph-base .bg-gold {
z-index:8;
background:linear-gradient(180deg,#ffe4a3,#efae10 50%,#ff8300 50%,#ffc947 75%,#fff)!important
}
.overpower-graph-base .bg-silver {
z-index:6;
background:linear-gradient(180deg,#c8e7ff,#8fceff 50%,#6eb5ff 50%,#b7ddff 75%,#fff)!important
}
`),
_('div', {className: 'narrow_block clearfix', style: {whiteSpace:'pre-wrap'}}, [
diffToggleForm = _('form', { event: { change: e => {
diffToggles[e.target.name] = e.target.checked
musicOnPage.forEach(i => {
if (i.dif !== e.target.name) return
i.box.style.display = e.target.checked ? '' : 'none'
})
addOverPowerToPage()
} } }, [
!diffTogglesDisplay.bas ? new Comment('bas toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'bas', [diffToggles.bas ? 'checked' : 'checked_']: '1' }),
_('text', 'BAS'),
]),
!diffTogglesDisplay.adv ? new Comment('adv toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'adv', [diffToggles.adv ? 'checked' : 'checked_']: '1' }),
_('text', 'ADV'),
]),
!diffTogglesDisplay.exp ? new Comment('exp toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'exp', [diffToggles.exp ? 'checked' : 'checked_']: '1' }),
_('text', 'EXP'),
]),
!diffTogglesDisplay.mas ? new Comment('mas toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'mas', [diffToggles.mas ? 'checked' : 'checked_']: '1' }),
_('text', 'MAS'),
]),
!diffTogglesDisplay.ult ? new Comment('ult toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'ult', [diffToggles.ult ? 'checked' : 'checked_']: '1' }),
_('text', 'ULT'),
]),
!diffTogglesDisplay.other ? new Comment('other toggle') : _('label', {}, [
_('input', {type: 'checkbox', name: 'other', [diffToggles.other ? 'checked' : 'checked_']: '1' }),
_('text', 'OTHER'),
]),
]),
_('text', [
`共计 ${countedSize} 曲目`,
`${total.rate} / ${total.max}`,
(total.rate/total.max*100).toFixed(4)+'%',
'',
'已游玩平均:',
//`${total.rate} / ${total.playedmax}`,
(total.rate/total.playedmax*100).toFixed(4)+'%',
'','',
].join('\n')),
_('table', { style: {width: '100%', fontSize: '16px'} }, [
_('tr', {}, [
_('td', {}, [_('text', 'SSS+')]),
_('td', {}, [_('text', 'SSS')]),
_('td', {}, [_('text', 'SS+')]),
_('td', {}, [_('text', 'SS')]),
_('td', {}, [_('text', 'S+')]),
_('td', {}, [_('text', 'S')]),
_('td', {}, [_('text', 'OTHER')]),
]),
_('tr', {}, [
_('td', {}, [_('text', resultCount.sssp)]),
_('td', {}, [_('text', resultCount.sss - resultCount.sssp)]),
_('td', {}, [_('text', resultCount.ssp - resultCount.sss)]),
_('td', {}, [_('text', resultCount.ss - resultCount.ssp)]),
_('td', {}, [_('text', resultCount.sp - resultCount.ss)]),
_('td', {}, [_('text', resultCount.s - resultCount.sp)]),
_('td', {}, [_('text', resultCount.other)]),
]),
]),
_('div', {className: 'overpower-graph-base'}, [
_('div', {className: 'bg-platinum', style: {width: `${resultCount.sss/countedSize*100}%`}}),
_('div', {className: 'bg-gold', style: {width: `${resultCount.ss/countedSize*100}%`}}),
_('div', {className: 'bg-silver', style: {width: `${resultCount.s/countedSize*100}%`}}),
]),
_('table', { style: {width: '100%', fontSize: '16px'} }, [
_('tr', {}, [
_('td', {}, [_('text', 'MAX')]),
_('td', {}, [_('text', 'AJ')]),
_('td', {}, [_('text', 'FC')]),
_('td', {}, [_('text', 'OTHER')]),
]),
_('tr', {}, [
_('td', {}, [_('text', resultCount.max)]),
_('td', {}, [_('text', resultCount.aj - resultCount.max)]),
_('td', {}, [_('text', resultCount.fc - resultCount.aj)]),
_('td', {}, [_('text', resultCount.nonfc)]),
]),
]),
_('div', {className: 'overpower-graph-base'}, [
_('div', {className: 'bg-white', style: {width: `${resultCount.max/countedSize*100}%`}}),
_('div', {className: 'bg-platinum', style: {width: `${resultCount.aj/countedSize*100}%`}}),
_('div', {className: 'bg-gold', style: {width: `${resultCount.fc/countedSize*100}%`}}),
]),
]),
]))
if (diffToggleForm.children.length === 1) {
diffToggleForm.remove()
}
applySort.call(sortForm)
}
function addOverPowerToList(titleDiv, idx) {
const dif = getDifFromClass(titleDiv.parentNode)
if (!dif) return null
const box = titleDiv.parentNode
const titleText = titleDiv.lastChild.textContent.trim()
const level = getLevelByTitleAndDif(titleText, dif) * 1
const returnData = {
box,
idx,
dif,
level,
rate: 0,
disabled: disabledSongs[server].includes(titleText),
parsed: {},
max: (level+3) * 500
}
const scoreBox = box.querySelector('.play_musicdata_highscore .text_b')
if (!scoreBox) return returnData
if (returnData.disabled) {
return returnData
}
const obj = {
scoreMax: scoreBox.textContent.replace(/,/g, '')*1,
maxComboCount: 0,
isFullCombo: false,
isAllJustice: false,
isSuccess: 0,
fullChain: 0,
maxChain: 0,
scoreRank: 0,
isLock: false,
theoryCount: 0,
}
if (obj.scoreMax === 1010000) { obj.theoryCount = 1; }
[...box.querySelectorAll('.play_musicdata_icon img')].forEach(i => {
const icon = i.src.replace(/.+\//, '').replace(/icon_(.+)\.png/, '$1')
switch (icon) {
case 'clear': { obj.isSuccess = 1; break }
case 'hard': { obj.isSuccess = 2; break }
case 'absolute': { obj.isSuccess = 3; break }
case 'absolutep': { obj.isSuccess = 4; break }
case 'absolutepp': { obj.isSuccess = 5; break }
case 'catastrophy': { obj.isSuccess = 6; break }
case 'rank_1': { obj.scoreRank = 1; break }
case 'rank_2': { obj.scoreRank = 2; break }
case 'rank_3': { obj.scoreRank = 3; break }
case 'rank_4': { obj.scoreRank = 4; break }
case 'rank_5': { obj.scoreRank = 5; break }
case 'rank_6': { obj.scoreRank = 6; break }
case 'rank_7': { obj.scoreRank = 7; break }
case 'rank_8': { obj.scoreRank = 8; break }
case 'rank_9': { obj.scoreRank = 9; break }
case 'rank_10': { obj.scoreRank = 10; break }
case 'rank_11': { obj.scoreRank = 11; break }
case 'rank_12': { obj.scoreRank = 12; break }
case 'rank_13': { obj.scoreRank = 13; break }
case 'alljusticecritical':
case 'alljustice': { obj.isAllJustice = true }
case 'fullcombo': { obj.isFullCombo = true; obj.missCount = 0; break }
case 'fullchain2': { obj.fullChain = 1; break }
case 'fullchain': { obj.fullChain = 2; break }
}
})
returnData.parsed = obj
returnData.rating = getRatingFromConstantAndScore(level, obj.scoreMax)
const comboRate = obj.theoryCount ? 125 :
obj.isAllJustice ? 100 :
obj.isFullCombo ? 50 : 0
if (obj.scoreMax <= 1007500) {
// 低于sss
returnData.rate = Math.floor(returnData.rating*500 * 2) / 2 + comboRate
addOverPowerAfterScore(scoreBox, returnData)
return returnData
}
const sssRate = Math.floor((obj.scoreMax - 1007500) * 0.15 * 2) / 2
returnData.rate = (level + 2) * 500 + comboRate + sssRate
addOverPowerAfterScore(scoreBox, returnData)
return returnData
}
function addOverPowerAfterScore(scoreBox, returnData) {
scoreBox.parentNode.appendChild(_('span', {style: {marginLeft: '2em', fontFamily: 'Arial'}}, [
_('text', (Math.floor(returnData.rate)/100).toFixed(2) + ' / ' + (returnData.max/100).toFixed(2) + ' ' + (returnData.rate/returnData.max*100).toFixed(2)+'%'),
]))
}
function getDifFromClass(div) {
const divClass = [...div.classList].filter(i => i.startsWith('bg_') || i.startsWith('title_'))
if (!divClass.length) return '?'
switch (divClass[0]) {
case 'bg_basic': { return 'bas' }
case 'bg_advanced': { return 'adv' }
case 'bg_expert': { return 'exp' }
case 'bg_master': { return 'mas' }
case 'bg_ultima': { return 'ult' }
case 'title_basic': { return 'bas' }
case 'title_advanced': { return 'adv' }
case 'title_expert': { return 'exp' }
case 'title_master': { return 'mas' }
case 'title_ultima': { return 'ult' }
}
return '?'
}
function getLevelByTitleAndDif(title, dif) {
if (!musicLevelInfo[title]) return 0
if (!musicLevelInfo[title][dif]) return 0
return musicLevelInfo[title][dif].replace('+', '.5')
}
var ratingBorder = [
[1009000, +2.15, 1],
[1007500, +2.0, 1],
[1005000, +1.5, 1],
[1000000, +1.0, 1],
[975000, +0.0, 1],
[925000, -3.0, 1],
[900000, -5.0, 1],
[800000, -5.0, 2],
[500000, 0, 1]
];
function getRatingFromConstantAndScore(constant, score) {
if (constant == 0) return 0;
if (score >= ratingBorder[0][0]) return constant + ratingBorder[0][1];
for (var i = 0; i < ratingBorder.length - 1; i++) {
if (score >= ratingBorder[i + 1][0]) {
return (constant + ratingBorder[i + 1][1]) / ratingBorder[i + 1][2]
+ (score - ratingBorder[i + 1][0]) / (ratingBorder[i][0] - ratingBorder[i + 1][0])
* (
(constant + ratingBorder[i][1]) / ratingBorder[i][2] -
(constant + ratingBorder[i + 1][1]) / ratingBorder[i + 1][2]
)
}
}
return 0;
}
function applySort() {
const form = this
console.log(form)
const key = form.key.value
const desc = form.desc.value === '1'
musicOnPage.sort((a, b) => (desc ? -1 : 1) * (a.idx - b.idx))
if (key !== 'initial') {
musicOnPage.sort((a, b) => {
let result = 0
switch (key) {
case 'level': { result = a.level - b.level; break }
case 'score': { result = a.parsed.scoreMax - b.parsed.scoreMax; break }
case 'aj': { result = toAjScore(a) - toAjScore(b); break }
case 'op_percent': { result = a.rate/a.max - b.rate/b.max; break }
case 'op_remain': { result = (a.max - a.rate) - (b.max - b.rate); break }
}
return desc ? -result : result
})
}
musicOnPage.forEach((i, idx) => {
i.box.parentNode.parentNode.appendChild(i.box.parentNode)
})
}
function toAjScore(i) {
return i.parsed.theoryCount ? 3 : (
i.parsed.isAllJustice ? 2 : (
i.parsed.isFullCombo ? 1 : 0
)
)
}
let totalRating = 0
function addRatingToPage() {
const musics = [...document.querySelectorAll('.musiclist_box .music_title')].map(addRatingToList)
if (!musics.length) return
const boxInfoText = document.querySelector('.box01 .font_x-small')
if (!boxInfoText) return
boxInfoText.appendChild(_('br'))
boxInfoText.appendChild(_('span', {}, [_('text', `平均:${(totalRating / musics.length).toFixed(3)} (${musics.length}) 总计:${totalRating.toFixed(2)}`)]))
}
function addRatingToList(titleDiv) {
const dif = getDifFromClass(titleDiv.parentNode)
if (!dif) return null
const box = titleDiv.parentNode
const titleText = titleDiv.lastChild.textContent.trim()
const level = getLevelByTitleAndDif(titleText, dif) * 1
const returnData = {
rating: 0,
disabled: disabledSongs[server].includes(titleText),
ratingMax: level + 2.15,
}
const scoreBox = box.querySelector('.play_musicdata_highscore .text_b')
if (!scoreBox) return returnData
const scoreMax = scoreBox.textContent.replace(/,/g, '')*1
returnData.rating = getRatingFromConstantAndScore(level, scoreMax)
addRatingAfterScore(scoreBox, returnData)
totalRating += Math.floor(returnData.rating *100) /100
return returnData
}
function addRatingAfterScore(scoreBox, returnData) {
scoreBox.parentNode.appendChild(_('span', {style: {marginRight: '.5em', fontFamily: 'Arial', float: 'right'}}, [
_('text', (returnData.rating === returnData.ratingMax ? returnData.rating.toFixed(2) : returnData.rating.toFixed(3)) + ' / ' + returnData.ratingMax.toFixed(2)),
]))
}
loadLocalInfo()
if (location.href.indexOf('home/playerData/rating') !== -1) {
addRatingToPage()
} else {
initPageOverPower()
}
})()