diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index dd5afc0..9627227 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -299,6 +299,7 @@ export interface TsuguHook { - 已新增 `cache-client-policy.spec.ts`,覆盖缓存客户端 retry/status/not-found 策略;`file-cache-client.spec.ts` 补 SVG 缺失资源 404 缓存,避免 SVG 素材缺失仍反复请求。 - 本地图片烟测已生成查歌 `136`、查活动 `50` 简易背景和真实活动背景图片,证明 provider/repository 第一段迁移后仍能输出非空图片。 - 已新增 `models/song-resource-repository.ts`,把 `Song` 的歌曲详情、谱面、封面路径、封面完整 URL 和封面缺失时服务器回退下载收口到不依赖 `Song` 类的资源 repository,避免 `song.ts` 继续直接拼 Bestdori API/asset 路径;`song-resource-repository.spec.ts` 覆盖 detail/chart provider 调用、13/40 旧封面批次、273 强制 CN 和封面 fallback 行为,本地 `/查曲 136`、`/查谱面 136 expert` 图片 smoke 输出非空。 +- 收尾批量把 `Song.getSongChart`、`SongResourceRepository.getChart` 和 `drawBestdoriPreview` 的谱面协议改为 `BestdoriNote[]`,移除 `drawSongChart` 的 `as any`;`EventStage.getData` 透传 `EventStageDataRows`,`stageType`/`rotationMusics` 改为 repository 行类型并用一次 `Promise.all` 加载;`readExcelFile` 改为泛型 `Record` 输出;`asset-cache-client` 和 `server.ts` 清理参数命名与 `Array` 残留,本地 `/查谱面 136 expert`、`/查试炼 310` smoke 输出非空。 - 已新增 `models/card-resource-repository.ts`,把 `Card` 的详情请求、资源批次目录、图标/插画/trim 图片路径和重图缓存策略收口到 provider-backed repository;`card-resource-repository.spec.ts` 覆盖 `/api/cards` 缓存参数、`9999` 后资源批次、CN 优先资源路径和非 icon 图片 `memoryCache=false`,本地 `/查卡 472` 图片 smoke 输出非空。 - 已新增 `models/gacha-resource-repository.ts`,把 `Gacha` 的详情请求、首页横幅、screen 背景、`bg1` fallback 和 Logo 资源路径收口到 provider-backed repository;`gacha-resource-repository.spec.ts` 覆盖 `/api/gacha` 缓存参数、CN 优先 screen 路径、横幅缺失回退 Logo 和背景 fallback,本地 `/查卡池 259`、`/抽卡模拟 10 259` 图片 smoke 输出非空。 - 已新增 `models/event-stage-data-repository.ts`,把 `EventStage` 的 festival stages/rotationMusics 数据请求从 `bestdoriUrl + callAPIAndCacheResponse` 收口到 provider-backed repository;`event-stage-data-repository.spec.ts` 覆盖两个 festival API 路径和缓存参数,本地 `/查试炼 310` 保持拆成 5 张图片输出。 @@ -399,6 +400,7 @@ export interface TsuguHook { - 已新增 `render-blocks/list-rarity-spec.ts`,收口星级列表的文字大小、间距和特训星图使用阈值;`list-rarity.ts` 改为消费 spec,星图资源加载、星级数量和列表绘制顺序保持不变;`list-rarity-spec.spec.ts` 覆盖列表规格和 3/4/5 星及未特训边界,本地 `/查卡 472` 图片 smoke 输出非空。 - 已批量新增 `render-blocks/skill-text-spec.ts`、`list-entity-spec.ts`、`degree-list-spec.ts`、`data-block-spec.ts`、`list-player-ranking-spec.ts`、`list-time-spec.ts`、`timeline-chart-spec.ts`、`cutoff-chart-spec.ts`、`gacha-list-spec.ts`,收口技能角标、基础实体列表、称号/活动奖励称号、数据块、玩家排名行、时间格式、时间线/档线图表和卡池列表文案/布局规格;`skill-text.ts`、`list-attribute.ts`、`list-band.ts`、`list-character.ts`、`degree-badge.ts`、`list-degree-list.ts`、`data-block.ts`、`list-player-ranking.ts`、`list-time.ts`、`timeline-chart.ts`、`cutoff-chart.ts`、`list-gacha-payment-method.ts`、`list-gacha-pick-up.ts`、`list-gacha-rate.ts` 已改为消费 spec;同步修复 `list-band.ts` 多乐队分支误用 `this.hasIcon` 导致图标判断失效的问题;新增对应 8 组 spec 单测,本地 `/查卡 472`、`/查卡池 259`、`/查活动 50`、`/ycx 100 50 cn`、`/查谱面 136 expert` 图片 smoke 输出非空。 - 已补充 `song-chart-preview-spec.ts` 的封面信息块偏移、ID/难度面板、轨道渐变和双押线高度规格,`song-chart-preview.ts` 不再在绘制函数中散落这些视觉常量;`song-chart-preview-spec.spec.ts` 覆盖新增规格,本地 `/查谱面 136 expert` 图片 smoke 输出非空。 +- 收尾批量把谱面预览的绘图上下文改为 `CanvasRenderingContext2D`,`timeline-chart-spec.ts` 定义 `TimelineChartPoint/TimelineChartDataset`,`Cutoff.getChartData` 和 `CutoffEventTop.getChartData` 统一输出毫秒时间戳而不是 `Date` 对象;`drawTimeLineChart` 改用 typed `ChartConfiguration`,只在 skia-canvas 与 Chart.js 的 `ChartItem` 兼容边界保留显式 `unknown` 转换;同步修正 `labelName`、`cutoffEventTop`、`accuracy`、`constellationTextImage` 等拼写残留。本地 BangDream Tsugu 55 个 Jest suite / 186 个测试、`pnpm run typecheck`、scoped ESLint、`pnpm run build` 通过,`/查谱面 136 expert`、`/ycx 100 50 cn`、`/lsycx 100 50 cn`、`/查试炼 310` 图片 smoke 输出非空。 - smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录;PowerShell 双引号 here-string 中不要写 JS `${...}` 模板字面量,避免被 PowerShell 提前插值;smoke 没有真实图片落盘时必须失败。 ### Phase 6:策略 policy 和时间/档线规则 diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/character-detail.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/character-detail.ts index 91ec55f..c2f4c5f 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/character-detail.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/character-detail.ts @@ -168,11 +168,11 @@ export async function drawCharacterDetail( key: '生日', }); //星座 - const constellationTextImafe = drawList({ + const constellationTextImage = drawList({ text: constellationList[character.profile.constellation], key: '星座', }); - list.push(drawListMerge([birthdayTextImage, constellationTextImafe])); + list.push(drawListMerge([birthdayTextImage, constellationTextImage])); list.push(line); //身高 const heightTextImage = drawList({ diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-chart.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-chart.ts index 8592a56..089729b 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-chart.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-chart.ts @@ -46,7 +46,7 @@ export async function drawSongChart( diff: difficultyName[difficultyId], cover: song.getSongJacketImageURL(displayedServerList), }, - songChart as any, + songChart, ); let buffer: Buffer; diff --git a/src/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client.ts b/src/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client.ts index 080b925..fbc1ebb 100644 --- a/src/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client.ts +++ b/src/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client.ts @@ -22,14 +22,14 @@ const memoryCache: { [url: string]: Buffer } = {}; * 在数据下载与缓存层中下载File。 * * @param url - 远端资源地址。 - * @param IgnoreErr - IgnoreErr参数,未传入时使用默认值。 + * @param ignoreError - ignoreError参数,未传入时使用默认值。 * @param overwrite - overwrite参数,未传入时使用默认值。 * @param retryCount - retryCount参数,未传入时使用默认值。 * @returns 异步处理结果。 */ async function downloadFile( url: string, - IgnoreErr: boolean = true, + ignoreError: boolean = true, overwrite = false, retryCount = 3, ): Promise { @@ -84,7 +84,7 @@ async function downloadFile( errUrl[url] = Date.now(); } - if ((url.includes('.png') || url.includes('.svg')) && IgnoreErr) { + if ((url.includes('.png') || url.includes('.svg')) && ignoreError) { return assetErrorImageBuffer; } @@ -96,17 +96,17 @@ async function downloadFile( * 在数据下载与缓存层中下载File缓存。 * * @param url - 远端资源地址。 - * @param IgnoreErr - IgnoreErr参数,未传入时使用默认值。 + * @param ignoreError - ignoreError参数,未传入时使用默认值。 * @returns 异步处理结果。 */ async function downloadFileCache( url: string, - IgnoreErr = true, + ignoreError = true, ): Promise { if (memoryCache[url]) { return memoryCache[url]; } - const data = await downloadFile(url, IgnoreErr); + const data = await downloadFile(url, ignoreError); if (process.env.MEMORY_CACHE == 'true') { memoryCache[url] = data; } diff --git a/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts index 21f8bad..76f20fb 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts @@ -87,38 +87,38 @@ export class CutoffEventTop { * @returns 计算后的数值。 */ getChartData(setStartToZero = false): { - [key: number]: { x: Date; y: number }[]; + [key: number]: { x: number; y: number }[]; } { if (this.isExist == false) { return; } - const chartDate: { [key: number]: { x: Date; y: number }[] } = {}; + const chartDate: { [key: number]: { x: number; y: number }[] } = {}; for (let i = 0; i < this.points.length; i++) { const element = this.points[i]; if (!(element.uid in chartDate)) { chartDate[element.uid] = []; if (setStartToZero) { - chartDate[element.uid].push({ x: new Date(0), y: 0 }); + chartDate[element.uid].push({ x: 0, y: 0 }); chartDate[element.uid].push({ - x: new Date(element.time - this.startAt), + x: element.time - this.startAt, y: element.value, }); } else { - chartDate[element.uid].push({ x: new Date(this.startAt), y: 0 }); + chartDate[element.uid].push({ x: this.startAt, y: 0 }); chartDate[element.uid].push({ - x: new Date(element.time), + x: element.time, y: element.value, }); } } else { if (setStartToZero) { chartDate[element.uid].push({ - x: new Date(element.time - this.startAt), + x: element.time - this.startAt, y: element.value, }); } else { chartDate[element.uid].push({ - x: new Date(element.time), + x: element.time, y: element.value, }); } diff --git a/src/qqbot/plugins/bangDream/tsugu/models/cutoff.ts b/src/qqbot/plugins/bangDream/tsugu/models/cutoff.ts index d3a62ca..545118e 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/cutoff.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/cutoff.ts @@ -634,15 +634,15 @@ export class Cutoff { * @param setStartToZero - setStartToZero参数,未传入时使用默认值。 * @returns 计算后的数值。 */ - getChartData(setStartToZero = false): { x: Date; y: number }[] { + getChartData(setStartToZero = false): { x: number; y: number }[] { if (this.isExist == false) { return []; } - const chartData: { x: Date; y: number }[] = []; + const chartData: { x: number; y: number }[] = []; if (setStartToZero) { - chartData.push({ x: new Date(0), y: 0 }); + chartData.push({ x: 0, y: 0 }); } else { - chartData.push({ x: new Date(this.startAt), y: 0 }); + chartData.push({ x: this.startAt, y: 0 }); } // 在访问 this.cutoffs[0].time 之前检查 this.cutoffs 是否存在且长度大于0 @@ -655,11 +655,11 @@ export class Cutoff { if (setStartToZero) { // 确保 tempTime 不为 null 才执行减法操作 chartData.push({ - x: tempTime ? new Date(element.time - this.startAt) : new Date(0), + x: tempTime ? element.time - this.startAt : 0, y: element.ep, }); } else { - chartData.push({ x: new Date(element.time), y: element.ep }); + chartData.push({ x: element.time, y: element.ep }); } tempTime = element.time; } diff --git a/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts b/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts index 647ad4d..da24fad 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts @@ -7,6 +7,9 @@ import { } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants'; import { eventStageDataRepository, + type EventStageDataRows, + type EventStageRotationMusicRow, + type EventStageTypeRow, type EventStageDataType, } from '@/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository'; @@ -28,8 +31,8 @@ export class EventStage { eventId: number; isExist: boolean = false; isInitFull = false; - stageType: Array<{ type: string; startAt: string; endAt: string }>; - rotationMusics: Array<{ musicId: string; startAt: string; endAt: string }>; + stageType: EventStageTypeRow[] = []; + rotationMusics: EventStageRotationMusicRow[] = []; /** * 构造 EventStage 实例,并初始化该模型的本地基础字段。 * @@ -59,10 +62,12 @@ export class EventStage { return; } try { - const stageData = await this.getData(true, 'stages'); - const rotationMusicsData = await this.getData(true, 'rotationMusics'); - this.stageType = stageData as any; - this.rotationMusics = rotationMusicsData as any; + const [stageData, rotationMusicsData] = await Promise.all([ + this.getData(true, 'stages'), + this.getData(true, 'rotationMusics'), + ]); + this.stageType = stageData; + this.rotationMusics = rotationMusicsData; this.isInitFull = true; } catch { this.isExist = false; @@ -74,7 +79,10 @@ export class EventStage { * @param update - update参数,未传入时使用默认值。 * @param type - 数据类型或匹配类型。 */ - async getData(update: boolean = true, type: EventStageDataType) { + async getData( + update: boolean = true, + type: T, + ): Promise> { return await eventStageDataRepository.getFestivalData( this.eventId, type, @@ -88,25 +96,26 @@ export class EventStage { * @returns 处理后的列表。 */ getStageList(): Stage[] { - //获取所有的stage,并且按照时间排序 [{type,startAt,endAt,songIdList}] if (!this.isInitFull) { - return; + return []; } - const temp = {}; - for (const i in this.rotationMusics) { - const tempStartAt = this.rotationMusics[i].startAt; - if (temp[tempStartAt] == undefined) { - temp[tempStartAt] = { - startAt: this.rotationMusics[i].startAt, - endAt: this.rotationMusics[i].endAt, + const groupedByStartAt: Record< + string, + { startAt: string; endAt: string; music: number[] } + > = {}; + for (const rotationMusic of this.rotationMusics) { + const tempStartAt = rotationMusic.startAt; + if (groupedByStartAt[tempStartAt] == undefined) { + groupedByStartAt[tempStartAt] = { + startAt: rotationMusic.startAt, + endAt: rotationMusic.endAt, music: [], }; } - temp[tempStartAt].music.push(this.rotationMusics[i].musicId); + groupedByStartAt[tempStartAt].music.push(Number(rotationMusic.musicId)); } const tempStageList: Stage[] = []; - for (const i in temp) { - const element = temp[i]; + for (const element of Object.values(groupedByStartAt)) { const tempStartAt = parseInt(element.startAt); const tempEndAt = parseInt(element.endAt); const tempStageType = this.getStageTypeByTime(tempStartAt, tempEndAt); @@ -114,10 +123,9 @@ export class EventStage { type: tempStageType, startAt: tempStartAt, endAt: tempEndAt, - songIdList: temp[i].music, + songIdList: element.music, }); } - //排序 tempStageList.sort((a, b) => { return a.startAt - b.startAt; }); @@ -133,9 +141,8 @@ export class EventStage { * @returns 格式化后的文本。 */ getStageTypeByTime(startAt: number, endAt: number): string { - //根据时间获取当前stage类型 if (!this.isInitFull) { - return; + return BangDreamEventStageType.unknown; } const stage = this.stageType.find((x) => { const startTime = parseInt(x.startAt); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/model-utils.ts b/src/qqbot/plugins/bangDream/tsugu/models/model-utils.ts index a003530..d6b22f8 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/model-utils.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/model-utils.ts @@ -48,20 +48,14 @@ export async function writeJSON(filepath: string, data: object) { * @param filePath - 本地文件路径。 * @returns 异步处理结果。 */ -export async function readExcelFile(filePath: string): Promise { - // 读取Excel文件 +export async function readExcelFile< + T extends Record = Record, +>(filePath: string): Promise { const workbook = XLSX.readFile(filePath); - // 获取工作表的名字 const sheetName = workbook.SheetNames[0]; - - // 获取工作表 const worksheet = workbook.Sheets[sheetName]; - - // 将工作表转换为JSON - const json = XLSX.utils.sheet_to_json(worksheet); - - return json; + return XLSX.utils.sheet_to_json(worksheet); } //将string[]转变为number[] diff --git a/src/qqbot/plugins/bangDream/tsugu/models/server.ts b/src/qqbot/plugins/bangDream/tsugu/models/server.ts index d8ce2cb..b1717c8 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/server.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/server.ts @@ -93,7 +93,7 @@ export async function getIcon(server: Server): Promise { * @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。 */ export function getServerByPriority( - content: Array, + content: Array, displayedServerList: Server[] = globalDefaultServer, ) { const serverPriority: Server[] = [ @@ -114,13 +114,8 @@ export function getServerByPriority( * @param server - 目标服务器。 * @returns 判断结果。 */ -export function isServer(server: any): boolean { - if (typeof server == 'number') { - server = Server[server]; - } else { - return false; - } - return Object.keys(Server).includes(server); +export function isServer(server: unknown): boolean { + return typeof server === 'number' && serverList.includes(server); } /** @@ -129,7 +124,7 @@ export function isServer(server: any): boolean { * @param serverList - 服务器列表参数。 * @returns 判断结果。 */ -export function isServerList(serverList: Array): boolean { +export function isServerList(serverList: Array): boolean { let result = true; for (let i = 0; i < serverList.length; i++) { const element = serverList[i]; diff --git a/src/qqbot/plugins/bangDream/tsugu/models/song-resource-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/song-resource-repository.ts index 3ec6f47..05b50c4 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/song-resource-repository.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/song-resource-repository.ts @@ -9,6 +9,7 @@ import { getServerByPriority, Server, } from '@/qqbot/plugins/bangDream/tsugu/models/server'; +import type { BestdoriNote } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview-spec'; export interface SongJacketSource { jacketImage: Array; @@ -50,8 +51,8 @@ export class SongResourceRepository { * @param songId - 歌曲 ID。 * @param difficultyId - 难度 ID。 */ - async getChart(songId: number, difficultyId: number): Promise { - return await this.provider.getJson( + async getChart(songId: number, difficultyId: number): Promise { + return await this.provider.getJson( `/api/charts/${songId}/${difficultyNameById[difficultyId]}.json`, ); } diff --git a/src/qqbot/plugins/bangDream/tsugu/models/song.ts b/src/qqbot/plugins/bangDream/tsugu/models/song.ts index 5e079c7..d896c2f 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/song.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/song.ts @@ -9,6 +9,7 @@ import { BANGDREAM_SONG_TAG_NAME, } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants'; import { songResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/song-resource-repository'; +import type { BestdoriNote } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview-spec'; export const difficultyName: Record = BANGDREAM_DIFFICULTY_NAME_BY_ID; @@ -248,7 +249,7 @@ export class Song { * @param difficultyId - 难度ID参数。 * @returns 异步处理结果。 */ - async getSongChart(difficultyId: number): Promise { + async getSongChart(difficultyId: number): Promise { return await songResourceRepository.getChart(this.songId, difficultyId); } @@ -272,7 +273,7 @@ export class Song { * @param difficultyId - 难度ID参数。 * @param scoreUpMaxValue - 分数UpMax值参数,未传入时使用默认值。 * @param skillDuration - 技能Duration参数,未传入时使用默认值。 - * @param accruacy - accruacy参数,未传入时使用默认值。 + * @param accuracy - accuracy参数,未传入时使用默认值。 * @returns 计算后的数值。 */ calcMeta( @@ -280,7 +281,7 @@ export class Song { difficultyId: number, scoreUpMaxValue: number = 100, skillDuration: number = 7, - accruacy: number = 100, + accuracy: number = 100, ): number { if (this.hasMeta == false) { return 0; @@ -298,7 +299,7 @@ export class Song { this.meta[difficultyId][skillDuration][1]; } const scoreParameter = - skillParameter * ((1.1 * accruacy) / 100 + 0.8 * (1 - accruacy / 100)); + skillParameter * ((1.1 * accuracy) / 100 + 0.8 * (1 - accuracy / 100)); return scoreParameter; } } diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart.ts index e85c407..bb6dba3 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart.ts @@ -12,6 +12,10 @@ import { BANGDREAM_CUTOFF_CHART_SPEC, stripCutoffChartLabelTags, } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart-spec'; +import type { + TimelineChartDataset, + TimelineChartPoint, +} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec'; /** * 在图片布局层中绘制档线谱面。 @@ -26,7 +30,7 @@ export async function drawCutoffChart( server: Server = Server['jp'], ) { //setStartToZero:是否将开始时间设置为0 - const datasets = []; + const datasets: TimelineChartDataset[] = []; const time = new Date().getTime(); if (cutoffList.length == 0) { return new Canvas( @@ -44,9 +48,9 @@ export async function drawCutoffChart( const cutoff = cutoffList[i]; const tempEvent = new Event(cutoff.eventId); - let lableName: string; + let labelName: string; if (setStartToZero) { - lableName = `[${tempEvent.eventId}] ${tempEvent.eventName[server]} T${cutoff.tier}`; + labelName = `[${tempEvent.eventId}] ${tempEvent.eventName[server]} T${cutoff.tier}`; list.push( drawList({ content: [ @@ -59,7 +63,7 @@ export async function drawCutoffChart( }), ); } else { - lableName = `T${cutoff.tier}`; + labelName = `T${cutoff.tier}`; list.push( drawList({ content: [ @@ -73,7 +77,7 @@ export async function drawCutoffChart( ); } datasets.push({ - label: lableName, + label: labelName, data: cutoff.getChartData(setStartToZero), borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth, borderColor: [tempColor.getRGBA(1)], @@ -89,39 +93,39 @@ export async function drawCutoffChart( if (cutoff.status == BangDreamEventStatus.inProgress) { if (cutoff.predictEP != null && cutoff.predictEP != 0) { - let data = []; + let data: TimelineChartPoint[] = []; const history = cutoff.getPredictionHistory(); if (history.length > 0) { if (setStartToZero) { for (const p of history) { - data.push({ x: new Date(p.time - cutoff.startAt), y: p.ep }); + data.push({ x: p.time - cutoff.startAt, y: p.ep }); } data.push({ - x: new Date(cutoff.endAt - cutoff.startAt), + x: cutoff.endAt - cutoff.startAt, y: history[history.length - 1].ep, }); } else { for (const p of history) { - data.push({ x: new Date(p.time), y: p.ep }); + data.push({ x: p.time, y: p.ep }); } data.push({ - x: new Date(cutoff.endAt), + x: cutoff.endAt, y: history[history.length - 1].ep, }); } } else { if (setStartToZero) { data = [ - { x: new Date(0), y: cutoff.predictEP }, + { x: 0, y: cutoff.predictEP }, { - x: new Date(cutoff.endAt - cutoff.startAt), + x: cutoff.endAt - cutoff.startAt, y: cutoff.predictEP, }, ]; } else { data = [ - { x: new Date(cutoff.startAt), y: cutoff.predictEP }, - { x: new Date(cutoff.endAt), y: cutoff.predictEP }, + { x: cutoff.startAt, y: cutoff.predictEP }, + { x: cutoff.endAt, y: cutoff.predictEP }, ]; } } @@ -140,7 +144,9 @@ export async function drawCutoffChart( ], data: data, borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth, - borderDash: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionDash, + borderDash: [ + ...BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionDash, + ], fill: false, pointRadius: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionPointRadius, @@ -157,7 +163,7 @@ export async function drawCutoffChart( label: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.label, borderColor: [tempColor.getRGBA(1)], backgroundColor: [tempColor.getRGBA(1)], - data: [{ x: new Date(time), y: 0 }], + data: [{ x: time, y: 0 }], fill: false, pointRadius: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.pointRadius, pointHoverRadius: @@ -204,27 +210,27 @@ export async function drawCutoffChart( /** * 在图片布局层中绘制档线活动排名谱面。 * - * @param CutoffEventTop - 档线活动排名参数。 + * @param cutoffEventTop - 档线活动排名参数。 * @param setStartToZero - setStartToZero参数,未传入时使用默认值。 */ export async function drawCutoffEventTopChart( - CutoffEventTop: CutoffEventTop, + cutoffEventTop: CutoffEventTop, setStartToZero = false, ) { - const datasets = []; - if (CutoffEventTop == undefined) { + const datasets: TimelineChartDataset[] = []; + if (cutoffEventTop == undefined) { return new Canvas( BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.width, BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.height, ); } - const allData = CutoffEventTop.getChartData(); + const allData = cutoffEventTop.getChartData(); let colorNumber = 0; for (const key in allData) { const tempColor = getPresetColor(colorNumber); datasets.push({ label: stripCutoffChartLabelTags( - CutoffEventTop.getUserNameById(Number(key)), + cutoffEventTop.getUserNameById(Number(key)), ), data: allData[key], borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.eventTopDataset.borderWidth, @@ -249,8 +255,8 @@ export async function drawCutoffEventTopChart( return await drawTimeLineChart( { data, - start: new Date(CutoffEventTop.startAt), - end: new Date(CutoffEventTop.endAt), + start: new Date(cutoffEventTop.startAt), + end: new Date(cutoffEventTop.endAt), setStartToZero, }, true, diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts index 47d0b1f..4f43034 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts @@ -1,4 +1,5 @@ import { Canvas, Image, loadImage } from 'skia-canvas'; +import type { CanvasRenderingContext2D } from 'skia-canvas'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; import { BangDreamLocalAssetKey, @@ -96,7 +97,7 @@ async function loadCoverImage(cover: string | Buffer): Promise { * @param y - 纵向绘制坐标。 */ function setAdaptiveTextBaseline( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, fontSize: number, y: number, @@ -135,7 +136,7 @@ function getTimePosition(layout: PreviewLayout, time: number) { * @param coverImg - coverImg参数。 */ function drawBaseInfo( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, payload: BestdoriPreviewPayload, coverImg: Image, @@ -218,7 +219,7 @@ function drawBaseInfo( * @param ctx - 画布绘图上下文。 * @param layout - 布局参数。 */ -function drawTracks(ctx: any, layout: PreviewLayout): void { +function drawTracks(ctx: CanvasRenderingContext2D, layout: PreviewLayout): void { for (let i = 0; i < layout.colCount; i++) { ctx.save(); const x = @@ -259,7 +260,7 @@ function drawTracks(ctx: any, layout: PreviewLayout): void { * @param notes - 谱面音符列表。 */ function drawBeatLines( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, notes: PreviewNote[], ): void { @@ -309,7 +310,10 @@ function drawBeatLines( * @param ctx - 画布绘图上下文。 * @param layout - 布局参数。 */ -function drawTimeline(ctx: any, layout: PreviewLayout): void { +function drawTimeline( + ctx: CanvasRenderingContext2D, + layout: PreviewLayout, +): void { ctx.save(); ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`; ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText; @@ -330,7 +334,7 @@ function drawTimeline(ctx: any, layout: PreviewLayout): void { * @param notes - 谱面音符列表。 */ function drawCountAndBpmLines( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, notes: PreviewNote[], ): void { @@ -376,7 +380,7 @@ function drawCountAndBpmLines( * @param note - 谱面音符。 */ function drawTapNote( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, noteImages: Record, note: PreviewNote, @@ -417,7 +421,7 @@ function drawTapNote( * @param note - 谱面音符。 */ function drawDirectionalNote( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, noteImages: Record, note: PreviewNote, @@ -469,7 +473,7 @@ function drawDirectionalNote( * @param note - 谱面音符。 */ function drawSimNote( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, noteImages: Record, note: PreviewNote, @@ -498,7 +502,11 @@ function drawSimNote( * @param layout - 布局参数。 * @param note - 谱面音符。 */ -function drawBarNote(ctx: any, layout: PreviewLayout, note: PreviewNote): void { +function drawBarNote( + ctx: CanvasRenderingContext2D, + layout: PreviewLayout, + note: PreviewNote, +): void { const time = note.time as number[]; const lane = note.lane as number[]; const startCol = Math.floor(time[0] / layout.secondsPerCol); @@ -543,7 +551,7 @@ function drawBarNote(ctx: any, layout: PreviewLayout, note: PreviewNote): void { * @param notes - 谱面音符列表。 */ function drawNotes( - ctx: any, + ctx: CanvasRenderingContext2D, layout: PreviewLayout, noteImages: Record, notes: PreviewNote[], diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec.ts index 6568172..d0f3e83 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec.ts @@ -1,3 +1,5 @@ +import type { ChartDataset } from 'chart.js'; + export const BANGDREAM_TIMELINE_CHART_SPEC = { animation: false, canvas: { @@ -18,14 +20,19 @@ export const BANGDREAM_TIMELINE_CHART_SPEC = { }, } as const; +export interface TimelineChartPoint { + x: number; + y: number; +} + +export type TimelineChartDataset = ChartDataset<'line', TimelineChartPoint[]>; + /** * 计算时间线图表的原始 Y 轴最大值。 * * @param datasets - Chart.js 数据集。 */ -export function getTimelineRawYMax( - datasets: Array<{ data: Array<{ y: number }> }>, -) { +export function getTimelineRawYMax(datasets: TimelineChartDataset[]) { return Math.max( ...datasets.map((dataset) => Math.max(...dataset.data.map((pt) => pt.y))), ); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart.ts index b03e1c4..ec0f8cd 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart.ts @@ -1,4 +1,5 @@ import { Chart, registerables } from 'chart.js'; +import type { ChartConfiguration, ChartItem } from 'chart.js'; import { Canvas, FontLibrary, loadImage } from 'skia-canvas'; import 'chartjs-adapter-moment'; import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; @@ -7,6 +8,8 @@ import { BANGDREAM_TIMELINE_CHART_SPEC, getTimelineDisplayYMax, getTimelineRawYMax, + type TimelineChartDataset, + type TimelineChartPoint, } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec'; // 2. 注册 Chart.js 所有组件 @@ -19,12 +22,12 @@ Chart.register(...registerables); FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]); // 5. 定义参数接口 -interface drawTimeLineChartOptions { +interface DrawTimeLineChartOptions { start: Date; end: Date; setStartToZero?: boolean; data: { - datasets: any[]; + datasets: TimelineChartDataset[]; }; } @@ -36,7 +39,7 @@ interface drawTimeLineChartOptions { * @param displayLabel - 展示Label参数,未传入时使用默认值。 */ export async function drawTimeLineChart( - { start, end, setStartToZero = false, data }: drawTimeLineChartOptions, + { start, end, setStartToZero = false, data }: DrawTimeLineChartOptions, displayLabel = false, ) { const width = BANGDREAM_TIMELINE_CHART_SPEC.canvas.width; @@ -63,12 +66,12 @@ export async function drawTimeLineChart( }, scales: { x: { - type: 'time', + type: 'time' as const, time: { unit: BANGDREAM_TIMELINE_CHART_SPEC.xAxis.unit, }, - min: start, - max: end, + min: start.getTime(), + max: end.getTime(), display: !setStartToZero, }, y: { @@ -79,7 +82,7 @@ export async function drawTimeLineChart( }; // 10. Chart.js 配置 - const config = { + const config: ChartConfiguration<'line', TimelineChartPoint[]> = { type: 'line' as const, data, options: { @@ -91,7 +94,7 @@ export async function drawTimeLineChart( try { // 11. 生成 Chart.js 图表 - new Chart(ctx as any, config as any); + new Chart(ctx as unknown as ChartItem, config); // 12. 返回 skia-canvas 的 Image 对象 return canvas; diff --git a/test/qqbot/plugins/bangDream/tsugu/chart-spec.spec.ts b/test/qqbot/plugins/bangDream/tsugu/chart-spec.spec.ts index 94f73ce..dbccdd7 100644 --- a/test/qqbot/plugins/bangDream/tsugu/chart-spec.spec.ts +++ b/test/qqbot/plugins/bangDream/tsugu/chart-spec.spec.ts @@ -16,8 +16,8 @@ describe('BangDream chart specs', () => { }); expect( getTimelineRawYMax([ - { data: [{ y: 100 }, { y: 300 }] }, - { data: [{ y: 200 }] }, + { data: [{ x: 0, y: 100 }, { x: 1, y: 300 }] }, + { data: [{ x: 0, y: 200 }] }, ]), ).toBe(300); expect(getTimelineDisplayYMax(300)).toBeCloseTo(1430);