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,抽卡概率和卡池选择逻辑保持不变。
|
||||
- 已新增 `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 缓存缺失资源两种路径。
|
||||
- 已新增 `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 和时间/档线规则
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage';
|
||||
import { serverNameFullList } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { drawTitle } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/title';
|
||||
import { Canvas } from 'skia-canvas';
|
||||
import {
|
||||
drawEventStageTypeTop,
|
||||
drawEventStageSongHorizontal,
|
||||
@ -17,6 +16,7 @@ import {
|
||||
stackImage,
|
||||
stackImageHorizontal,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
|
||||
import { splitEventStageImagesByColumnHeight } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||
|
||||
/**
|
||||
* 在QQBot 图片视图层中绘制活动试炼。
|
||||
@ -81,33 +81,9 @@ export async function drawEventStage(
|
||||
const eventStageResults = await Promise.all(eventStagePromises);
|
||||
|
||||
//将活动stage图片纵向并横向合并
|
||||
let tempH = 0;
|
||||
const maxHeight = 6000;
|
||||
|
||||
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 eventStageImageListHorizontal = splitEventStageImagesByColumnHeight(
|
||||
eventStageResults,
|
||||
).map((list) => drawDataBlock({ list }));
|
||||
const eventStageListImage = stackImageHorizontal(
|
||||
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 { stackImageHorizontal } from './image-stack';
|
||||
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`]);
|
||||
|
||||
@ -63,19 +70,22 @@ export async function drawEventStageTypeTop(stage: Stage): Promise<Canvas> {
|
||||
eventStageTypeTopImage.height,
|
||||
);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const typeTopSpec = BANGDREAM_EVENT_STAGE_SPEC.typeTop;
|
||||
ctx.drawImage(eventStageTypeTopImage, 0, 0);
|
||||
ctx.textBaseline = 'middle';
|
||||
setFontStyle(ctx, 25, 'old');
|
||||
setFontStyle(ctx, typeTopSpec.fontSize, 'old');
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.lineWidth = 4.5;
|
||||
ctx.fillStyle = typeTopSpec.textColor;
|
||||
ctx.lineWidth = typeTopSpec.strokeWidth;
|
||||
ctx.strokeStyle = stageTypeTextStrokeColor[type];
|
||||
ctx.strokeText(timeText, 20, canvas.height / 2 - 2);
|
||||
ctx.fillText(timeText, 20, canvas.height / 2 - 2);
|
||||
const textY = canvas.height / 2 + typeTopSpec.yOffset;
|
||||
ctx.strokeText(timeText, typeTopSpec.textX, textY);
|
||||
ctx.fillText(timeText, typeTopSpec.textX, textY);
|
||||
|
||||
ctx.textAlign = 'right';
|
||||
ctx.strokeText(typeName, canvas.width - 50, canvas.height / 2 - 2);
|
||||
ctx.fillText(typeName, canvas.width - 50, canvas.height / 2 - 2);
|
||||
const typeNameX = canvas.width - typeTopSpec.rightPadding;
|
||||
ctx.strokeText(typeName, typeNameX, textY);
|
||||
ctx.fillText(typeName, typeNameX, textY);
|
||||
|
||||
//如果是当前进行中活动,额外画标志
|
||||
if (new Date() >= new Date(startAt) && new Date() <= new Date(endAt)) {
|
||||
@ -98,23 +108,26 @@ async function drawSongInEventStageSongHorizontal(
|
||||
meta: boolean,
|
||||
): 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 jacketImageHeight = 800 / 8 - 6;
|
||||
ctx.drawImage(
|
||||
await song.getSongJacketImage(),
|
||||
3,
|
||||
0,
|
||||
songRowSpec.jacketInsetX,
|
||||
songRowSpec.jacketY,
|
||||
jacketImageHeight,
|
||||
jacketImageHeight,
|
||||
);
|
||||
|
||||
ctx.textAlign = 'start';
|
||||
ctx.textBaseline = 'middle';
|
||||
setFontStyle(ctx, 16, 'old');
|
||||
ctx.fillStyle = '#a7a7a7';
|
||||
ctx.fillText(`ID:${song.songId}`, 4, 108);
|
||||
setFontStyle(ctx, songRowSpec.songId.fontSize, 'old');
|
||||
ctx.fillStyle = songRowSpec.songId.color;
|
||||
ctx.fillText(`ID:${song.songId}`, songRowSpec.songId.x, songRowSpec.songId.y);
|
||||
|
||||
//难度,高度为meta*10像素
|
||||
/**
|
||||
@ -130,9 +143,10 @@ async function drawSongInEventStageSongHorizontal(
|
||||
ctx.fillStyle = difficultyColorList[difficultyId];
|
||||
ctx.fillRect(
|
||||
0,
|
||||
jacketImageHeight - meta * 10,
|
||||
jacketImageHeight -
|
||||
meta * BANGDREAM_EVENT_STAGE_SPEC.songRow.difficultyHeightScale,
|
||||
jacketImageHeight,
|
||||
meta * 10,
|
||||
meta * BANGDREAM_EVENT_STAGE_SPEC.songRow.difficultyHeightScale,
|
||||
);
|
||||
return canvas;
|
||||
}
|
||||
@ -145,7 +159,11 @@ async function drawSongInEventStageSongHorizontal(
|
||||
}
|
||||
|
||||
const difficultyLineGraph = stackImageHorizontal(difficultyLineGraphList);
|
||||
ctx.drawImage(difficultyLineGraph, 3, 0);
|
||||
ctx.drawImage(
|
||||
difficultyLineGraph,
|
||||
songRowSpec.jacketInsetX,
|
||||
songRowSpec.jacketY,
|
||||
);
|
||||
}
|
||||
|
||||
return canvas;
|
||||
@ -165,13 +183,14 @@ export async function drawEventStageSongHorizontal(
|
||||
//绘制活动中的歌曲列表(横向)
|
||||
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');
|
||||
for (let i = 0; i < songIdList.length; i++) {
|
||||
const song = new Song(songIdList[i]);
|
||||
ctx.drawImage(
|
||||
await drawSongInEventStageSongHorizontal(song, meta),
|
||||
(800 / 8) * i,
|
||||
getEventStageSongCellWidth() * i,
|
||||
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