chuni-net - Map Details

Display map details on chunithm-net

当前为 2024-10-25 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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()