refactor: 收口 BangDream 协议类型链路
This commit is contained in:
parent
36c76ce959
commit
2c9a3e3e68
@ -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<T>`,`stageType`/`rotationMusics` 改为 repository 行类型并用一次 `Promise.all` 加载;`readExcelFile` 改为泛型 `Record<string, unknown>` 输出;`asset-cache-client` 和 `server.ts` 清理参数命名与 `Array<any>` 残留,本地 `/查谱面 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 和时间/档线规则
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -46,7 +46,7 @@ export async function drawSongChart(
|
||||
diff: difficultyName[difficultyId],
|
||||
cover: song.getSongJacketImageURL(displayedServerList),
|
||||
},
|
||||
songChart as any,
|
||||
songChart,
|
||||
);
|
||||
|
||||
let buffer: Buffer;
|
||||
|
||||
@ -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<Buffer> {
|
||||
@ -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<Buffer> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<T extends EventStageDataType>(
|
||||
update: boolean = true,
|
||||
type: T,
|
||||
): Promise<EventStageDataRows<T>> {
|
||||
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);
|
||||
|
||||
@ -48,20 +48,14 @@ export async function writeJSON(filepath: string, data: object) {
|
||||
* @param filePath - 本地文件路径。
|
||||
* @returns 异步处理结果。
|
||||
*/
|
||||
export async function readExcelFile(filePath: string): Promise<any[]> {
|
||||
// 读取Excel文件
|
||||
export async function readExcelFile<
|
||||
T extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(filePath: string): Promise<T[]> {
|
||||
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<T>(worksheet);
|
||||
}
|
||||
|
||||
//将string[]转变为number[]
|
||||
|
||||
@ -93,7 +93,7 @@ export async function getIcon(server: Server): Promise<Image> {
|
||||
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
|
||||
*/
|
||||
export function getServerByPriority(
|
||||
content: Array<any>,
|
||||
content: Array<unknown>,
|
||||
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<any>): boolean {
|
||||
export function isServerList(serverList: Array<unknown>): boolean {
|
||||
let result = true;
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
const element = serverList[i];
|
||||
|
||||
@ -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<string>;
|
||||
@ -50,8 +51,8 @@ export class SongResourceRepository {
|
||||
* @param songId - 歌曲 ID。
|
||||
* @param difficultyId - 难度 ID。
|
||||
*/
|
||||
async getChart(songId: number, difficultyId: number): Promise<object> {
|
||||
return await this.provider.getJson<object>(
|
||||
async getChart(songId: number, difficultyId: number): Promise<BestdoriNote[]> {
|
||||
return await this.provider.getJson<BestdoriNote[]>(
|
||||
`/api/charts/${songId}/${difficultyNameById[difficultyId]}.json`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<number, string> =
|
||||
BANGDREAM_DIFFICULTY_NAME_BY_ID;
|
||||
@ -248,7 +249,7 @@ export class Song {
|
||||
* @param difficultyId - 难度ID参数。
|
||||
* @returns 异步处理结果。
|
||||
*/
|
||||
async getSongChart(difficultyId: number): Promise<object> {
|
||||
async getSongChart(difficultyId: number): Promise<BestdoriNote[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Image> {
|
||||
* @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<string, Image>,
|
||||
note: PreviewNote,
|
||||
@ -417,7 +421,7 @@ function drawTapNote(
|
||||
* @param note - 谱面音符。
|
||||
*/
|
||||
function drawDirectionalNote(
|
||||
ctx: any,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
layout: PreviewLayout,
|
||||
noteImages: Record<string, Image>,
|
||||
note: PreviewNote,
|
||||
@ -469,7 +473,7 @@ function drawDirectionalNote(
|
||||
* @param note - 谱面音符。
|
||||
*/
|
||||
function drawSimNote(
|
||||
ctx: any,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
layout: PreviewLayout,
|
||||
noteImages: Record<string, Image>,
|
||||
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<string, Image>,
|
||||
notes: PreviewNote[],
|
||||
|
||||
@ -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))),
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user