refactor: 收口 BangDream 难度列表规格

This commit is contained in:
sunlei 2026-06-07 04:43:03 +08:00
parent b360a005e7
commit ef0f571ae8
4 changed files with 196 additions and 20 deletions

View File

@ -388,6 +388,7 @@ export interface TsuguHook {
- 已新增 `render-blocks/list-player-card-icon-spec.ts`,收口玩家详情主卡组展示顺序、默认行高、文本字号比例、卡牌间距比例和卡牌图标可见性标记;`list-player-card-icon-list.ts` 改为消费 spec主卡组渲染顺序和输出结构保持不变`list-player-card-icon-spec.spec.ts` 覆盖历史卡牌顺序、缺失条目跳过和字号/间距计算,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。 - 已新增 `render-blocks/list-player-card-icon-spec.ts`,收口玩家详情主卡组展示顺序、默认行高、文本字号比例、卡牌间距比例和卡牌图标可见性标记;`list-player-card-icon-list.ts` 改为消费 spec主卡组渲染顺序和输出结构保持不变`list-player-card-icon-spec.spec.ts` 覆盖历史卡牌顺序、缺失条目跳过和字号/间距计算,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。
- 已新增 `render-blocks/list-card-icon-spec.ts`,收口通用卡牌图标列表默认行高、文本字号比例、卡牌间距比例、默认可见性标记和历史排序比较器;`list-card-icon-list.ts` 改为消费 spec卡牌图标列表排序和输出结构保持不变`list-card-icon-spec.spec.ts` 覆盖稀有度排序、优先卡牌类型排序、普通卡按 ID 排序和字号/间距计算,本地 `/查卡 472` 图片 smoke 输出非空。 - 已新增 `render-blocks/list-card-icon-spec.ts`,收口通用卡牌图标列表默认行高、文本字号比例、卡牌间距比例、默认可见性标记和历史排序比较器;`list-card-icon-list.ts` 改为消费 spec卡牌图标列表排序和输出结构保持不变`list-card-icon-spec.spec.ts` 覆盖稀有度排序、优先卡牌类型排序、普通卡按 ID 排序和字号/间距计算,本地 `/查卡 472` 图片 smoke 输出非空。
- 已新增 `render-blocks/list-song-spec.ts`,收口歌曲列表单行封面、文本、难度块位置、列表内容宽度、分割线高度和外层行高计算;`list-song.ts` 改为消费 spec歌曲列表绘制顺序和输出结构保持不变`list-song-spec.spec.ts` 覆盖单行尺寸、难度块垂直居中和歌曲组列表尺寸,本地 `/查曲 136` 图片 smoke 输出非空。 - 已新增 `render-blocks/list-song-spec.ts`,收口歌曲列表单行封面、文本、难度块位置、列表内容宽度、分割线高度和外层行高计算;`list-song.ts` 改为消费 spec歌曲列表绘制顺序和输出结构保持不变`list-song-spec.spec.ts` 覆盖单行尺寸、难度块垂直居中和歌曲组列表尺寸,本地 `/查曲 136` 图片 smoke 输出非空。
- 已新增 `render-blocks/list-difficulty-spec.ts`,收口难度列表默认高度/间距、徽章兜底色、圆形布局和等级文字比例/居中计算;`list-difficulty.ts` 改为消费 spec难度列表绘制顺序和输出结构保持不变`list-difficulty-spec.spec.ts` 覆盖列表宽度、条目偏移、徽章颜色、圆形布局和文字居中,本地 `/查曲 136` 图片 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 和时间/档线规则

View File

@ -0,0 +1,116 @@
interface ImageLike {
height: number;
width: number;
}
export const BANGDREAM_DIFFICULTY_LIST_SPEC = {
badge: {
arcStart: 0,
fallbackColor: '#f1f1f1',
fullCircleRadian: Math.PI * 2,
textMaxWidthRatio: 3,
textSizeRatio: 2 / 3,
},
list: {
defaultImageHeight: 60,
defaultSpacing: 10,
},
} as const;
/**
*
*
* @param difficultyCount -
* @param imageHeight -
* @param spacing -
*/
export function getDifficultyListCanvasWidth(
difficultyCount: number,
imageHeight: number,
spacing: number,
) {
return imageHeight * difficultyCount + (difficultyCount - 1) * spacing;
}
/**
*
*
* @param index -
* @param imageHeight -
* @param spacing -
*/
export function getDifficultyListItemX(
index: number,
imageHeight: number,
spacing: number,
) {
return index * (imageHeight + spacing);
}
/**
*
*
* @param difficultyType -
* @param colors -
*/
export function getDifficultyBadgeColor(
difficultyType: number,
colors: ReadonlyArray<string | undefined>,
) {
return (
colors[difficultyType] ??
BANGDREAM_DIFFICULTY_LIST_SPEC.badge.fallbackColor
);
}
/**
*
*
* @param imageHeight -
*/
export function createDifficultyBadgeLayout(imageHeight: number) {
return {
arcEnd: BANGDREAM_DIFFICULTY_LIST_SPEC.badge.fullCircleRadian,
arcRadius: imageHeight / 2,
arcStart: BANGDREAM_DIFFICULTY_LIST_SPEC.badge.arcStart,
arcX: imageHeight / 2,
arcY: imageHeight / 2,
canvasHeight: imageHeight,
canvasWidth: imageHeight,
};
}
/**
*
*
* @param imageHeight -
* @param playLevel -
*/
export function createDifficultyLevelTextSpec(
imageHeight: number,
playLevel: number,
) {
return {
maxWidth:
imageHeight * BANGDREAM_DIFFICULTY_LIST_SPEC.badge.textMaxWidthRatio,
text: playLevel.toString(),
textSize:
imageHeight * BANGDREAM_DIFFICULTY_LIST_SPEC.badge.textSizeRatio,
};
}
/**
*
*
* @param imageHeight -
* @param levelText -
*/
export function getDifficultyLevelTextPosition(
imageHeight: number,
levelText: ImageLike,
) {
return {
x: imageHeight / 2 - levelText.width / 2,
y: imageHeight / 2 - levelText.height / 2,
};
}

View File

@ -2,6 +2,15 @@ import { Canvas } from 'skia-canvas';
import { Song } from '@/qqbot/plugins/bangDream/tsugu/models/song'; import { Song } from '@/qqbot/plugins/bangDream/tsugu/models/song';
import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import { difficultyColorList } from '@/qqbot/plugins/bangDream/tsugu/models/song'; import { difficultyColorList } from '@/qqbot/plugins/bangDream/tsugu/models/song';
import {
BANGDREAM_DIFFICULTY_LIST_SPEC,
createDifficultyBadgeLayout,
createDifficultyLevelTextSpec,
getDifficultyBadgeColor,
getDifficultyLevelTextPosition,
getDifficultyListCanvasWidth,
getDifficultyListItemX,
} from './list-difficulty-spec';
/** /**
* *
@ -13,12 +22,13 @@ import { difficultyColorList } from '@/qqbot/plugins/bangDream/tsugu/models/song
*/ */
export function drawDifficultyList( export function drawDifficultyList(
song: Song, song: Song,
imageHeight: number = 60, imageHeight: number = BANGDREAM_DIFFICULTY_LIST_SPEC.list
spacing: number = 10, .defaultImageHeight,
spacing: number = BANGDREAM_DIFFICULTY_LIST_SPEC.list.defaultSpacing,
): Canvas { ): Canvas {
const difficultyCount = Object.keys(song.difficulty).length; const difficultyCount = Object.keys(song.difficulty).length;
const canvas = new Canvas( const canvas = new Canvas(
imageHeight * difficultyCount + (difficultyCount - 1) * spacing, getDifficultyListCanvasWidth(difficultyCount, imageHeight, spacing),
imageHeight, imageHeight,
); );
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -26,7 +36,7 @@ export function drawDifficultyList(
const i = parseInt(d); const i = parseInt(d);
ctx.drawImage( ctx.drawImage(
drawDifficulty(i, song.difficulty[i].playLevel, imageHeight), drawDifficulty(i, song.difficulty[i].playLevel, imageHeight),
i * (imageHeight + spacing), getDifficultyListItemX(i, imageHeight, spacing),
0, 0,
); );
} }
@ -47,22 +57,20 @@ export function drawDifficulty(
) { ) {
const tempCanvas = new Canvas(imageHeight, imageHeight); const tempCanvas = new Canvas(imageHeight, imageHeight);
const ctx = tempCanvas.getContext('2d'); const ctx = tempCanvas.getContext('2d');
if (difficultyColorList[difficultyType] != undefined) { const badgeLayout = createDifficultyBadgeLayout(imageHeight);
ctx.fillStyle = difficultyColorList[difficultyType]; ctx.fillStyle = getDifficultyBadgeColor(difficultyType, difficultyColorList);
} else { ctx.arc(
ctx.fillStyle = '#f1f1f1'; badgeLayout.arcX,
} badgeLayout.arcY,
ctx.arc(imageHeight / 2, imageHeight / 2, imageHeight / 2, 0, 2 * Math.PI); badgeLayout.arcRadius,
ctx.fill(); badgeLayout.arcStart,
const levelText = drawText({ badgeLayout.arcEnd,
textSize: (imageHeight / 3) * 2,
text: playLevel.toString(),
maxWidth: imageHeight * 3,
});
ctx.drawImage(
levelText,
imageHeight / 2 - levelText.width / 2,
imageHeight / 2 - levelText.height / 2,
); );
ctx.fill();
const levelText = drawText(
createDifficultyLevelTextSpec(imageHeight, playLevel),
);
const position = getDifficultyLevelTextPosition(imageHeight, levelText);
ctx.drawImage(levelText, position.x, position.y);
return tempCanvas; return tempCanvas;
} }

View File

@ -0,0 +1,51 @@
import {
BANGDREAM_DIFFICULTY_LIST_SPEC,
createDifficultyBadgeLayout,
createDifficultyLevelTextSpec,
getDifficultyBadgeColor,
getDifficultyLevelTextPosition,
getDifficultyListCanvasWidth,
getDifficultyListItemX,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-difficulty-spec';
describe('BangDream difficulty list spec', () => {
it('keeps default difficulty list dimensions stable', () => {
expect(BANGDREAM_DIFFICULTY_LIST_SPEC.list.defaultImageHeight).toBe(60);
expect(BANGDREAM_DIFFICULTY_LIST_SPEC.list.defaultSpacing).toBe(10);
expect(getDifficultyListCanvasWidth(5, 60, 10)).toBe(340);
expect(getDifficultyListItemX(3, 60, 10)).toBe(210);
});
it('keeps difficulty badge color fallback stable', () => {
expect(getDifficultyBadgeColor(1, ['#111111', '#222222'])).toBe('#222222');
expect(getDifficultyBadgeColor(6, ['#111111', '#222222'])).toBe(
'#f1f1f1',
);
});
it('keeps difficulty badge circle layout stable', () => {
expect(createDifficultyBadgeLayout(45)).toEqual({
arcEnd: Math.PI * 2,
arcRadius: 22.5,
arcStart: 0,
arcX: 22.5,
arcY: 22.5,
canvasHeight: 45,
canvasWidth: 45,
});
});
it('keeps difficulty level text layout stable', () => {
expect(createDifficultyLevelTextSpec(45, 26)).toEqual({
maxWidth: 135,
text: '26',
textSize: 30,
});
expect(getDifficultyLevelTextPosition(45, { height: 12, width: 20 })).toEqual(
{
x: 12.5,
y: 16.5,
},
);
});
});