Display map details on chunithm-net
当前为
// ==UserScript==
// @name chuni-net - Map Details
// @namespace esterTion
// @license MIT
// @match https://chunithm-net-eng.com/mobile/record/
// @match https://new.chunithm-net.com/mobile/record/
// @match https://chunithm.wahlap.com/mobile/record/
// @grant GM.xmlHttpRequest
// @version 1.1.0
// @author esterTion
// @description Display map details on chunithm-net
// @run-at document-end
// ==/UserScript==
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')
// 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 = 'CNMD_maps_info_time'
const localStorageDataKey = 'CNMD_maps_info'
let mapInfo = {}
function loadLocalInfo() {
if (!localStorage[localStorageDataKey]) return
mapInfo = JSON.parse(localStorage[localStorageDataKey])
}
function checkUpdateForLocalInfo() {
const today = getDateStringForUpdate()
if (!localStorage[localStorageTimeKey] || localStorage[localStorageTimeKey] !== today) {
downloadInfo(today)
}
}
async function downloadInfo(today) {
console.log('downloading map info')
switch (server) {
case 'jp': {
throw new Error('not implemented')
break;
}
case 'ex': {
await fetchJson('https://estertion.win/__private__/chuni-intl-maps.json').then(r => mapInfo = r)
break;
}
case 'cn': {
await fetchJson('https://estertion.win/__private__/chuni-chn-maps.json').then(r => mapInfo = r)
break;
}
}
localStorage[localStorageDataKey] = JSON.stringify(mapInfo)
localStorage[localStorageTimeKey] = today
console.log('stored map info: ', Object.keys(mapInfo).length, 'entries')
}
function getDateStringForUpdate() {
const d = new Date
d.setTime(d.getTime() + d.getTimezoneOffset() * 60e3 + {jp:11,ex:11,cn:10}[server]*3600e3)
return [d.getUTCFullYear(), d.getUTCMonth()+1, d.getUTCDate()].join('/')
}
function fetchJson(url) {
return new Promise((res, rej) => {
GM.xmlHttpRequest({
url: url + '?_=' + Date.now(),
responseType: 'json',
method: 'GET',
onload: r => res(r.response),
onerror: e => rej(e),
})
})
}
const mapBlockMap = {}
function generatePage(map, page) {
const mapTitle = map.name
const currentPage = page
const totalPage = map.areas.slice(-1)[0].page + 1
const mapPage = _('div', { className: 'map_block w400' }, [
_('div', { className: ['map-paginator', page>1?'has-prev':'', page<totalPage?'has-next':''].join(' ') }, [
_('span', { className: 'prev', event: { click: _ => { mapPage.parentNode.replaceChild(getPage(mapTitle, page - 1), mapPage); executeMarquee() } } }, [_('text', '<')]),
_('span', { className: 'next', event: { click: _ => { mapPage.parentNode.replaceChild(getPage(mapTitle, page + 1), mapPage); executeMarquee() } } }, [_('text', '>')]),
]),
_('div', { className: 'map_title w388' }, [
_('div', { className: 'map_title_text text_l text_b' }, [
_('text', mapTitle),
]),
_('div', { className: 'map_titale_page' }, [
_('div', { className: 'map_title_page_num font_90' }, [
_('text', currentPage),
]),
_('div', { className: 'map_title_page_den font_90' }, [
_('text', totalPage),
]),
]),
]),
_('div', { className: 'maparea_period w388' }, []),
])
const pageAreas = new Array(9)
map.areas.forEach(area => {
if (area.page !== page - 1) return
pageAreas[area.position] = area
})
for (let i = 0; i < 9; i++) {
if (!pageAreas[i]) {
mapPage.appendChild(_('div', { className: 'maparea_block'}, [
_('div', {className: 'maparea_blank'})
]))
} else {
const index = i
const areaLength = pageAreas[i].rewards.slice(-1)[0].position
const isClearedPage = initialPages[mapTitle] && initialPages[mapTitle].currentPage > page
const areaLengthText = isClearedPage ? 0 : areaLength
const pageAreasCount = pageAreas.filter(a => a).length
mapPage.appendChild(_('div', { className: 'maparea_block user_data_friend_tap', event: { click: () => showMapAreaDetail(mapTitle, currentPage, index, isClearedPage ? pageAreasCount : 0, areaLengthText) } }, [
_('div', { className: 'maparea' }, [
isClearedPage ? _('div', { className: 'map_clear' }, [_('img', { src: '/mobile//images/progress_RewardIconClear.png' })]) : _('text', ''),
_('div', { className: 'map_icon' }, [
getRewardIcon(pageAreas[i].rewards.slice(-1)[0])
]),
_('div', { className: 'map_remain' }, [_('div', { className: 'map_remain_text' }, [_('text', areaLengthText)])]),
_('div', { className: 'map_skillseed_block font_0' }, [_('div', { className: 'map_skillseed_text' }, [_('span', {}, [_('text', pageAreas[i].skill)])])]),
])
]))
}
mapPage.appendChild(_('text', ' '))
}
return mapPage
}
function getRewardIcon(reward) {
switch (reward.type) {
case 0: // gamePoint
case 2: // trophy
case 4: // skillSeed
return _('div', { className: 'map_icon_limitbreak' }, [_('text', reward.reward)])
case 1: // ticket
{
if (reward.id >= 20008000) {
return _('div', { className: 'map_icon_limitbreak' }, [getRewardIconImg(reward.id, reward.reward)])
}
}
case 5: // namePlate
case 8: // systemVoice
{
return _('div', { className: 'map_icon_nameplate' }, [getRewardIconImg(reward.id, reward.reward)])
}
case 3: // chara
{
return _('div', { className: 'map_icon_chara' }, [getRewardIconImg(reward.id, reward.reward)])
}
case 6: // music
case 12: // ultima
{
return _('div', { className: 'map_icon_music' }, [getRewardIconImg(reward.id, reward.reward)])
}
case 7: // mapIcon
case 9: // avatarAccessory
{
return _('div', { className: 'map_icon_avatar' }, [getRewardIconImg(reward.id, reward.reward)])
}
}
return _('div', { className: 'map_icon_limitbreak' }, [_('text', reward.reward)])
}
function getRewardIconImg(id, text) {
return _('img', { src: `https://estertion.win/__private__/chuni-rewards-${server}/${id}.webp`, event: { error: e => {
e.target.parentNode.replaceChild(_('text', text), e.target)
} }})
}
const initialPages = {}
function getPage(mapTitle, page) {
if (initialPages[mapTitle] && initialPages[mapTitle][page]) return initialPages[mapTitle][page]
const map = mapInfo.find(m => m.name === mapTitle)
if (!map) {
alert(`未找到地图\n${mapTitle}\n第 ${page} 页`)
throw new Error(`未找到地图\n${mapTitle}\n第 ${page} 页`)
}
return generatePage(map, page)
}
function addClickHandler() {
const mapBlocks = [...document.querySelectorAll('.map_block')]
mapBlocks.forEach(block => {
const mapTitle = block.querySelector('.map_title_text').textContent.trim()
const currentPage = block.querySelector('.map_title_page_num').textContent *1
initialPages[mapTitle] = initialPages[mapTitle] || { [currentPage]: block, currentPage }
const totalPage = block.querySelector('.map_title_page_den').textContent *1
block.insertBefore(_('div', { className: ['map-paginator', currentPage>1?'has-prev':'', currentPage<totalPage?'has-next':''].join(' ') }, [
_('span', { className: 'prev', event: { click: _ => { block.parentNode.replaceChild(getPage(mapTitle, currentPage - 1), block); executeMarquee() } } }, [_('text', '<')]),
_('span', { className: 'next', event: { click: _ => { block.parentNode.replaceChild(getPage(mapTitle, currentPage + 1), block); executeMarquee() } } }, [_('text', '>')]),
]), block.firstChild)
const tiles = [...block.querySelectorAll('.maparea_block')]
const clearedCount = document.querySelectorAll('.map_clear').length
tiles.forEach((tile, i) => {
if (!tile.querySelector('.maparea')) return
const remainingTiles = tile.querySelector('.map_remain_text').textContent *1
tile.classList.add('user_data_friend_tap')
tile.addEventListener('click', () => showMapAreaDetail(mapTitle, currentPage, i, clearedCount, remainingTiles))
})
})
}
function addRemainingMaps() {
const container = document.querySelector('.box01')
container.appendChild(_('div', { className: 'box01_title text_b' }, [_('text', '其他地图')]))
mapInfo.forEach(map => {
const mapTitle = map.name
if (initialPages[mapTitle]) return
initialPages[mapTitle] = { currentPage: 1 }
container.appendChild(generatePage(map, 1))
})
}
function addGlobalStyle() {
document.head.appendChild(_('style', {}, [_('text', [
'html.showing-detail{overflow:hidden}',
'.map-detail-background{',
' position:fixed;',
' top:0;',
' left:0;',
' width:100vw;',
' height:100vh;',
' background-color:rgba(0,0,0,0.5);',
'}',
'.map-detail{',
' margin: 0 auto;',
' background-color: white;',
' padding: 30px 20px 40px;',
' cursor: default;',
'}',
'.map-detail .map-reward.reward-got{',
' color: #AAA;',
'}',
'.bonus-target-cell{',
' padding: 0 0.5em;',
' max-width: 320px;',
' word-break: break-all;',
'}',
'.reward-name-cell{',
' max-width: 250px;',
' word-break: break-all;',
'}',
'.map-paginator{ position:relative; user-select:none; -webkit-user-select:none; }',
'.map-paginator span{',
' font-weight: bold;',
' position: absolute;',
' color: white;',
' margin-top: 4px;',
' text-shadow: 1px 0px 2px black,-1px 0px 2px black,0px 1px 2px black,0px -1px 2px black;',
'}',
'.map-paginator span.prev{ right: 3.5em; }',
'.map-paginator span.next{ right: -0.2em; }',
'.map-paginator:not(.has-prev) span.prev{ display:none; }',
'.map-paginator:not(.has-next) span.next{ display:none; }',
].join('\n'))]))
}
function executeMarquee() {
document.head.appendChild(_('script', {}, [_('text', `
$(function() {
// 許容する最大のWidth
var widthMax = 88;
$('.map_skillseed_text').each(function() {
var span = $(this).children('span');
// 称号文字列を表示しているSpanのWidth取得
var textWidth = span.innerWidth();
// Widthが許容値を超えていればマーキーさせる
if (textWidth > widthMax) {
$(this).marquee({speed : 60});
}
});
});
`)])).remove()
}
function showMapAreaDetail(mapTitle, currentPage, index, clearedCount, remainingTiles) {
const map = mapInfo.find(m => m.name === mapTitle)
if (!map) return alert('未找到地图')
const area = map.areas.find(a => a.page === currentPage-1 && a.position === index)
if (!area) return alert('未找到区域')
const totalPage = map.areas.slice(-1)[0].page + 1
const shrinkLength = area.shrink.slice(0, clearedCount).reduce((a,b)=>a+b, 0)
const length = area.rewards.slice(-1)[0].position - shrinkLength
const position = length - remainingTiles
document.body.parentNode.classList.add('showing-detail')
const detailContainer = document.body.appendChild(_('div', { className: 'map-detail-background', event: { click: _ => { detailContainer.remove(); document.body.parentNode.classList.remove('showing-detail') } } }, [
_('div', { className: 'map-detail w460 text_l' }, [
_('div', { className: 'text_b' }, [_('text', map.name)]),
_('div', {}, [_('text', `第 ${currentPage}/${totalPage} 页,第 ${index+1} 格`)]),
_('br'),
_('div', {}, [_('text', area.required > 0 ? `区域解锁:${clearedCount} / ${area.required}` : '')]),
_('div', {}, [_('text', area.boost > 1 ? `跑图加成x${area.boost}` : '')]),
_('div', {}, area.shrink.map((v,i)=>[i,v]).filter(v => v[1]>0).map(([count, shrink]) => _('div', {}, [_('text', `完成${count}个区域后缩短${shrink}格`)]))),
_('table', {}, area.bonus.map(showBonus)),
_('br'),
_('div', {}, [_('text', `当前位置:${position} / ${length}`)]),
_('table', {}, [_('tbody', {}, area.rewards.map(reward => _('tr', { className: 'map-reward '+(reward.position <= position ? 'reward-got' : 'reward-not-got') }, [
_('td', { className: 'text_r' }, [_('text', reward.position + '格')]),
_('td', { className: 'text_r', style:{padding:'0 0.5em'} }, [_('text', reward.position <= position ? '已取得' : ('还剩'+(reward.position - position)+'格'))]),
_('td', { className: 'text_l reward-name-cell' }, [_('text', reward.reward)]),
])))]),
_('div', {}, [_('text', map.task ? `课题曲:${map.task}` : '')]),
])
]))
}
function showBonus(bonus) {
switch (bonus.type) {
case 'chara':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '使用角色')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'charaWorks':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '使用分类角色')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'skill':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '使用技能')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'skillCategory':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '使用技能类型')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'music':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '游玩歌曲')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'musicGenre':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '游玩分类歌曲')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'musicWorks':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '游玩来源歌曲')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'musicDif':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '游玩难度')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'musicLv':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '游玩歌曲等级')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
case 'charaRank':
return _('tr', {}, [
_('td', { className: 'text_r'}, [_('text', '角色等级大于')]),
_('td', { className: 'bonus-target-cell' }, [_('text', bonus.target)]),
_('td', {}, [_('text', `+${bonus.point}`)]),
])
}
return _('div', {}, [_('text', `${bonus.type} ${bonus.target} +${bonus.point}`)])
}
loadLocalInfo()
checkUpdateForLocalInfo()
addClickHandler()
addRemainingMaps()
addGlobalStyle()