/* * 胡桃数据库的统计 * * */ import lodash from 'lodash' import { Cfg, Common, App, Data } from '../components/index.js' import { Abyss, Character, MysApi, Player } from '../models/index.js' import HutaoApi from './wiki/HutaoApi.js' let app = App.init({ id: 'stat', name: '深渊统计' }) app.reg('cons-stat', consStat, { rule: /^#(喵喵)?角色(持有|持有率|命座|命之座|.命)(分布|统计|持有|持有率)?$/, desc: '【#统计】 #角色持有率 #角色5命统计' }) app.reg('abyss-pct', abyssPct, { rule: /^#(喵喵)?深渊(第?.{1,2}层)?(角色)?(出场|使用)(率|统计)*$/, desc: '【#统计】 #深渊出场率 #深渊12层出场率' }) app.reg('abyss-team', abyssTeam, { rule: /#深渊(组队|配队)/, describe: '【#角色】 #深渊组队' }) app.reg('upload-data', uploadData, { rule: /^#*(喵喵|上传|本期)*(深渊|深境|深境螺旋)[ |0-9]*(数据)?$/, desc: '上传深渊' }) export default app async function consStat (e) { let consData = await HutaoApi.getCons() let overview = await HutaoApi.getOverview() if (!consData) { e.reply('角色持有数据获取失败,请稍后重试~') return true } let msg = e.msg let mode = /持有/.test(msg) ? 'char' : 'cons' let conNum = -1 if (mode === 'cons') { lodash.forEach([/0|零/, /1|一/, /2|二/, /3|三/, /4|四/, /5|五/, /6|六|满/], (reg, idx) => { if (reg.test(msg)) { conNum = idx return false } }) } if (!consData && !consData.data) { return true } let data = consData.data let Lumine = lodash.filter(data, (ds) => ds.avatar === 10000007)[0] || {} let Aether = lodash.filter(data, (ds) => ds.avatar === 10000005)[0] || {} Lumine.holdingRate = (1 - Aether.holdingRate) || Lumine.holdingRate let ret = [] lodash.forEach(data, (ds) => { let char = Character.get(ds.avatar) let data = { name: char.name || ds.avatar, abbr: char.abbr, star: char.star || 3, side: char.side, hold: ds.holdingRate } if (mode === 'char') { data.cons = lodash.map(ds.rate, (c) => { c.value = c.value * ds.holdingRate return c }) } else { data.cons = ds.rate } data.cons = lodash.sortBy(data.cons, ['id']) ret.push(data) }) if (conNum > -1) { ret = lodash.sortBy(ret, [`cons[${conNum}].value`]) ret.reverse() } else { ret = lodash.sortBy(ret, ['hold']) } // 渲染图像 return await Common.render('stat/character', { chars: ret, mode, conNum, totalCount: overview?.data?.totalPlayerCount || 0, lastUpdate: consData.lastUpdate, pct: function (num) { return (num * 100).toFixed(2) } }, { e, scale: 1.5 }) } async function abyssPct (e) { let mode = /使用/.test(e.msg) ? 'use' : 'pct' let modeName let abyssData let modeMulti = 1 if (mode === 'use') { modeName = '使用率' abyssData = await HutaoApi.getAbyssUse() } else { modeName = '出场率' abyssData = await HutaoApi.getAbyssPct() modeMulti = 8 } let overview = await HutaoApi.getOverview() if (!abyssData) { e.reply(`深渊${modeName}数据获取失败,请稍后重试~`) return true } let ret = [] let chooseFloor = -1 let msg = e.msg const floorName = { 12: '十二层', 11: '十一层', 10: '十层', 9: '九层' } // 匹配深渊楼层信息 lodash.forEach(floorName, (cn, num) => { let reg = new RegExp(`${cn}|${num}`) if (reg.test(msg)) { chooseFloor = num return false } }) let data = abyssData.data data = lodash.sortBy(data, 'floor') data = data.reverse() lodash.forEach(data, (floorData) => { let avatars = [] lodash.forEach(floorData.avatarUsage, (ds) => { let char = Character.get(ds.id) if (char) { avatars.push({ name: char.name, star: char.star, value: ds.value * modeMulti, face: char.face }) } }) avatars = lodash.sortBy(avatars, 'value', ['asc']) avatars.reverse() if (chooseFloor === -1) { avatars = avatars.slice(0, 14) } ret.push({ floor: floorData.floor, avatars }) }) return await Common.render('stat/abyss-pct', { abyss: ret, floorName, chooseFloor, mode, modeName, totalCount: overview?.data?.collectedPlayerCount || 0, lastUpdate: abyssData.lastUpdate }, { e, scale: 1.5 }) } async function abyssTeam (e) { let mys = await MysApi.init(e, 'cookie') if (!mys || !mys.uid || !mys.isSelfCookie) { return true } let player = Player.create(e) await player.refreshMysDetail() await player.refreshTalent() let abyssData = await HutaoApi.getAbyssTeam() if (!abyssData || !abyssData.data) { e.reply('深渊组队数据获取失败,请稍后重试~') return true } abyssData = abyssData.data let avatarData = player.getAvatarData() let avatarRet = {} let data = {} let noAvatar = {} lodash.forEach(avatarData, (avatar) => { let t = avatar.originalTalent avatarRet[avatar.id] = Math.min(avatar.level, avatar.weapon?.level || 1) * 100 + Math.max(t?.a, t?.e, t?.q) * 1000 }) let getTeamCfg = (str) => { let teams = str.split(',') teams.sort() let teamMark = 0 lodash.forEach(teams, (a) => { if (!avatarRet[a]) { teamMark = -1 noAvatar[a] = true } if (teamMark !== -1) { teamMark += avatarRet[a] * 1 } }) if (teamMark === -1) { teamMark = 1 } return { key: teams.join(','), mark: teamMark } } let hasSame = function (team1, team2) { for (let idx = 0; idx < team1.length; idx++) { if (team2.includes(team1[idx])) { return true } } return false } lodash.forEach(abyssData, (ds) => { let floor = ds.floor if (!data[floor]) { data[floor] = { up: {}, down: {}, teams: [] } } lodash.forEach(['up', 'down'], (halfKey) => { lodash.forEach(ds[halfKey], (ds) => { let teamCfg = getTeamCfg(ds.item) if (teamCfg) { if (!data[floor][halfKey][teamCfg.key]) { data[floor][halfKey][teamCfg.key] = { count: 0, mark: 0, hasTeam: teamCfg.mark > 1 } } data[floor][halfKey][teamCfg.key].count += ds.rate data[floor][halfKey][teamCfg.key].mark += ds.rate * teamCfg.mark } }) }) let temp = [] lodash.forEach(['up', 'down'], (halfKey) => { lodash.forEach(data[floor][halfKey], (ds, team) => { temp.push({ team, teamArr: team.split(','), half: halfKey, count: ds.count, mark: ds.mark, mark2: 1, hasTeam: ds.hasTeam }) }) temp = lodash.sortBy(temp, 'mark') data[floor].teams = temp.reverse() }) }) let ret = {} lodash.forEach(data, (floorData, floor) => { ret[floor] = {} let ds = ret[floor] lodash.forEach(floorData.teams, (t1) => { if (t1.mark2 <= 0) { return true } lodash.forEach(floorData.teams, (t2) => { if (t1.mark2 <= 0) { return true } if (t1.half === t2.half || t2.mark2 <= 0) { return true } let teamKey = t1.half === 'up' ? (t1.team + '+' + t2.team) : (t2.team + '+' + t1.team) if (ds[teamKey]) { return true } if (hasSame(t1.teamArr, t2.teamArr)) { return true } ds[teamKey] = { up: t1.half === 'up' ? t1 : t2, down: t1.half === 'up' ? t2 : t1, count: Math.min(t1.count, t2.count), mark: t1.hasTeam && t2.hasTeam ? t1.mark + t2.mark : t1.count + t2.count // 如果不存在组队则进行评分惩罚 } t1.mark2-- t2.mark2-- return false }) if (lodash.keys(ds).length >= 20) { return false } }) }) lodash.forEach(ret, (ds, floor) => { ds = lodash.sortBy(lodash.values(ds), 'mark') ds = ds.reverse() ds = ds.slice(0, 4) lodash.forEach(ds, (team) => { team.up.teamArr = Character.sortIds(team.up.teamArr) team.down.teamArr = Character.sortIds(team.down.teamArr) }) ret[floor] = ds }) let avatarMap = {} lodash.forEach(avatarData, (ds) => { let char = Character.get(ds.id) avatarMap[ds.id] = { id: ds.id, name: ds.name, star: ds.star, level: ds.level, cons: ds.cons, face: char.face } }) lodash.forEach(noAvatar, (d, id) => { let char = Character.get(id) avatarMap[id] = { id, name: char.name, face: char.face, star: char.star, level: 0, cons: 0 } }) return await Common.render('stat/abyss-team', { teams: ret, avatars: avatarMap }, { e, scale: 1.5 }) } async function uploadData (e) { let isMatch = /^#(喵喵|上传)深渊(数据)?$/.test(e.original_msg || e.msg || '') if (!Cfg.get('uploadAbyssData', false) && !isMatch) { return false } let mys = await MysApi.init(e, 'all') if (!mys || !mys.uid) { if (isMatch) { e.reply(`请绑定ck后再使用${e.original_msg || e.msg}`) } return false } let ret = {} let uid = mys.uid let player = Player.create(e) let resDetail, resAbyss try { resAbyss = await mys.getSpiralAbyss(1) let lvs = Data.getVal(resAbyss, 'floors.0.levels.0') // 检查是否查询到了深渊信息 if (!lvs || !lvs.battles) { e.reply('暂未获得本期深渊挑战数据...') return true } else if (lvs && lvs.battles && lvs.battles.length === 0) { if (!mys.isSelfCookie) { if (isMatch) { e.reply(`请绑定ck后再使用${e.original_msg || e.msg}`) } return false } } resDetail = await mys.getCharacter() if (!resDetail || !resAbyss || !resDetail.avatars || resDetail.avatars.length <= 3) { e.reply('角色信息获取失败') return true } delete resDetail._res delete resAbyss._res ret = await HutaoApi.uploadData({ uid, resDetail, resAbyss }) } catch (err) { // console.log(err); } // 更新player信息 player.setMysCharData(resDetail) if (ret && ret.retcode === 0) { let stat = [] if (ret.data) { if (resAbyss.floors.length === 0) { e.reply('暂未获得本期深渊挑战数据...') return true } let abyss = new Abyss(resAbyss) let abyssData = abyss.getData() let avatarIds = abyss.getAvatars() let overview = ret.info || (await HutaoApi.getOverview())?.data || {} let addMsg = function (title, ds) { let tmp = {} if (!ds) { return false } if (!ds.avatarId && !ds.id) { return false } let char = Character.get(ds.avatarId || ds.id) tmp.title = title tmp.id = char.id tmp.value = `${(ds.value / 10000).toFixed(1)} W` let msg = [] tmp.msg = msg let pct = (percent, name) => { if (percent < 0.2) { msg.push({ title: '少于', value: (Math.max(0.1, 100 - percent * 100)).toFixed(1), name: name }) } else { msg.push({ title: '超过', value: (Math.min(99.9, percent * 100)).toFixed(1), name: name }) } } if (ds.percent) { pct(ds.percent, char.sName) pct(ds.percentTotal, '总记录') } else { msg.push({ txt: '暂无统计信息' }) } stat.push(tmp) } addMsg('最强一击', ret.data?.damage || abyssData?.stat?.dmg || {}) addMsg('最高承伤', ret.data?.takeDamage || abyssData?.stat.takeDmg || {}) let abyssStat = abyssData?.stat || {} lodash.forEach({ defeat: '最多击破', e: '元素战技', q: '元素爆发' }, (title, key) => { if (abyssStat[key]) { stat.push({ title, id: abyssStat[key]?.id || 0, value: `${abyssStat[key]?.value}次` }) } else { stat.push({}) } }) await player.refreshTalent(avatarIds) let avatarData = player.getAvatarData(avatarIds) return await Common.render('stat/abyss-summary', { abyss: abyssData, avatars: avatarData, stat, save_id: uid, totalCount: overview?.collectedPlayerCount || 0, uid }, { e, scale: 1.2 }) } else { e.reply('暂未获得本期深渊挑战数据...') return true } } else { e.reply(`${ret.message || '上传失败'},请稍后重试...`) } return true }