UNPKG

koishi-plugin-ba-raid

Version:
376 lines (375 loc) 15.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.apply = exports.inject = exports.Config = exports.usage = exports.name = void 0; const koishi_1 = require("koishi"); const x_crawl_1 = __importDefault(require("x-crawl")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const data_1 = require("./data"); exports.name = 'ba-raid'; exports.usage = '## 感谢以下数据源:\n- [AronaAi](https://arona.ai/graph/):提供了47期往后的总力统计信息\n- [bawiki](https://ba.gamekee.com/):提供了总力概览信息\n' + '### 注意事项:\n- 使用图形化输出:Linux服务器的bot可能出现图形中文乱码的现象,请自行安装微软雅黑字体。\n' + '### 功能和指令:\n - 功能:分数线,最高分,通关人数,排名或分数互查(仅二档内),查看boss上场次数。\n' + '- 指令:zlcx bosscx rankcx,详情请自行help\n' + '### 反馈及建议:\n- 请发送邮件至eruru.sion@gmail.com'; exports.Config = koishi_1.Schema.object({ 'toPhoto': koishi_1.Schema.boolean().default(true).description('图形化渲染'), }); exports.inject = { required: ['canvas', 'database'], }; function apply(ctx, config) { const myXCrawl = (0, x_crawl_1.default)({ timeout: 10000, intervalTime: { max: 2000, min: 1000 }, maxRetry: 3 }); const totalPath = path_1.default.join(ctx.baseDir, 'cache', 'total'); let dispo = true; ctx.on('ready', async () => { fs_1.default.mkdirSync(totalPath, { recursive: true }); await data_1.DB.initTotalTable(ctx); //await getRecord(ctx) //await autoCrawl() }); ctx.on('dispose', () => { dispo = false; }); ctx.command('zlcx', '获取总力数据(47期往后)。').alias('总力查询').example('zlcx 60') .usage("可选参数:期数\n快捷查询:\n总力人数查询(zlcx -l)\n总力最高分查询(zlcx -t)") .option('l', '查询难度通关人数').shortcut("总力人数查询", { options: { l: true } }) .option('t', '查询难度最高分').shortcut("总力最高分查询", { options: { t: true } }) .action(async ({ session, options }, ...args) => { try { let data; let res = args.length === 0 ? await ctx.database.select('ba_jp_total').orderBy('season', 'desc').limit(1).execute() : await ctx.database.get('ba_jp_total', { season: parseInt(args[0]) }); if (options.t) { data = await queryTotal(res[0], 3); } else if (options.l) { data = await queryTotal(res[0], 2); } else { data = await queryTotal(res[0], 1); } if (config.toPhoto) { data = koishi_1.h.image(toPng(data)); } await session.send(data); } catch (error) { session.send("查无此期数据"); } }); ctx.command('bosscx', '查询指定总力boss登场记录').alias('总力boss查询').example('bosscx 大蛇').usage('必选参数:boss名字') .action(async ({ session }, ...args) => { let bossName = args[0]; for (const key in data_1.mapper) { if (data_1.mapper[key].includes(args[0])) { bossName = key; break; } } try { let resultData; let res = await ctx.database.get('ba_jp_total', { boss_name: bossName }); let data = await queryTotal(res[res.length - 1], 1); resultData = `${res[0].boss_name}共出现:${res.length}期\n最近一期:第${res[res.length - 1].season}期\n${data}`; if (config.toPhoto) { resultData = koishi_1.h.image(toPng(resultData)); } await session.send(resultData); } catch (error) { session.send("查无此Boss"); } }); ctx.command('rankcx', '功能1:根据分数查询排名\n功能2:根据排名查询分数').alias('总力排名查询') .usage('可选参数:期数\n必选参数:排名或分数\n查询范围:只提供二档以内的查询\n快捷查询:\n总力排名查询(rankcx)\n总力分数查询(rankcx -s)').example('rankcx 60 27654545') .option('s', '根据排名查询分数').shortcut("总力分数查询", { options: { s: true } }).example('rankcx 期数 分数/排名') .action(async ({ session, options }, ...args) => { if (args.length === 0) return options.s ? "输入你要查询的分数" : "输入你要查询的排名"; try { //分数和排名查询 let query = options.s ? 4 : 5; let score = args.length === 1 ? parseInt(args[0]) : parseInt(args[1]); if (Number.isNaN(score)) { return '输入异常'; } let data; //默认查询和选期查询 let res = args.length === 1 ? await ctx.database.select('ba_jp_total').orderBy('season', 'desc').limit(1).execute() : await ctx.database.get('ba_jp_total', { season: parseInt(args[0]) }); data = await queryTotal(res[0], query, score); if (config.toPhoto) { data = koishi_1.h.image(toPng(data)); } await session.send(data); } catch (error) { session.send("查无此期数据"); } }); /** 自动数据获取 */ async function autoCrawl() { while (dispo) { let nextTime = await getData(); console.log("距离下次Crawl还有:" + nextTime / 3600000 + "小时。"); await new Promise(resolve => setTimeout(resolve, nextTime)); } } /** 获取总力分数记录数据*/ async function getData() { let N = 47; //arona只保留了47期(TM)往后的数据 let time; while (true) { if (fs_1.default.existsSync(path_1.default.join(totalPath, `Total${N}Data.json`))) { N++; continue; } else { let apiUrl = `https://blue.triple-lab.com/raid/${N}`; let res = await myXCrawl.crawlData({ targets: [apiUrl] }); let resultData = res[0].data; if (!res[0].isSuccess || res[0].data.statusCode === 404) { N--; apiUrl = `https://blue.triple-lab.com/raid/${N}`; res = await myXCrawl.crawlData({ targets: [apiUrl] }); resultData = res[0].data; try { time = 1800000; let data = await fs_1.default.promises.readFile(path_1.default.join(totalPath, `Total${N}Data.json`), 'utf8'); let localData = JSON.parse(data); if (localData.data.e[0] !== resultData.data.e[0]) saveData(resultData, N); } catch (err) { console.error(err); } return time; } console.log("data from:" + apiUrl); saveData(resultData, N); N++; } } } /** 获取总力记录信息*/ async function getRecord(ctx) { let res = await myXCrawl.crawlHTML(['https://ba.gamekee.com/584839.html']).then((res) => { return res[0].data.html; }); let tbody = /<tbody[\s\S]*<\/tbody>/.exec(res)[0]; //获取<tbody >标签 let trreg = tbody.match(/<tr[\s\S]*?<\/tr>/g).reverse(); //获取<tr >标签 trreg.forEach((tr, index) => { if (trreg.length - 1 === index) return; let div = tr.match(/<div(?:(?!<span)[\s\S])*?<\/div>/g).map(divs => /<div(?:[^<]*?)>([\s\S]*?)<\/div>/.exec(divs)[1]); //获取排除拥有<span>标签的<div标签> if (div.length !== 4) ctx.database.upsert('ba_jp_total', [{ season: index - 1, boss_name: div[0], boss_type: div[1], time: div[2] }]); else ctx.database.upsert('ba_jp_total', [{ season: index - 1, boss_name: div[1], boss_type: div[2], time: div[3] }]); }); } /** 数据保存 * 参数: * resultData:待保存json数据 * N:计数器,记录期数 */ async function saveData(resultData, N) { renameKeys(resultData.data); try { fs_1.default.writeFileSync(path_1.default.join(totalPath, `Total${N}Data.json`), JSON.stringify(resultData, null, 2)); } catch (err) { console.error('保存文件时出错:', err); } } /** 修改键名 * data:数据源 */ function renameKeys(data) { const keyMap = { a: 'army', b: 'benchmark', t: 'top', l: 'liberator', r: 'rank', s: 'silver', g: 'gold', p: 'platinum' }; for (const oldKey in keyMap) { const newKey = keyMap[oldKey]; data[newKey] = data[oldKey]; delete data[oldKey]; } data.army.forEach(a => { a.score = a.s; delete a.s; a.team = a.t; delete a.t; a.team.forEach(t => { t.striker = t.m; delete t.m; t.special = t.s; delete t.s; }); }); } /** 数据查询 * 参数列表: * raid: 查询期数信息 * query: 查询类型判断 * socre:待查询信息(可选) */ async function queryTotal(raid, query, score = 0) { try { let jsonData = fs_1.default.readFileSync(path_1.default.join(totalPath, `Total${raid.season}Data.json`), 'utf-8'); let data = await JSON.parse(jsonData).data; const lastTime = new Date(data.e[0] + data.e[2]); const season = `第${raid.season}期总力统计\nboss:${raid.boss_name}\n类型:${raid.boss_type}\n`; const lastUP = "数据来源:https://arona.ai/graph\n最后更新时间:" + lastTime.toLocaleString().trim(); switch (query) { case 1: return season + queryBenchmark(data.benchmark) + lastUP; case 2: return season + queryLiberator(data.liberator) + lastUP; case 3: return season + queryTop(data.top) + lastUP; case 4: return season + queryRank(data.rank, score, true) + lastUP; case 5: return season + queryRank(data.rank, score, false) + lastUP; } } catch (error) { return `第${raid.season}期总力战\nboss:${raid.boss_name}\n类型:${raid.boss_type}\n暂未统计`; } } /** 分数线查询 * 参数列表: * data:数据源 */ function queryBenchmark(data) { let resultString = ''; for (const key in data) { if (Object.hasOwnProperty.call(data, key)) { const value = data[key]; resultString += `第${key}名的分数:${value}\n`; } } return resultString; } /** 通关人数查询 * 参数列表: * data:数据源 */ function queryLiberator(data) { let resultString = ""; let liberator = data[data.length - 1]; liberator[1].map((score, index) => { switch (index) { case 0: score = 'Normal通关人数:' + score + '\n'; break; case 1: score = 'Hard通关人数:' + score + '\n'; break; case 2: score = 'VeryHard通关人数:' + score + '\n'; break; case 3: score = 'HardCore通关人数:' + score + '\n'; break; case 4: score = 'Extreme通关人数:' + score + '\n'; break; case 5: score = 'Insane通关人数:' + score + '\n'; break; case 6: score = 'Torment通关人数:' + score + '\n'; break; } resultString += score; }); return `通关人数\n` + resultString; } /** 最高分查询 * 参数列表: * data:数据源 */ function queryTop(data) { let resultString = ''; data.map((score, index) => { if (score === -1) { score = "暂未统计"; } switch (index) { case 0: score = 'Normal最高分:' + score + '\n'; break; case 1: score = 'Hard最高分:' + score + '\n'; break; case 2: score = 'VeryHard最高分:' + score + '\n'; break; case 3: score = 'HardCore最高分:' + score + '\n'; break; case 4: score = 'Extreme最高分:' + score + '\n'; break; case 5: score = 'Insane最高分:' + score + '\n'; break; case 6: score = 'Torment最高分:' + score + '\n'; break; } resultString += score; }); return '难度最高分\n' + resultString; } /** 排名或分数查询 * 参数列表: * data:数据源 * score: 待查询分数或排名 * rank: 查询类型判断 */ function queryRank(data, score, rank) { if (rank) { let index = data.findIndex(([value]) => value <= score); let res = index !== -1 ? data.slice(0, index + 1) : data; let sum = res.reduce((total, [, value]) => total + value, 0); return `分数:${score}\n排名:${sum}名\n`; } else { let sum = 0; for (let i = 0; i < data.length; i++) { sum += data[i][1]; if (sum >= score) return `分数:${data[i][0]}\n排名:${score}名\n`; } return '查询排名超过2档\n'; } } //图片渲染 function toPng(data) { let lines = data.split('\n'); //行分割 const fontSize = 20; const lineHeight = 1.2; const height = (lines.length + 1) * fontSize * lineHeight; let lineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, line.length), 0) * 0.75; //最长行 const canvas = ctx.canvas.createCanvas(lineWidth * fontSize, height); const context = canvas.getContext("2d"); context.fillStyle = '#ffffff'; context.fillRect(0, 0, canvas.width, canvas.height); context.font = `${fontSize}px "Microsoft YaHei", sans-serif`; context.fillStyle = "#000000"; for (let i = 0; i < lines.length; i++) { context.fillText(lines[i], fontSize, fontSize * lineHeight * (i + 1)); } return canvas.toDataURL(); } } exports.apply = apply;