mirror of
https://github.com/yoimiya-kokomi/miao-plugin.git
synced 2024-11-22 06:58:24 +00:00
482 lines
14 KiB
JavaScript
482 lines
14 KiB
JavaScript
import { Common } from '#miao'
|
||
import { MysApi, Player, Character } from '#miao.models'
|
||
import moment from 'moment'
|
||
import lodash from 'lodash'
|
||
import cheerio from 'cheerio'
|
||
import fetch from 'node-fetch'
|
||
|
||
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
|