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 素材缺失仍反复请求。
|
- 已新增 `cache-client-policy.spec.ts`,覆盖缓存客户端 retry/status/not-found 策略;`file-cache-client.spec.ts` 补 SVG 缺失资源 404 缓存,避免 SVG 素材缺失仍反复请求。
|
||||||
- 本地图片烟测已生成查歌 `136`、查活动 `50` 简易背景和真实活动背景图片,证明 provider/repository 第一段迁移后仍能输出非空图片。
|
- 本地图片烟测已生成查歌 `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 输出非空。
|
- 已新增 `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/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/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 张图片输出。
|
- 已新增 `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/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 输出非空。
|
- 已批量新增 `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 输出非空。
|
- 已补充 `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 没有真实图片落盘时必须失败。
|
- smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录;PowerShell 双引号 here-string 中不要写 JS `${...}` 模板字面量,避免被 PowerShell 提前插值;smoke 没有真实图片落盘时必须失败。
|
||||||
|
|
||||||
### Phase 6:策略 policy 和时间/档线规则
|
### Phase 6:策略 policy 和时间/档线规则
|
||||||
|
|||||||
@ -168,11 +168,11 @@ export async function drawCharacterDetail(
|
|||||||
key: '生日',
|
key: '生日',
|
||||||
});
|
});
|
||||||
//星座
|
//星座
|
||||||
const constellationTextImafe = drawList({
|
const constellationTextImage = drawList({
|
||||||
text: constellationList[character.profile.constellation],
|
text: constellationList[character.profile.constellation],
|
||||||
key: '星座',
|
key: '星座',
|
||||||
});
|
});
|
||||||
list.push(drawListMerge([birthdayTextImage, constellationTextImafe]));
|
list.push(drawListMerge([birthdayTextImage, constellationTextImage]));
|
||||||
list.push(line);
|
list.push(line);
|
||||||
//身高
|
//身高
|
||||||
const heightTextImage = drawList({
|
const heightTextImage = drawList({
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export async function drawSongChart(
|
|||||||
diff: difficultyName[difficultyId],
|
diff: difficultyName[difficultyId],
|
||||||
cover: song.getSongJacketImageURL(displayedServerList),
|
cover: song.getSongJacketImageURL(displayedServerList),
|
||||||
},
|
},
|
||||||
songChart as any,
|
songChart,
|
||||||
);
|
);
|
||||||
|
|
||||||
let buffer: Buffer;
|
let buffer: Buffer;
|
||||||
|
|||||||
@ -22,14 +22,14 @@ const memoryCache: { [url: string]: Buffer } = {};
|
|||||||
* 在数据下载与缓存层中下载File。
|
* 在数据下载与缓存层中下载File。
|
||||||
*
|
*
|
||||||
* @param url - 远端资源地址。
|
* @param url - 远端资源地址。
|
||||||
* @param IgnoreErr - IgnoreErr参数,未传入时使用默认值。
|
* @param ignoreError - ignoreError参数,未传入时使用默认值。
|
||||||
* @param overwrite - overwrite参数,未传入时使用默认值。
|
* @param overwrite - overwrite参数,未传入时使用默认值。
|
||||||
* @param retryCount - retryCount参数,未传入时使用默认值。
|
* @param retryCount - retryCount参数,未传入时使用默认值。
|
||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async function downloadFile(
|
async function downloadFile(
|
||||||
url: string,
|
url: string,
|
||||||
IgnoreErr: boolean = true,
|
ignoreError: boolean = true,
|
||||||
overwrite = false,
|
overwrite = false,
|
||||||
retryCount = 3,
|
retryCount = 3,
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
@ -84,7 +84,7 @@ async function downloadFile(
|
|||||||
errUrl[url] = Date.now();
|
errUrl[url] = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((url.includes('.png') || url.includes('.svg')) && IgnoreErr) {
|
if ((url.includes('.png') || url.includes('.svg')) && ignoreError) {
|
||||||
return assetErrorImageBuffer;
|
return assetErrorImageBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,17 +96,17 @@ async function downloadFile(
|
|||||||
* 在数据下载与缓存层中下载File缓存。
|
* 在数据下载与缓存层中下载File缓存。
|
||||||
*
|
*
|
||||||
* @param url - 远端资源地址。
|
* @param url - 远端资源地址。
|
||||||
* @param IgnoreErr - IgnoreErr参数,未传入时使用默认值。
|
* @param ignoreError - ignoreError参数,未传入时使用默认值。
|
||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async function downloadFileCache(
|
async function downloadFileCache(
|
||||||
url: string,
|
url: string,
|
||||||
IgnoreErr = true,
|
ignoreError = true,
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
if (memoryCache[url]) {
|
if (memoryCache[url]) {
|
||||||
return memoryCache[url];
|
return memoryCache[url];
|
||||||
}
|
}
|
||||||
const data = await downloadFile(url, IgnoreErr);
|
const data = await downloadFile(url, ignoreError);
|
||||||
if (process.env.MEMORY_CACHE == 'true') {
|
if (process.env.MEMORY_CACHE == 'true') {
|
||||||
memoryCache[url] = data;
|
memoryCache[url] = data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,38 +87,38 @@ export class CutoffEventTop {
|
|||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
getChartData(setStartToZero = false): {
|
getChartData(setStartToZero = false): {
|
||||||
[key: number]: { x: Date; y: number }[];
|
[key: number]: { x: number; y: number }[];
|
||||||
} {
|
} {
|
||||||
if (this.isExist == false) {
|
if (this.isExist == false) {
|
||||||
return;
|
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++) {
|
for (let i = 0; i < this.points.length; i++) {
|
||||||
const element = this.points[i];
|
const element = this.points[i];
|
||||||
if (!(element.uid in chartDate)) {
|
if (!(element.uid in chartDate)) {
|
||||||
chartDate[element.uid] = [];
|
chartDate[element.uid] = [];
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
chartDate[element.uid].push({ x: new Date(0), y: 0 });
|
chartDate[element.uid].push({ x: 0, y: 0 });
|
||||||
chartDate[element.uid].push({
|
chartDate[element.uid].push({
|
||||||
x: new Date(element.time - this.startAt),
|
x: element.time - this.startAt,
|
||||||
y: element.value,
|
y: element.value,
|
||||||
});
|
});
|
||||||
} else {
|
} 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({
|
chartDate[element.uid].push({
|
||||||
x: new Date(element.time),
|
x: element.time,
|
||||||
y: element.value,
|
y: element.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
chartDate[element.uid].push({
|
chartDate[element.uid].push({
|
||||||
x: new Date(element.time - this.startAt),
|
x: element.time - this.startAt,
|
||||||
y: element.value,
|
y: element.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
chartDate[element.uid].push({
|
chartDate[element.uid].push({
|
||||||
x: new Date(element.time),
|
x: element.time,
|
||||||
y: element.value,
|
y: element.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -634,15 +634,15 @@ export class Cutoff {
|
|||||||
* @param setStartToZero - setStartToZero参数,未传入时使用默认值。
|
* @param setStartToZero - setStartToZero参数,未传入时使用默认值。
|
||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
getChartData(setStartToZero = false): { x: Date; y: number }[] {
|
getChartData(setStartToZero = false): { x: number; y: number }[] {
|
||||||
if (this.isExist == false) {
|
if (this.isExist == false) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const chartData: { x: Date; y: number }[] = [];
|
const chartData: { x: number; y: number }[] = [];
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
chartData.push({ x: new Date(0), y: 0 });
|
chartData.push({ x: 0, y: 0 });
|
||||||
} else {
|
} else {
|
||||||
chartData.push({ x: new Date(this.startAt), y: 0 });
|
chartData.push({ x: this.startAt, y: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在访问 this.cutoffs[0].time 之前检查 this.cutoffs 是否存在且长度大于0
|
// 在访问 this.cutoffs[0].time 之前检查 this.cutoffs 是否存在且长度大于0
|
||||||
@ -655,11 +655,11 @@ export class Cutoff {
|
|||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
// 确保 tempTime 不为 null 才执行减法操作
|
// 确保 tempTime 不为 null 才执行减法操作
|
||||||
chartData.push({
|
chartData.push({
|
||||||
x: tempTime ? new Date(element.time - this.startAt) : new Date(0),
|
x: tempTime ? element.time - this.startAt : 0,
|
||||||
y: element.ep,
|
y: element.ep,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
chartData.push({ x: new Date(element.time), y: element.ep });
|
chartData.push({ x: element.time, y: element.ep });
|
||||||
}
|
}
|
||||||
tempTime = element.time;
|
tempTime = element.time;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import {
|
|||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||||
import {
|
import {
|
||||||
eventStageDataRepository,
|
eventStageDataRepository,
|
||||||
|
type EventStageDataRows,
|
||||||
|
type EventStageRotationMusicRow,
|
||||||
|
type EventStageTypeRow,
|
||||||
type EventStageDataType,
|
type EventStageDataType,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository';
|
} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository';
|
||||||
|
|
||||||
@ -28,8 +31,8 @@ export class EventStage {
|
|||||||
eventId: number;
|
eventId: number;
|
||||||
isExist: boolean = false;
|
isExist: boolean = false;
|
||||||
isInitFull = false;
|
isInitFull = false;
|
||||||
stageType: Array<{ type: string; startAt: string; endAt: string }>;
|
stageType: EventStageTypeRow[] = [];
|
||||||
rotationMusics: Array<{ musicId: string; startAt: string; endAt: string }>;
|
rotationMusics: EventStageRotationMusicRow[] = [];
|
||||||
/**
|
/**
|
||||||
* 构造 EventStage 实例,并初始化该模型的本地基础字段。
|
* 构造 EventStage 实例,并初始化该模型的本地基础字段。
|
||||||
*
|
*
|
||||||
@ -59,10 +62,12 @@ export class EventStage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const stageData = await this.getData(true, 'stages');
|
const [stageData, rotationMusicsData] = await Promise.all([
|
||||||
const rotationMusicsData = await this.getData(true, 'rotationMusics');
|
this.getData(true, 'stages'),
|
||||||
this.stageType = stageData as any;
|
this.getData(true, 'rotationMusics'),
|
||||||
this.rotationMusics = rotationMusicsData as any;
|
]);
|
||||||
|
this.stageType = stageData;
|
||||||
|
this.rotationMusics = rotationMusicsData;
|
||||||
this.isInitFull = true;
|
this.isInitFull = true;
|
||||||
} catch {
|
} catch {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
@ -74,7 +79,10 @@ export class EventStage {
|
|||||||
* @param update - update参数,未传入时使用默认值。
|
* @param update - update参数,未传入时使用默认值。
|
||||||
* @param type - 数据类型或匹配类型。
|
* @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(
|
return await eventStageDataRepository.getFestivalData(
|
||||||
this.eventId,
|
this.eventId,
|
||||||
type,
|
type,
|
||||||
@ -88,25 +96,26 @@ export class EventStage {
|
|||||||
* @returns 处理后的列表。
|
* @returns 处理后的列表。
|
||||||
*/
|
*/
|
||||||
getStageList(): Stage[] {
|
getStageList(): Stage[] {
|
||||||
//获取所有的stage,并且按照时间排序 [{type,startAt,endAt,songIdList}]
|
|
||||||
if (!this.isInitFull) {
|
if (!this.isInitFull) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
const temp = {};
|
const groupedByStartAt: Record<
|
||||||
for (const i in this.rotationMusics) {
|
string,
|
||||||
const tempStartAt = this.rotationMusics[i].startAt;
|
{ startAt: string; endAt: string; music: number[] }
|
||||||
if (temp[tempStartAt] == undefined) {
|
> = {};
|
||||||
temp[tempStartAt] = {
|
for (const rotationMusic of this.rotationMusics) {
|
||||||
startAt: this.rotationMusics[i].startAt,
|
const tempStartAt = rotationMusic.startAt;
|
||||||
endAt: this.rotationMusics[i].endAt,
|
if (groupedByStartAt[tempStartAt] == undefined) {
|
||||||
|
groupedByStartAt[tempStartAt] = {
|
||||||
|
startAt: rotationMusic.startAt,
|
||||||
|
endAt: rotationMusic.endAt,
|
||||||
music: [],
|
music: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
temp[tempStartAt].music.push(this.rotationMusics[i].musicId);
|
groupedByStartAt[tempStartAt].music.push(Number(rotationMusic.musicId));
|
||||||
}
|
}
|
||||||
const tempStageList: Stage[] = [];
|
const tempStageList: Stage[] = [];
|
||||||
for (const i in temp) {
|
for (const element of Object.values(groupedByStartAt)) {
|
||||||
const element = temp[i];
|
|
||||||
const tempStartAt = parseInt(element.startAt);
|
const tempStartAt = parseInt(element.startAt);
|
||||||
const tempEndAt = parseInt(element.endAt);
|
const tempEndAt = parseInt(element.endAt);
|
||||||
const tempStageType = this.getStageTypeByTime(tempStartAt, tempEndAt);
|
const tempStageType = this.getStageTypeByTime(tempStartAt, tempEndAt);
|
||||||
@ -114,10 +123,9 @@ export class EventStage {
|
|||||||
type: tempStageType,
|
type: tempStageType,
|
||||||
startAt: tempStartAt,
|
startAt: tempStartAt,
|
||||||
endAt: tempEndAt,
|
endAt: tempEndAt,
|
||||||
songIdList: temp[i].music,
|
songIdList: element.music,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//排序
|
|
||||||
tempStageList.sort((a, b) => {
|
tempStageList.sort((a, b) => {
|
||||||
return a.startAt - b.startAt;
|
return a.startAt - b.startAt;
|
||||||
});
|
});
|
||||||
@ -133,9 +141,8 @@ export class EventStage {
|
|||||||
* @returns 格式化后的文本。
|
* @returns 格式化后的文本。
|
||||||
*/
|
*/
|
||||||
getStageTypeByTime(startAt: number, endAt: number): string {
|
getStageTypeByTime(startAt: number, endAt: number): string {
|
||||||
//根据时间获取当前stage类型
|
|
||||||
if (!this.isInitFull) {
|
if (!this.isInitFull) {
|
||||||
return;
|
return BangDreamEventStageType.unknown;
|
||||||
}
|
}
|
||||||
const stage = this.stageType.find((x) => {
|
const stage = this.stageType.find((x) => {
|
||||||
const startTime = parseInt(x.startAt);
|
const startTime = parseInt(x.startAt);
|
||||||
|
|||||||
@ -48,20 +48,14 @@ export async function writeJSON(filepath: string, data: object) {
|
|||||||
* @param filePath - 本地文件路径。
|
* @param filePath - 本地文件路径。
|
||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
export async function readExcelFile(filePath: string): Promise<any[]> {
|
export async function readExcelFile<
|
||||||
// 读取Excel文件
|
T extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
>(filePath: string): Promise<T[]> {
|
||||||
const workbook = XLSX.readFile(filePath);
|
const workbook = XLSX.readFile(filePath);
|
||||||
|
|
||||||
// 获取工作表的名字
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
|
||||||
// 获取工作表
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
return XLSX.utils.sheet_to_json<T>(worksheet);
|
||||||
// 将工作表转换为JSON
|
|
||||||
const json = XLSX.utils.sheet_to_json(worksheet);
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//将string[]转变为number[]
|
//将string[]转变为number[]
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export async function getIcon(server: Server): Promise<Image> {
|
|||||||
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
|
* @param displayedServerList - 允许展示或下载资源的服务器优先级列表,未传入时使用默认值。
|
||||||
*/
|
*/
|
||||||
export function getServerByPriority(
|
export function getServerByPriority(
|
||||||
content: Array<any>,
|
content: Array<unknown>,
|
||||||
displayedServerList: Server[] = globalDefaultServer,
|
displayedServerList: Server[] = globalDefaultServer,
|
||||||
) {
|
) {
|
||||||
const serverPriority: Server[] = [
|
const serverPriority: Server[] = [
|
||||||
@ -114,13 +114,8 @@ export function getServerByPriority(
|
|||||||
* @param server - 目标服务器。
|
* @param server - 目标服务器。
|
||||||
* @returns 判断结果。
|
* @returns 判断结果。
|
||||||
*/
|
*/
|
||||||
export function isServer(server: any): boolean {
|
export function isServer(server: unknown): boolean {
|
||||||
if (typeof server == 'number') {
|
return typeof server === 'number' && serverList.includes(server);
|
||||||
server = Server[server];
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Object.keys(Server).includes(server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,7 +124,7 @@ export function isServer(server: any): boolean {
|
|||||||
* @param serverList - 服务器列表参数。
|
* @param serverList - 服务器列表参数。
|
||||||
* @returns 判断结果。
|
* @returns 判断结果。
|
||||||
*/
|
*/
|
||||||
export function isServerList(serverList: Array<any>): boolean {
|
export function isServerList(serverList: Array<unknown>): boolean {
|
||||||
let result = true;
|
let result = true;
|
||||||
for (let i = 0; i < serverList.length; i++) {
|
for (let i = 0; i < serverList.length; i++) {
|
||||||
const element = serverList[i];
|
const element = serverList[i];
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
getServerByPriority,
|
getServerByPriority,
|
||||||
Server,
|
Server,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/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 {
|
export interface SongJacketSource {
|
||||||
jacketImage: Array<string>;
|
jacketImage: Array<string>;
|
||||||
@ -50,8 +51,8 @@ export class SongResourceRepository {
|
|||||||
* @param songId - 歌曲 ID。
|
* @param songId - 歌曲 ID。
|
||||||
* @param difficultyId - 难度 ID。
|
* @param difficultyId - 难度 ID。
|
||||||
*/
|
*/
|
||||||
async getChart(songId: number, difficultyId: number): Promise<object> {
|
async getChart(songId: number, difficultyId: number): Promise<BestdoriNote[]> {
|
||||||
return await this.provider.getJson<object>(
|
return await this.provider.getJson<BestdoriNote[]>(
|
||||||
`/api/charts/${songId}/${difficultyNameById[difficultyId]}.json`,
|
`/api/charts/${songId}/${difficultyNameById[difficultyId]}.json`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
BANGDREAM_SONG_TAG_NAME,
|
BANGDREAM_SONG_TAG_NAME,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||||
import { songResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/song-resource-repository';
|
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> =
|
export const difficultyName: Record<number, string> =
|
||||||
BANGDREAM_DIFFICULTY_NAME_BY_ID;
|
BANGDREAM_DIFFICULTY_NAME_BY_ID;
|
||||||
@ -248,7 +249,7 @@ export class Song {
|
|||||||
* @param difficultyId - 难度ID参数。
|
* @param difficultyId - 难度ID参数。
|
||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async getSongChart(difficultyId: number): Promise<object> {
|
async getSongChart(difficultyId: number): Promise<BestdoriNote[]> {
|
||||||
return await songResourceRepository.getChart(this.songId, difficultyId);
|
return await songResourceRepository.getChart(this.songId, difficultyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +273,7 @@ export class Song {
|
|||||||
* @param difficultyId - 难度ID参数。
|
* @param difficultyId - 难度ID参数。
|
||||||
* @param scoreUpMaxValue - 分数UpMax值参数,未传入时使用默认值。
|
* @param scoreUpMaxValue - 分数UpMax值参数,未传入时使用默认值。
|
||||||
* @param skillDuration - 技能Duration参数,未传入时使用默认值。
|
* @param skillDuration - 技能Duration参数,未传入时使用默认值。
|
||||||
* @param accruacy - accruacy参数,未传入时使用默认值。
|
* @param accuracy - accuracy参数,未传入时使用默认值。
|
||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
calcMeta(
|
calcMeta(
|
||||||
@ -280,7 +281,7 @@ export class Song {
|
|||||||
difficultyId: number,
|
difficultyId: number,
|
||||||
scoreUpMaxValue: number = 100,
|
scoreUpMaxValue: number = 100,
|
||||||
skillDuration: number = 7,
|
skillDuration: number = 7,
|
||||||
accruacy: number = 100,
|
accuracy: number = 100,
|
||||||
): number {
|
): number {
|
||||||
if (this.hasMeta == false) {
|
if (this.hasMeta == false) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -298,7 +299,7 @@ export class Song {
|
|||||||
this.meta[difficultyId][skillDuration][1];
|
this.meta[difficultyId][skillDuration][1];
|
||||||
}
|
}
|
||||||
const scoreParameter =
|
const scoreParameter =
|
||||||
skillParameter * ((1.1 * accruacy) / 100 + 0.8 * (1 - accruacy / 100));
|
skillParameter * ((1.1 * accuracy) / 100 + 0.8 * (1 - accuracy / 100));
|
||||||
return scoreParameter;
|
return scoreParameter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,10 @@ import {
|
|||||||
BANGDREAM_CUTOFF_CHART_SPEC,
|
BANGDREAM_CUTOFF_CHART_SPEC,
|
||||||
stripCutoffChartLabelTags,
|
stripCutoffChartLabelTags,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart-spec';
|
} 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'],
|
server: Server = Server['jp'],
|
||||||
) {
|
) {
|
||||||
//setStartToZero:是否将开始时间设置为0
|
//setStartToZero:是否将开始时间设置为0
|
||||||
const datasets = [];
|
const datasets: TimelineChartDataset[] = [];
|
||||||
const time = new Date().getTime();
|
const time = new Date().getTime();
|
||||||
if (cutoffList.length == 0) {
|
if (cutoffList.length == 0) {
|
||||||
return new Canvas(
|
return new Canvas(
|
||||||
@ -44,9 +48,9 @@ export async function drawCutoffChart(
|
|||||||
const cutoff = cutoffList[i];
|
const cutoff = cutoffList[i];
|
||||||
const tempEvent = new Event(cutoff.eventId);
|
const tempEvent = new Event(cutoff.eventId);
|
||||||
|
|
||||||
let lableName: string;
|
let labelName: string;
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
lableName = `[${tempEvent.eventId}] ${tempEvent.eventName[server]} T${cutoff.tier}`;
|
labelName = `[${tempEvent.eventId}] ${tempEvent.eventName[server]} T${cutoff.tier}`;
|
||||||
list.push(
|
list.push(
|
||||||
drawList({
|
drawList({
|
||||||
content: [
|
content: [
|
||||||
@ -59,7 +63,7 @@ export async function drawCutoffChart(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lableName = `T${cutoff.tier}`;
|
labelName = `T${cutoff.tier}`;
|
||||||
list.push(
|
list.push(
|
||||||
drawList({
|
drawList({
|
||||||
content: [
|
content: [
|
||||||
@ -73,7 +77,7 @@ export async function drawCutoffChart(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: lableName,
|
label: labelName,
|
||||||
data: cutoff.getChartData(setStartToZero),
|
data: cutoff.getChartData(setStartToZero),
|
||||||
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth,
|
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth,
|
||||||
borderColor: [tempColor.getRGBA(1)],
|
borderColor: [tempColor.getRGBA(1)],
|
||||||
@ -89,39 +93,39 @@ export async function drawCutoffChart(
|
|||||||
|
|
||||||
if (cutoff.status == BangDreamEventStatus.inProgress) {
|
if (cutoff.status == BangDreamEventStatus.inProgress) {
|
||||||
if (cutoff.predictEP != null && cutoff.predictEP != 0) {
|
if (cutoff.predictEP != null && cutoff.predictEP != 0) {
|
||||||
let data = [];
|
let data: TimelineChartPoint[] = [];
|
||||||
const history = cutoff.getPredictionHistory();
|
const history = cutoff.getPredictionHistory();
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
for (const p of history) {
|
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({
|
data.push({
|
||||||
x: new Date(cutoff.endAt - cutoff.startAt),
|
x: cutoff.endAt - cutoff.startAt,
|
||||||
y: history[history.length - 1].ep,
|
y: history[history.length - 1].ep,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for (const p of history) {
|
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({
|
data.push({
|
||||||
x: new Date(cutoff.endAt),
|
x: cutoff.endAt,
|
||||||
y: history[history.length - 1].ep,
|
y: history[history.length - 1].ep,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (setStartToZero) {
|
if (setStartToZero) {
|
||||||
data = [
|
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,
|
y: cutoff.predictEP,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
data = [
|
data = [
|
||||||
{ x: new Date(cutoff.startAt), y: cutoff.predictEP },
|
{ x: cutoff.startAt, y: cutoff.predictEP },
|
||||||
{ x: new Date(cutoff.endAt), y: cutoff.predictEP },
|
{ x: cutoff.endAt, y: cutoff.predictEP },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +144,9 @@ export async function drawCutoffChart(
|
|||||||
],
|
],
|
||||||
data: data,
|
data: data,
|
||||||
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth,
|
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.borderWidth,
|
||||||
borderDash: BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionDash,
|
borderDash: [
|
||||||
|
...BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionDash,
|
||||||
|
],
|
||||||
fill: false,
|
fill: false,
|
||||||
pointRadius:
|
pointRadius:
|
||||||
BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionPointRadius,
|
BANGDREAM_CUTOFF_CHART_SPEC.lineDataset.predictionPointRadius,
|
||||||
@ -157,7 +163,7 @@ export async function drawCutoffChart(
|
|||||||
label: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.label,
|
label: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.label,
|
||||||
borderColor: [tempColor.getRGBA(1)],
|
borderColor: [tempColor.getRGBA(1)],
|
||||||
backgroundColor: [tempColor.getRGBA(1)],
|
backgroundColor: [tempColor.getRGBA(1)],
|
||||||
data: [{ x: new Date(time), y: 0 }],
|
data: [{ x: time, y: 0 }],
|
||||||
fill: false,
|
fill: false,
|
||||||
pointRadius: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.pointRadius,
|
pointRadius: BANGDREAM_CUTOFF_CHART_SPEC.currentTimeDataset.pointRadius,
|
||||||
pointHoverRadius:
|
pointHoverRadius:
|
||||||
@ -204,27 +210,27 @@ export async function drawCutoffChart(
|
|||||||
/**
|
/**
|
||||||
* 在图片布局层中绘制档线活动排名谱面。
|
* 在图片布局层中绘制档线活动排名谱面。
|
||||||
*
|
*
|
||||||
* @param CutoffEventTop - 档线活动排名参数。
|
* @param cutoffEventTop - 档线活动排名参数。
|
||||||
* @param setStartToZero - setStartToZero参数,未传入时使用默认值。
|
* @param setStartToZero - setStartToZero参数,未传入时使用默认值。
|
||||||
*/
|
*/
|
||||||
export async function drawCutoffEventTopChart(
|
export async function drawCutoffEventTopChart(
|
||||||
CutoffEventTop: CutoffEventTop,
|
cutoffEventTop: CutoffEventTop,
|
||||||
setStartToZero = false,
|
setStartToZero = false,
|
||||||
) {
|
) {
|
||||||
const datasets = [];
|
const datasets: TimelineChartDataset[] = [];
|
||||||
if (CutoffEventTop == undefined) {
|
if (cutoffEventTop == undefined) {
|
||||||
return new Canvas(
|
return new Canvas(
|
||||||
BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.width,
|
BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.width,
|
||||||
BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.height,
|
BANGDREAM_CUTOFF_CHART_SPEC.emptyCanvas.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const allData = CutoffEventTop.getChartData();
|
const allData = cutoffEventTop.getChartData();
|
||||||
let colorNumber = 0;
|
let colorNumber = 0;
|
||||||
for (const key in allData) {
|
for (const key in allData) {
|
||||||
const tempColor = getPresetColor(colorNumber);
|
const tempColor = getPresetColor(colorNumber);
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: stripCutoffChartLabelTags(
|
label: stripCutoffChartLabelTags(
|
||||||
CutoffEventTop.getUserNameById(Number(key)),
|
cutoffEventTop.getUserNameById(Number(key)),
|
||||||
),
|
),
|
||||||
data: allData[key],
|
data: allData[key],
|
||||||
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.eventTopDataset.borderWidth,
|
borderWidth: BANGDREAM_CUTOFF_CHART_SPEC.eventTopDataset.borderWidth,
|
||||||
@ -249,8 +255,8 @@ export async function drawCutoffEventTopChart(
|
|||||||
return await drawTimeLineChart(
|
return await drawTimeLineChart(
|
||||||
{
|
{
|
||||||
data,
|
data,
|
||||||
start: new Date(CutoffEventTop.startAt),
|
start: new Date(cutoffEventTop.startAt),
|
||||||
end: new Date(CutoffEventTop.endAt),
|
end: new Date(cutoffEventTop.endAt),
|
||||||
setStartToZero,
|
setStartToZero,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Canvas, Image, loadImage } from 'skia-canvas';
|
import { Canvas, Image, loadImage } from 'skia-canvas';
|
||||||
|
import type { CanvasRenderingContext2D } from 'skia-canvas';
|
||||||
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
|
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
|
||||||
import {
|
import {
|
||||||
BangDreamLocalAssetKey,
|
BangDreamLocalAssetKey,
|
||||||
@ -96,7 +97,7 @@ async function loadCoverImage(cover: string | Buffer): Promise<Image> {
|
|||||||
* @param y - 纵向绘制坐标。
|
* @param y - 纵向绘制坐标。
|
||||||
*/
|
*/
|
||||||
function setAdaptiveTextBaseline(
|
function setAdaptiveTextBaseline(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -135,7 +136,7 @@ function getTimePosition(layout: PreviewLayout, time: number) {
|
|||||||
* @param coverImg - coverImg参数。
|
* @param coverImg - coverImg参数。
|
||||||
*/
|
*/
|
||||||
function drawBaseInfo(
|
function drawBaseInfo(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
payload: BestdoriPreviewPayload,
|
payload: BestdoriPreviewPayload,
|
||||||
coverImg: Image,
|
coverImg: Image,
|
||||||
@ -218,7 +219,7 @@ function drawBaseInfo(
|
|||||||
* @param ctx - 画布绘图上下文。
|
* @param ctx - 画布绘图上下文。
|
||||||
* @param layout - 布局参数。
|
* @param layout - 布局参数。
|
||||||
*/
|
*/
|
||||||
function drawTracks(ctx: any, layout: PreviewLayout): void {
|
function drawTracks(ctx: CanvasRenderingContext2D, layout: PreviewLayout): void {
|
||||||
for (let i = 0; i < layout.colCount; i++) {
|
for (let i = 0; i < layout.colCount; i++) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const x =
|
const x =
|
||||||
@ -259,7 +260,7 @@ function drawTracks(ctx: any, layout: PreviewLayout): void {
|
|||||||
* @param notes - 谱面音符列表。
|
* @param notes - 谱面音符列表。
|
||||||
*/
|
*/
|
||||||
function drawBeatLines(
|
function drawBeatLines(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
notes: PreviewNote[],
|
notes: PreviewNote[],
|
||||||
): void {
|
): void {
|
||||||
@ -309,7 +310,10 @@ function drawBeatLines(
|
|||||||
* @param ctx - 画布绘图上下文。
|
* @param ctx - 画布绘图上下文。
|
||||||
* @param layout - 布局参数。
|
* @param layout - 布局参数。
|
||||||
*/
|
*/
|
||||||
function drawTimeline(ctx: any, layout: PreviewLayout): void {
|
function drawTimeline(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
layout: PreviewLayout,
|
||||||
|
): void {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
|
ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`;
|
||||||
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
|
ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText;
|
||||||
@ -330,7 +334,7 @@ function drawTimeline(ctx: any, layout: PreviewLayout): void {
|
|||||||
* @param notes - 谱面音符列表。
|
* @param notes - 谱面音符列表。
|
||||||
*/
|
*/
|
||||||
function drawCountAndBpmLines(
|
function drawCountAndBpmLines(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
notes: PreviewNote[],
|
notes: PreviewNote[],
|
||||||
): void {
|
): void {
|
||||||
@ -376,7 +380,7 @@ function drawCountAndBpmLines(
|
|||||||
* @param note - 谱面音符。
|
* @param note - 谱面音符。
|
||||||
*/
|
*/
|
||||||
function drawTapNote(
|
function drawTapNote(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
noteImages: Record<string, Image>,
|
noteImages: Record<string, Image>,
|
||||||
note: PreviewNote,
|
note: PreviewNote,
|
||||||
@ -417,7 +421,7 @@ function drawTapNote(
|
|||||||
* @param note - 谱面音符。
|
* @param note - 谱面音符。
|
||||||
*/
|
*/
|
||||||
function drawDirectionalNote(
|
function drawDirectionalNote(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
noteImages: Record<string, Image>,
|
noteImages: Record<string, Image>,
|
||||||
note: PreviewNote,
|
note: PreviewNote,
|
||||||
@ -469,7 +473,7 @@ function drawDirectionalNote(
|
|||||||
* @param note - 谱面音符。
|
* @param note - 谱面音符。
|
||||||
*/
|
*/
|
||||||
function drawSimNote(
|
function drawSimNote(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
noteImages: Record<string, Image>,
|
noteImages: Record<string, Image>,
|
||||||
note: PreviewNote,
|
note: PreviewNote,
|
||||||
@ -498,7 +502,11 @@ function drawSimNote(
|
|||||||
* @param layout - 布局参数。
|
* @param layout - 布局参数。
|
||||||
* @param note - 谱面音符。
|
* @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 time = note.time as number[];
|
||||||
const lane = note.lane as number[];
|
const lane = note.lane as number[];
|
||||||
const startCol = Math.floor(time[0] / layout.secondsPerCol);
|
const startCol = Math.floor(time[0] / layout.secondsPerCol);
|
||||||
@ -543,7 +551,7 @@ function drawBarNote(ctx: any, layout: PreviewLayout, note: PreviewNote): void {
|
|||||||
* @param notes - 谱面音符列表。
|
* @param notes - 谱面音符列表。
|
||||||
*/
|
*/
|
||||||
function drawNotes(
|
function drawNotes(
|
||||||
ctx: any,
|
ctx: CanvasRenderingContext2D,
|
||||||
layout: PreviewLayout,
|
layout: PreviewLayout,
|
||||||
noteImages: Record<string, Image>,
|
noteImages: Record<string, Image>,
|
||||||
notes: PreviewNote[],
|
notes: PreviewNote[],
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { ChartDataset } from 'chart.js';
|
||||||
|
|
||||||
export const BANGDREAM_TIMELINE_CHART_SPEC = {
|
export const BANGDREAM_TIMELINE_CHART_SPEC = {
|
||||||
animation: false,
|
animation: false,
|
||||||
canvas: {
|
canvas: {
|
||||||
@ -18,14 +20,19 @@ export const BANGDREAM_TIMELINE_CHART_SPEC = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export interface TimelineChartPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimelineChartDataset = ChartDataset<'line', TimelineChartPoint[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算时间线图表的原始 Y 轴最大值。
|
* 计算时间线图表的原始 Y 轴最大值。
|
||||||
*
|
*
|
||||||
* @param datasets - Chart.js 数据集。
|
* @param datasets - Chart.js 数据集。
|
||||||
*/
|
*/
|
||||||
export function getTimelineRawYMax(
|
export function getTimelineRawYMax(datasets: TimelineChartDataset[]) {
|
||||||
datasets: Array<{ data: Array<{ y: number }> }>,
|
|
||||||
) {
|
|
||||||
return Math.max(
|
return Math.max(
|
||||||
...datasets.map((dataset) => Math.max(...dataset.data.map((pt) => pt.y))),
|
...datasets.map((dataset) => Math.max(...dataset.data.map((pt) => pt.y))),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import type { ChartConfiguration, ChartItem } from 'chart.js';
|
||||||
import { Canvas, FontLibrary, loadImage } from 'skia-canvas';
|
import { Canvas, FontLibrary, loadImage } from 'skia-canvas';
|
||||||
import 'chartjs-adapter-moment';
|
import 'chartjs-adapter-moment';
|
||||||
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
@ -7,6 +8,8 @@ import {
|
|||||||
BANGDREAM_TIMELINE_CHART_SPEC,
|
BANGDREAM_TIMELINE_CHART_SPEC,
|
||||||
getTimelineDisplayYMax,
|
getTimelineDisplayYMax,
|
||||||
getTimelineRawYMax,
|
getTimelineRawYMax,
|
||||||
|
type TimelineChartDataset,
|
||||||
|
type TimelineChartPoint,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec';
|
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/timeline-chart-spec';
|
||||||
|
|
||||||
// 2. 注册 Chart.js 所有组件
|
// 2. 注册 Chart.js 所有组件
|
||||||
@ -19,12 +22,12 @@ Chart.register(...registerables);
|
|||||||
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]);
|
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]);
|
||||||
|
|
||||||
// 5. 定义参数接口
|
// 5. 定义参数接口
|
||||||
interface drawTimeLineChartOptions {
|
interface DrawTimeLineChartOptions {
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
setStartToZero?: boolean;
|
setStartToZero?: boolean;
|
||||||
data: {
|
data: {
|
||||||
datasets: any[];
|
datasets: TimelineChartDataset[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +39,7 @@ interface drawTimeLineChartOptions {
|
|||||||
* @param displayLabel - 展示Label参数,未传入时使用默认值。
|
* @param displayLabel - 展示Label参数,未传入时使用默认值。
|
||||||
*/
|
*/
|
||||||
export async function drawTimeLineChart(
|
export async function drawTimeLineChart(
|
||||||
{ start, end, setStartToZero = false, data }: drawTimeLineChartOptions,
|
{ start, end, setStartToZero = false, data }: DrawTimeLineChartOptions,
|
||||||
displayLabel = false,
|
displayLabel = false,
|
||||||
) {
|
) {
|
||||||
const width = BANGDREAM_TIMELINE_CHART_SPEC.canvas.width;
|
const width = BANGDREAM_TIMELINE_CHART_SPEC.canvas.width;
|
||||||
@ -63,12 +66,12 @@ export async function drawTimeLineChart(
|
|||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'time',
|
type: 'time' as const,
|
||||||
time: {
|
time: {
|
||||||
unit: BANGDREAM_TIMELINE_CHART_SPEC.xAxis.unit,
|
unit: BANGDREAM_TIMELINE_CHART_SPEC.xAxis.unit,
|
||||||
},
|
},
|
||||||
min: start,
|
min: start.getTime(),
|
||||||
max: end,
|
max: end.getTime(),
|
||||||
display: !setStartToZero,
|
display: !setStartToZero,
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
@ -79,7 +82,7 @@ export async function drawTimeLineChart(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 10. Chart.js 配置
|
// 10. Chart.js 配置
|
||||||
const config = {
|
const config: ChartConfiguration<'line', TimelineChartPoint[]> = {
|
||||||
type: 'line' as const,
|
type: 'line' as const,
|
||||||
data,
|
data,
|
||||||
options: {
|
options: {
|
||||||
@ -91,7 +94,7 @@ export async function drawTimeLineChart(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 11. 生成 Chart.js 图表
|
// 11. 生成 Chart.js 图表
|
||||||
new Chart(ctx as any, config as any);
|
new Chart(ctx as unknown as ChartItem, config);
|
||||||
|
|
||||||
// 12. 返回 skia-canvas 的 Image 对象
|
// 12. 返回 skia-canvas 的 Image 对象
|
||||||
return canvas;
|
return canvas;
|
||||||
|
|||||||
@ -16,8 +16,8 @@ describe('BangDream chart specs', () => {
|
|||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
getTimelineRawYMax([
|
getTimelineRawYMax([
|
||||||
{ data: [{ y: 100 }, { y: 300 }] },
|
{ data: [{ x: 0, y: 100 }, { x: 1, y: 300 }] },
|
||||||
{ data: [{ y: 200 }] },
|
{ data: [{ x: 0, y: 200 }] },
|
||||||
]),
|
]),
|
||||||
).toBe(300);
|
).toBe(300);
|
||||||
expect(getTimelineDisplayYMax(300)).toBeCloseTo(1430);
|
expect(getTimelineDisplayYMax(300)).toBeCloseTo(1430);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user