miao-plugin/apps/profile/ProfileStat.js
2024-10-30 00:44:53 +08:00

482 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Common } from '#miao'
import { MysApi, Player, Character } from '#miao.models'
import moment from 'moment'
import lodash from 'lodash'
import fetch from 'node-fetch'
import * as cheerio from 'cheerio'
const ProfileStat = {
async stat (e) {
return ProfileStat.render(e, 'stat', false)
},
async roleStat(e) {
return ProfileStat.render(e, 'stat', true)
},
async avatarList (e) {
return ProfileStat.render(e, 'avatar')
},
async refreshTalent (e) {
let game = /星铁/.test(e.msg) ? 'sr' : 'gs'
e.isSr = game === 'sr'
let mys = await MysApi.init(e)
if (!mys || !mys.uid) return false
let player = Player.create(e, game)
let refreshCount = await player.refreshTalent('', 2)
if (refreshCount && !e.isSr) {
e.reply(`角色天赋更新成功,共${refreshCount}个角色\n你现在可以通过【#练度统计】【#天赋统计】来查看角色信息了...`)
} else if (e.isSr) {
e.reply(`角色行迹更新成功,共${refreshCount}个角色\n你现在可以通过【*练度统计】来查看角色信息了...`)
} else {
e.reply('角色天赋未能更新...')
}
},
getStarFilterFunc(e) {
let msg = e.msg.replace(/#星铁|#/, '').trim()
// starFilter: 检测是否有星级筛选
let requiredStar = 0
if (/(五|四|5|4|)+星/.test(msg)) {
requiredStar = /(五|5)+星/.test(msg) ? 5 : 4
}
let starFilter = ds => true;
if (requiredStar) {
starFilter = ds => ds.star === requiredStar
}
return starFilter
},
getElementFilerFromElements(requiredElements) {
return ds => requiredElements.includes(ds.elem)
},
getIdFilterFunc(requiredIds) {
return ds => requiredIds.includes(ds.id)
},
getElementFilterFunc(e) {
let msg = e.msg.replace(/#星铁|#/, '').trim()
// elementFilter: 检测是否有元素筛选
let requiredElements = []
let chineseToEnglishElements = {}
if (e.isSr) {
// 先给星铁的元素筛选留空
} else {
chineseToEnglishElements = {
'风': 'anemo',
'岩': 'geo',
'雷': 'electro',
'草': 'dendro',
'水': 'hydro',
'火': 'pyro',
'冰': 'cryo'
}
}
for (let [k, v] of Object.entries(chineseToEnglishElements)) {
// 如果后续需支持星铁,这里可能也要用到正则判断
// e.g. 物(理)? 量(子)? 虚(数)?
if (msg.includes(k)) {
requiredElements.push(v)
}
}
let elementFilter = ds => true;
if (requiredElements.length > 0) {
elementFilter = ProfileStat.getElementFilerFromElements(requiredElements)
}
return elementFilter
},
getFilterFunc(e) {
let starFilter = ProfileStat.getStarFilterFunc(e)
let elementFilter = ProfileStat.getElementFilterFunc(e)
// 组合函数
let combinedFilter = lodash.overEvery([starFilter, elementFilter])
return combinedFilter
},
getRoleFilterFunc(e, elements, invitationCharacterIds) {
let invitationCharacterFilter = ProfileStat.getIdFilterFunc(invitationCharacterIds)
let elementFilter = ProfileStat.getElementFilerFromElements(elements)
// 组合函数
let combinedFilter = lodash.overSome([invitationCharacterFilter, elementFilter])
let levelFilter = ProfileStat.getLevelFilterFunc()
combinedFilter = lodash.overEvery([combinedFilter, levelFilter])
return combinedFilter
},
getLevelFilterFunc() {
return ds => ds.level >= 70
},
async getOverallMazeData() {
const request_url = 'https://homdgcat.wiki/gi/CH/maze.js'
let resData = false
try {
resData = await (await fetch(request_url)).text()
} catch (error) {
logger.error('请求失败:', error)
return false // 直接返回以停止后续逻辑
}
const match = /var _overall = (.*?)var/s.exec(resData)
let overallMazeInfo = []
if (match) {
overallMazeInfo = JSON.parse(match[1])
} else {
logger.error('响应内容格式不对劲')
return false
}
return overallMazeInfo
},
sendRoleCombatInfo(e, elements, initialCharacterIds, invitationCharacterIds) {
const response = [
ProfileStat.getElementInfo(elements),
ProfileStat.getInitialCharacterInfo(initialCharacterIds),
ProfileStat.getInvitationCharacterInfo(invitationCharacterIds),
].join('\n')
e.reply(response)
},
getElementInfo(elements) {
// 让我们说中文!
const englishToChineseElements = {
'anemo': '风',
'geo': '岩',
'electro': '雷',
'dendro': '草',
'hydro': '水',
'pyro': '火',
'cryo': '冰'
}
// 使用 lodash 将元素转换为中文名称,并用'、'组合成'风、岩'
const chineseElements = lodash.map(elements, (element) => englishToChineseElements[element]).join('、');
return `限制元素:${chineseElements}`
},
getInitialCharacterInfo(initialCharacterIds) {
let characters = lodash.compact(lodash.map(initialCharacterIds, (id) => Character.get(id)))
let characterNames = lodash.map(characters, (character) => character.name).join('、')
return `开幕角色:${characterNames}`
},
getInvitationCharacterInfo(invitationCharacterIds) {
let characters = lodash.compact(lodash.map(invitationCharacterIds, (id) => Character.get(id)))
let characterNames = lodash.map(characters, (character) => character.name).join('、')
return `特邀角色:${characterNames}`
},
// TODO: BWiki 源的数据暂时没弄完,没接入逻辑中,暂时没啥必要?
async getOverallMazeLinkFromBWiki() {
const request_url = 'https://wiki.biligame.com/ys/%E5%B9%BB%E6%83%B3%E7%9C%9F%E5%A2%83%E5%89%A7%E8%AF%97'
try {
// 发送 GET 请求
const html = await (await fetch(request_url)).text()
// 加载 HTML
const $ = cheerio.load(html);
// 存储 href 属性的数组
const links = [];
// 查找 id="每期详情" 下的所有 <a> 标签
$('#每期详情').closest('h2').next('p').find('a').each((index, element) => {
const href = $(element).attr('href');
if (href) {
links.push(href);
}
});
return links
} catch (error) {
console.error('Error fetching the URL:', error.message);
}
},
async getRequestedMazeDataFromBWiki(e, links) {
const mazeId = ProfileStat.getMazeId(e)
if (mazeId >= 0 && mazeId < overallMazeData.length) {
const request_url = `https://wiki.biligame.com/${links[mazeId]}`
// 发送 GET 请求
const html = await (await fetch(request_url)).text()
// 加载 HTML
const $ = cheerio.load(html);
// 存储 href 属性的数组
const elements = [];
$('#限定元素').closest('h2').next('p').find('img').each((index, element) => {
const alt = $(element).attr('alt');
const match = /卡牌UI\-元素\-(.*?)\.png/.exec(alt);
if (match) {
elements.push(match[1]);
}
});
// TODO: 剩下的解析,并且转换成和 HomDGCat 相同的格式
return elements;
} else {
return false
}
},
getMazeId(e) {
const match = /202(\d{3})/.exec(e.msg)
if (!match) {
return false
}
const num = +match[1]
const numYear = Math.floor(num / 100)
const numMonth = num % 100
const newNum = numYear * 12 + numMonth - 1
const mazeId = newNum - (4 * 12 + 7 - 1)
return mazeId
},
extractRequestedMazeData(e, overallMazeData) {
const mazeId = ProfileStat.getMazeId(e)
if (mazeId >= 0 && mazeId < overallMazeData.length) {
return overallMazeData[mazeId]
} else {
return false
}
},
extractInitialCharacterIds(mazeData) {
return lodash.map(mazeData.Initial, item => item.ID + 10000000);
},
extractInvitationCharacterIds(mazeData) {
return lodash.map(mazeData.Invitation, item => item.ID + 10000000);
},
extractElements(mazeData) {
const elementMap = {
'Wind': 'anemo',
'Rock': 'geo',
'Elec': 'electro',
'Grass': 'dendro',
'Water': 'hydro',
'Fire': 'pyro',
'Ice': 'cryo'
}
return lodash.map(mazeData.Elem, item => elementMap[item]);
},
mergeStart (avatars, initialAvatarIds) {
let initialAvatars = []
lodash.forEach(initialAvatarIds, (id) => {
let char = Character.get(id)
if (char) {
initialAvatars.push({
id: id,
name: char.name,
elem: char.elem,
abbr: char.abbr,
star: char.star,
face: char.face,
level: 80,
cons: 0,
talent: {
a: {
level: 8,
original: 8
},
e: {
level: 8,
original: 8
},
q: {
level: 8,
original: 8
}
}
})
}
})
// mergedAvatars: 求 avatars 和 initialAvatars 的并集
// 判断标准为这些元素的 id 属性
// 如果 avatars 和 initialAvatars 中的元素 id 属性相同,则比较这两个元素的 level 属性,
// 选取 level 较大的那个元素放入 mergedAvatars
// 注意:即使 avatars 和 initialAvatars 中的元素除了 id 属性相同,其他属性完全不同,
// 但在 id 属性相同的情况下,仍需放入整个元素
let mergedAvatars = []
// 合并逻辑实现
let avatarMap = new Map();
// 遍历 avatars将每个元素加入到 avatarMap 中
avatars.forEach(avatar => {
avatarMap.set(avatar.id, avatar);
});
// 遍历 initialAvatars进行合并
initialAvatars.forEach(initialAvatar => {
if (avatarMap.has(initialAvatar.id)) {
// 如果 id 相同,比较 level选取较大的元素
let existingAvatar = avatarMap.get(initialAvatar.id);
avatarMap.set(initialAvatar.id,
existingAvatar.level >= initialAvatar.level ? existingAvatar : initialAvatar);
} else {
// 如果 id 不同,直接加入
avatarMap.set(initialAvatar.id, initialAvatar);
}
});
// 将合并后的结果转换为数组并返回
mergedAvatars = Array.from(avatarMap.values());
// 排序
// 按照元素进行区分
let sortKey = 'elem,level,star,aeq,cons,weapon.level,weapon.star,weapon.affix,fetter'.split(',')
mergedAvatars = lodash.orderBy(mergedAvatars, sortKey)
mergedAvatars = mergedAvatars.reverse()
return mergedAvatars;
},
// 渲染
// mode stat:练度统计 avatar:角色列表 talent:天赋统计
async render (e, mode = 'stat', isRole = false) {
let game = /星铁/.test(e.msg) ? 'sr' : 'gs'
e.isSr = game === 'sr'
e.game = game
// 缓存时间,单位小时
let msg = e.msg.replace(/#星铁|#/, '').trim()
if (msg === '角色统计' || msg === '武器统计') {
// 暂时避让一下抽卡分析的关键词
return false
}
if (/天赋|技能/.test(msg)) {
mode = 'talent'
}
let mys = await MysApi.init(e)
if (!mys || !mys.uid) return false
const uid = mys.uid
let player = Player.create(e, game)
let avatarRet = await player.refreshAndGetAvatarData({
index: 2,
detail: 1,
talent: mode === 'avatar' ? 0 : 1,
rank: true,
materials: mode === 'talent',
retType: 'array',
sort: true
})
if (avatarRet.length === 0) {
e._isReplyed || e.reply(`查询失败,暂未获得#${uid}角色数据请绑定CK或 #更新面板`)
return true
}
let filterFunc = (x) => true
if (isRole) {
let overallMazeData = await ProfileStat.getOverallMazeData()
if (!overallMazeData) {
e.reply(`请求 HomDGCat 数据库出错`)
return false
}
let currentMazeData = ProfileStat.extractRequestedMazeData(e, overallMazeData)
if (!currentMazeData) {
const n = overallMazeData.length - 1 + 4 * 12 + 7 - 1
const maxYear = Math.floor(n / 12)
const maxMonth = n % 12 + 1
const formattedMonth = String(maxMonth).padStart(2, '0'); // 将月份格式化为两位数
const response = [
`当前月份不在 HomDGCat 数据库中`,
`可供查询的月份202407 - 202${maxYear}${formattedMonth}`
].join('\n')
e.reply(response)
return false
}
let initialCharacterIds = ProfileStat.extractInitialCharacterIds(currentMazeData)
let invitationCharacterIds = ProfileStat.extractInvitationCharacterIds(currentMazeData)
let elements = ProfileStat.extractElements(currentMazeData)
// 发送简要的信息
ProfileStat.sendRoleCombatInfo(e, elements, initialCharacterIds, invitationCharacterIds)
avatarRet = ProfileStat.mergeStart(avatarRet, initialCharacterIds)
filterFunc = ProfileStat.getRoleFilterFunc(e, elements, invitationCharacterIds)
} else {
filterFunc = ProfileStat.getFilterFunc(e)
}
avatarRet = lodash.filter(avatarRet, filterFunc)
let now = moment(new Date())
if (now.hour() < 4) {
now = now.add(-1, 'days')
}
let week = now.weekday()
if (mode === 'talent') {
let weekRet = /周([1-6]|一|二|三|四|五|六)/.exec(msg)
let weekSel = weekRet?.[1]
if (/(今日|今天)/.test(msg)) {
weekSel = week + 1
} else if (/(明天|明日)/.test(msg)) {
now = now.add(1, 'days')
weekSel = now.weekday() + 1
}
let weekFilter = (weekSel * 1) || ('一二三四五六'.split('').indexOf(weekSel) + 1)
if (weekFilter && weekFilter !== 7) {
avatarRet = lodash.filter(avatarRet, ds => ds?.materials?.talent?.num === ['周一/周四', '周二/周五', '周三/周六'][(weekFilter - 1) % 3])
}
}
let faceChar = Character.get(player.face) || Character.get(avatarRet[0]?.id)
let imgs = faceChar.imgs
let face = {
banner: imgs?.banner,
face: imgs?.face,
qFace: imgs?.qFace,
name: player.name || `#${uid}`,
sign: player.sign,
level: player.level
}
let info = player.getInfo()
info.stats = info.stats || {}
info.statMap = {
achievement: '成就',
wayPoint: '锚点',
avatar: '角色',
avatar5: '五星角色',
goldCount: '金卡总数'
}
let tpl = mode === 'avatar' ? 'character/avatar-list' : 'character/profile-stat'
return await Common.render(tpl, {
save_id: uid,
uid,
info,
updateTime: player.getUpdateTime(),
isSelfCookie: e.isSelfCookie,
face,
mode,
week,
avatars: avatarRet,
game
}, { e, scale: 1.4 })
}
}
export default ProfileStat