refactor: 收口 BangDream 试炼渲染规格
This commit is contained in:
parent
f8d964c6ee
commit
4220efa6b2
@ -363,6 +363,9 @@ export interface TsuguHook {
|
|||||||
- 已新增 `render-blocks/gacha-simulate-spec.ts`,收口抽卡模拟网格宽度、单抽/汇总混排尺寸、重复卡叠层、数量右对齐和卡池横幅布局;`gacha-simulate.ts` 改为消费 spec,抽卡概率和卡池选择逻辑保持不变。
|
- 已新增 `render-blocks/gacha-simulate-spec.ts`,收口抽卡模拟网格宽度、单抽/汇总混排尺寸、重复卡叠层、数量右对齐和卡池横幅布局;`gacha-simulate.ts` 改为消费 spec,抽卡概率和卡池选择逻辑保持不变。
|
||||||
- 已新增 `gacha-simulate-spec.spec.ts`,覆盖 10 抽网格、汇总模式、重复卡叠层、计数字样和横幅尺寸;本地生成 `gacha-simulate-spec-10-259.jpg` 和 `gacha-simulate-spec-50-259-after-cache-fix.jpg`,验证抽卡模拟两种布局模式图片输出正常。
|
- 已新增 `gacha-simulate-spec.spec.ts`,覆盖 10 抽网格、汇总模式、重复卡叠层、计数字样和横幅尺寸;本地生成 `gacha-simulate-spec-10-259.jpg` 和 `gacha-simulate-spec-50-259-after-cache-fix.jpg`,验证抽卡模拟两种布局模式图片输出正常。
|
||||||
- 抽卡模拟 50 抽 smoke 暴露资源下载短时失败会把 URL 写入无过期错误缓存,导致后续重试直接输出 `asset error`;已改为只有 HTTP 404 进入错误缓存,超时/网络抖动不污染 URL,并新增 `file-cache-client.spec.ts` 覆盖瞬时失败后可再次下载、404 缓存缺失资源两种路径。
|
- 抽卡模拟 50 抽 smoke 暴露资源下载短时失败会把 URL 写入无过期错误缓存,导致后续重试直接输出 `asset error`;已改为只有 HTTP 404 进入错误缓存,超时/网络抖动不污染 URL,并新增 `file-cache-client.spec.ts` 覆盖瞬时失败后可再次下载、404 缓存缺失资源两种路径。
|
||||||
|
- 已新增 `render-blocks/event-stage-spec.ts`,收口试炼列表最大列高、歌曲单元格/封面/ID/难度条尺寸和试炼类型顶部文字规格;`event-stage.ts` 与 `list-event-stage.ts` 改为消费 spec,活动/歌曲选择和输出结构保持不变。
|
||||||
|
- 已新增 `event-stage-spec.spec.ts`,覆盖试炼歌曲行尺寸、类型顶部文字规格和按列高拆分算法;本地生成 `event-stage-spec-310.jpg`、`event-stage-spec-310-meta.jpg`,验证 `/查试炼 310` 与 `/查试炼 310 -m` 图片输出正常。
|
||||||
|
- smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录。
|
||||||
|
|
||||||
### Phase 6:策略 policy 和时间/档线规则
|
### Phase 6:策略 policy 和时间/档线规则
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage';
|
} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage';
|
||||||
import { serverNameFullList } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
import { serverNameFullList } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
import { drawTitle } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/title';
|
import { drawTitle } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/title';
|
||||||
import { Canvas } from 'skia-canvas';
|
|
||||||
import {
|
import {
|
||||||
drawEventStageTypeTop,
|
drawEventStageTypeTop,
|
||||||
drawEventStageSongHorizontal,
|
drawEventStageSongHorizontal,
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
stackImage,
|
stackImage,
|
||||||
stackImageHorizontal,
|
stackImageHorizontal,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
|
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
|
||||||
|
import { splitEventStageImagesByColumnHeight } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在QQBot 图片视图层中绘制活动试炼。
|
* 在QQBot 图片视图层中绘制活动试炼。
|
||||||
@ -81,33 +81,9 @@ export async function drawEventStage(
|
|||||||
const eventStageResults = await Promise.all(eventStagePromises);
|
const eventStageResults = await Promise.all(eventStagePromises);
|
||||||
|
|
||||||
//将活动stage图片纵向并横向合并
|
//将活动stage图片纵向并横向合并
|
||||||
let tempH = 0;
|
const eventStageImageListHorizontal = splitEventStageImagesByColumnHeight(
|
||||||
const maxHeight = 6000;
|
eventStageResults,
|
||||||
|
).map((list) => drawDataBlock({ list }));
|
||||||
let tempEventStageImageList: Canvas[] = [];
|
|
||||||
const eventStageImageListHorizontal: Canvas[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < eventStageResults.length; i++) {
|
|
||||||
const tempImage = eventStageResults[i];
|
|
||||||
tempH += tempImage.height;
|
|
||||||
if (tempH > maxHeight) {
|
|
||||||
if (tempEventStageImageList.length > 0) {
|
|
||||||
eventStageImageListHorizontal.push(
|
|
||||||
drawDataBlock({ list: tempEventStageImageList }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tempEventStageImageList = [];
|
|
||||||
tempH = tempImage.height;
|
|
||||||
}
|
|
||||||
tempEventStageImageList.push(tempImage);
|
|
||||||
|
|
||||||
if (i == eventStageResults.length - 1) {
|
|
||||||
eventStageImageListHorizontal.push(
|
|
||||||
drawDataBlock({ list: tempEventStageImageList }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventStageListImage = stackImageHorizontal(
|
const eventStageListImage = stackImageHorizontal(
|
||||||
eventStageImageListHorizontal,
|
eventStageImageListHorizontal,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
export interface EventStageCanvasLike {
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BANGDREAM_EVENT_STAGE_SPEC = {
|
||||||
|
list: {
|
||||||
|
maxColumnHeight: 6000,
|
||||||
|
},
|
||||||
|
songRow: {
|
||||||
|
difficultyHeightScale: 10,
|
||||||
|
jacketInsetX: 3,
|
||||||
|
jacketY: 0,
|
||||||
|
songCountPerRow: 8,
|
||||||
|
songId: {
|
||||||
|
color: '#a7a7a7',
|
||||||
|
fontSize: 16,
|
||||||
|
x: 4,
|
||||||
|
y: 108,
|
||||||
|
},
|
||||||
|
verticalPadding: 10,
|
||||||
|
width: 800,
|
||||||
|
},
|
||||||
|
typeTop: {
|
||||||
|
fontSize: 25,
|
||||||
|
rightPadding: 50,
|
||||||
|
strokeWidth: 4.5,
|
||||||
|
textColor: '#ffffff',
|
||||||
|
textX: 20,
|
||||||
|
yOffset: -2,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试炼歌曲单元格宽度。
|
||||||
|
*/
|
||||||
|
export function getEventStageSongCellWidth(): number {
|
||||||
|
return (
|
||||||
|
BANGDREAM_EVENT_STAGE_SPEC.songRow.width /
|
||||||
|
BANGDREAM_EVENT_STAGE_SPEC.songRow.songCountPerRow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试炼歌曲封面绘制高度。
|
||||||
|
*/
|
||||||
|
export function getEventStageSongJacketHeight(): number {
|
||||||
|
return getEventStageSongCellWidth() - 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试炼歌曲单元格高度。
|
||||||
|
*/
|
||||||
|
export function getEventStageSongCellHeight(): number {
|
||||||
|
return (getEventStageSongCellWidth() / 180) * 210;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试炼歌曲横向行尺寸。
|
||||||
|
*/
|
||||||
|
export function getEventStageSongRowSize(): { height: number; width: number } {
|
||||||
|
return {
|
||||||
|
height:
|
||||||
|
getEventStageSongCellHeight() +
|
||||||
|
BANGDREAM_EVENT_STAGE_SPEC.songRow.verticalPadding,
|
||||||
|
width: BANGDREAM_EVENT_STAGE_SPEC.songRow.width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将试炼 stage 图片按最大列高拆成多列。
|
||||||
|
*
|
||||||
|
* @param images - 试炼 stage 图片列表。
|
||||||
|
* @param maxHeight - 单列最大高度,未传入时使用默认值。
|
||||||
|
*/
|
||||||
|
export function splitEventStageImagesByColumnHeight<
|
||||||
|
T extends EventStageCanvasLike,
|
||||||
|
>(
|
||||||
|
images: T[],
|
||||||
|
maxHeight = BANGDREAM_EVENT_STAGE_SPEC.list.maxColumnHeight,
|
||||||
|
): T[][] {
|
||||||
|
const columns: T[][] = [];
|
||||||
|
let currentColumn: T[] = [];
|
||||||
|
let currentHeight = 0;
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
currentHeight += image.height;
|
||||||
|
if (currentHeight > maxHeight && currentColumn.length > 0) {
|
||||||
|
columns.push(currentColumn);
|
||||||
|
currentColumn = [];
|
||||||
|
currentHeight = image.height;
|
||||||
|
}
|
||||||
|
currentColumn.push(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentColumn.length > 0) {
|
||||||
|
columns.push(currentColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
@ -15,6 +15,13 @@ import { formatTime } from './list-time';
|
|||||||
import { setFontStyle } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
|
import { setFontStyle } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
|
||||||
import { stackImageHorizontal } from './image-stack';
|
import { stackImageHorizontal } from './image-stack';
|
||||||
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
|
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
|
||||||
|
import {
|
||||||
|
BANGDREAM_EVENT_STAGE_SPEC,
|
||||||
|
getEventStageSongCellHeight,
|
||||||
|
getEventStageSongCellWidth,
|
||||||
|
getEventStageSongJacketHeight,
|
||||||
|
getEventStageSongRowSize,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||||
|
|
||||||
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]);
|
FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]);
|
||||||
|
|
||||||
@ -63,19 +70,22 @@ export async function drawEventStageTypeTop(stage: Stage): Promise<Canvas> {
|
|||||||
eventStageTypeTopImage.height,
|
eventStageTypeTopImage.height,
|
||||||
);
|
);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
const typeTopSpec = BANGDREAM_EVENT_STAGE_SPEC.typeTop;
|
||||||
ctx.drawImage(eventStageTypeTopImage, 0, 0);
|
ctx.drawImage(eventStageTypeTopImage, 0, 0);
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
setFontStyle(ctx, 25, 'old');
|
setFontStyle(ctx, typeTopSpec.fontSize, 'old');
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = typeTopSpec.textColor;
|
||||||
ctx.lineWidth = 4.5;
|
ctx.lineWidth = typeTopSpec.strokeWidth;
|
||||||
ctx.strokeStyle = stageTypeTextStrokeColor[type];
|
ctx.strokeStyle = stageTypeTextStrokeColor[type];
|
||||||
ctx.strokeText(timeText, 20, canvas.height / 2 - 2);
|
const textY = canvas.height / 2 + typeTopSpec.yOffset;
|
||||||
ctx.fillText(timeText, 20, canvas.height / 2 - 2);
|
ctx.strokeText(timeText, typeTopSpec.textX, textY);
|
||||||
|
ctx.fillText(timeText, typeTopSpec.textX, textY);
|
||||||
|
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.strokeText(typeName, canvas.width - 50, canvas.height / 2 - 2);
|
const typeNameX = canvas.width - typeTopSpec.rightPadding;
|
||||||
ctx.fillText(typeName, canvas.width - 50, canvas.height / 2 - 2);
|
ctx.strokeText(typeName, typeNameX, textY);
|
||||||
|
ctx.fillText(typeName, typeNameX, textY);
|
||||||
|
|
||||||
//如果是当前进行中活动,额外画标志
|
//如果是当前进行中活动,额外画标志
|
||||||
if (new Date() >= new Date(startAt) && new Date() <= new Date(endAt)) {
|
if (new Date() >= new Date(startAt) && new Date() <= new Date(endAt)) {
|
||||||
@ -98,23 +108,26 @@ async function drawSongInEventStageSongHorizontal(
|
|||||||
meta: boolean,
|
meta: boolean,
|
||||||
): Promise<Canvas> {
|
): Promise<Canvas> {
|
||||||
//绘制活动中的每个歌曲(包括难度)
|
//绘制活动中的每个歌曲(包括难度)
|
||||||
const canvas = new Canvas(800 / 8, (800 / 8 / 180) * 210);
|
const cellWidth = getEventStageSongCellWidth();
|
||||||
|
const cellHeight = getEventStageSongCellHeight();
|
||||||
|
const jacketImageHeight = getEventStageSongJacketHeight();
|
||||||
|
const songRowSpec = BANGDREAM_EVENT_STAGE_SPEC.songRow;
|
||||||
|
const canvas = new Canvas(cellWidth, cellHeight);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
const jacketImageHeight = 800 / 8 - 6;
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
await song.getSongJacketImage(),
|
await song.getSongJacketImage(),
|
||||||
3,
|
songRowSpec.jacketInsetX,
|
||||||
0,
|
songRowSpec.jacketY,
|
||||||
jacketImageHeight,
|
jacketImageHeight,
|
||||||
jacketImageHeight,
|
jacketImageHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.textAlign = 'start';
|
ctx.textAlign = 'start';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
setFontStyle(ctx, 16, 'old');
|
setFontStyle(ctx, songRowSpec.songId.fontSize, 'old');
|
||||||
ctx.fillStyle = '#a7a7a7';
|
ctx.fillStyle = songRowSpec.songId.color;
|
||||||
ctx.fillText(`ID:${song.songId}`, 4, 108);
|
ctx.fillText(`ID:${song.songId}`, songRowSpec.songId.x, songRowSpec.songId.y);
|
||||||
|
|
||||||
//难度,高度为meta*10像素
|
//难度,高度为meta*10像素
|
||||||
/**
|
/**
|
||||||
@ -130,9 +143,10 @@ async function drawSongInEventStageSongHorizontal(
|
|||||||
ctx.fillStyle = difficultyColorList[difficultyId];
|
ctx.fillStyle = difficultyColorList[difficultyId];
|
||||||
ctx.fillRect(
|
ctx.fillRect(
|
||||||
0,
|
0,
|
||||||
jacketImageHeight - meta * 10,
|
jacketImageHeight -
|
||||||
|
meta * BANGDREAM_EVENT_STAGE_SPEC.songRow.difficultyHeightScale,
|
||||||
jacketImageHeight,
|
jacketImageHeight,
|
||||||
meta * 10,
|
meta * BANGDREAM_EVENT_STAGE_SPEC.songRow.difficultyHeightScale,
|
||||||
);
|
);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
@ -145,7 +159,11 @@ async function drawSongInEventStageSongHorizontal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const difficultyLineGraph = stackImageHorizontal(difficultyLineGraphList);
|
const difficultyLineGraph = stackImageHorizontal(difficultyLineGraphList);
|
||||||
ctx.drawImage(difficultyLineGraph, 3, 0);
|
ctx.drawImage(
|
||||||
|
difficultyLineGraph,
|
||||||
|
songRowSpec.jacketInsetX,
|
||||||
|
songRowSpec.jacketY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
@ -165,13 +183,14 @@ export async function drawEventStageSongHorizontal(
|
|||||||
//绘制活动中的歌曲列表(横向)
|
//绘制活动中的歌曲列表(横向)
|
||||||
const songIdList = stage.songIdList;
|
const songIdList = stage.songIdList;
|
||||||
|
|
||||||
const canvas = new Canvas(800, (800 / 8 / 180) * 210 + 10);
|
const songRowSize = getEventStageSongRowSize();
|
||||||
|
const canvas = new Canvas(songRowSize.width, songRowSize.height);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
for (let i = 0; i < songIdList.length; i++) {
|
for (let i = 0; i < songIdList.length; i++) {
|
||||||
const song = new Song(songIdList[i]);
|
const song = new Song(songIdList[i]);
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
await drawSongInEventStageSongHorizontal(song, meta),
|
await drawSongInEventStageSongHorizontal(song, meta),
|
||||||
(800 / 8) * i,
|
getEventStageSongCellWidth() * i,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
55
test/qqbot/plugins/bangDream/tsugu/event-stage-spec.spec.ts
Normal file
55
test/qqbot/plugins/bangDream/tsugu/event-stage-spec.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
BANGDREAM_EVENT_STAGE_SPEC,
|
||||||
|
getEventStageSongCellHeight,
|
||||||
|
getEventStageSongCellWidth,
|
||||||
|
getEventStageSongJacketHeight,
|
||||||
|
getEventStageSongRowSize,
|
||||||
|
splitEventStageImagesByColumnHeight,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||||
|
|
||||||
|
describe('BangDream event stage spec', () => {
|
||||||
|
it('keeps song row dimensions compatible with existing layout', () => {
|
||||||
|
expect(getEventStageSongCellWidth()).toBe(100);
|
||||||
|
expect(getEventStageSongJacketHeight()).toBe(94);
|
||||||
|
expect(getEventStageSongCellHeight()).toBeCloseTo((100 / 180) * 210, 6);
|
||||||
|
expect(getEventStageSongRowSize()).toEqual({
|
||||||
|
height: getEventStageSongCellHeight() + 10,
|
||||||
|
width: 800,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps type top text and column limits stable', () => {
|
||||||
|
expect(BANGDREAM_EVENT_STAGE_SPEC.typeTop.fontSize).toBe(25);
|
||||||
|
expect(BANGDREAM_EVENT_STAGE_SPEC.typeTop.strokeWidth).toBe(4.5);
|
||||||
|
expect(BANGDREAM_EVENT_STAGE_SPEC.list.maxColumnHeight).toBe(6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits stage images by max column height', () => {
|
||||||
|
const columns = splitEventStageImagesByColumnHeight([
|
||||||
|
{ height: 2000, id: 1 },
|
||||||
|
{ height: 3000, id: 2 },
|
||||||
|
{ height: 1500, id: 3 },
|
||||||
|
{ height: 1000, id: 4 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(columns.map((column) => column.map((item) => item.id))).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps oversized first image in its own column', () => {
|
||||||
|
const columns = splitEventStageImagesByColumnHeight(
|
||||||
|
[
|
||||||
|
{ height: 7000, id: 1 },
|
||||||
|
{ height: 100, id: 2 },
|
||||||
|
],
|
||||||
|
6000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(columns.map((column) => column.map((item) => item.id))).toEqual([
|
||||||
|
[1],
|
||||||
|
[2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user