refactor: 收口 BangDream 通用卡牌列表规格

This commit is contained in:
sunlei 2026-06-07 04:22:48 +08:00
parent 322938ae6f
commit e1ace51d6e
4 changed files with 169 additions and 26 deletions

View File

@ -386,6 +386,7 @@ export interface TsuguHook {
- 线上 smoke 暴露 `/查试炼 310 -m` 将所有列横向拼成一张巨大 canvas 会触发 Pod `OOMKilled exit=137`;已改为按小批次绘制 stage、边分列边输出多张 CQ 图片,不再创建最终横向巨图,普通版和 meta 版均拆成 5 张输出,同时避免完全串行导致冷缓存首条命令耗时过长。
- 已新增 `event-stage-spec.spec.ts`,覆盖试炼歌曲行尺寸、类型顶部文字规格、换列判断和按列高拆分算法;本地生成 `event-stage-split-310*.jpg`、`event-stage-split-310-meta*.jpg`,验证 `/查试炼 310``/查试炼 310 -m` 图片输出正常。
- 已新增 `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 输出非空。
- smoke/Jenkins/远程调试卡点继续按已固化规则处理同一卡点第二次尝试前必须改变可验证变量Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录PowerShell 双引号 here-string 中不要写 JS `${...}` 模板字面量,避免被 PowerShell 提前插值smoke 没有真实图片落盘时必须失败。
### Phase 6策略 policy 和时间/档线规则

View File

@ -2,7 +2,12 @@ import { drawList } from './list-frame';
import { Canvas } from 'skia-canvas';
import { drawCardIcon } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/card-art';
import { Card } from '@/qqbot/plugins/bangDream/tsugu/models/card';
import { BANGDREAM_CARD_PRIORITY_TYPES } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
import {
BANGDREAM_CARD_ICON_LIST_SPEC,
getCardIconListSpacing,
getCardIconListTextSize,
sortCardIconListCards,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec';
interface CardIconInListOptions {
key?: string;
@ -22,33 +27,15 @@ interface CardIconInListOptions {
export async function drawCardListInList({
key,
cardList,
cardIdVisible = false,
skillTypeVisible = true,
cardTypeVisible = true,
cardIdVisible = BANGDREAM_CARD_ICON_LIST_SPEC.card.showCardId,
skillTypeVisible = BANGDREAM_CARD_ICON_LIST_SPEC.card.showSkillType,
cardTypeVisible = BANGDREAM_CARD_ICON_LIST_SPEC.card.showCardType,
trainingStatus,
lineHeight = 200,
lineHeight = BANGDREAM_CARD_ICON_LIST_SPEC.list.defaultLineHeight,
}: CardIconInListOptions) {
//cardList排序稀有度高的在前面其中cardId低的在前面
const typeList: readonly string[] = BANGDREAM_CARD_PRIORITY_TYPES;
cardList.sort((a, b) => {
if (a.rarity == b.rarity) {
if (typeList.includes(a.type) && !typeList.includes(b.type)) {
return -1;
} else if (!typeList.includes(a.type) && typeList.includes(b.type)) {
return 1;
} else if (
typeList.indexOf(a.type) != -1 &&
typeList.indexOf(b.type) != -1
) {
return typeList.indexOf(a.type) - typeList.indexOf(b.type);
}
return a.cardId - b.cardId;
}
return b.rarity - a.rarity;
});
const textSize = (lineHeight / 200) * 180;
const spacing = (lineHeight / 200) * 13;
sortCardIconListCards(cardList);
const textSize = getCardIconListTextSize(lineHeight);
const spacing = getCardIconListSpacing(lineHeight);
const list: Array<Canvas> = [];
for (let i = 0; i < cardList.length; i++) {
const element: Card = cardList[i];

View File

@ -0,0 +1,84 @@
import { BANGDREAM_CARD_PRIORITY_TYPES } from '@/qqbot/plugins/bangDream/tsugu/runtime/runtime-options';
export interface CardIconListSortTarget {
cardId: number;
rarity: number;
type: string;
}
export const BANGDREAM_CARD_ICON_LIST_SPEC = {
card: {
priorityTypes: BANGDREAM_CARD_PRIORITY_TYPES,
showCardId: false,
showCardType: true,
showSkillType: true,
},
list: {
defaultLineHeight: 200,
spacingRatio: 13 / 200,
textSizeRatio: 180 / 200,
},
} as const;
/**
*
*
* @param lineHeight -
*/
export function getCardIconListTextSize(lineHeight: number) {
return lineHeight * BANGDREAM_CARD_ICON_LIST_SPEC.list.textSizeRatio;
}
/**
*
*
* @param lineHeight -
*/
export function getCardIconListSpacing(lineHeight: number) {
return lineHeight * BANGDREAM_CARD_ICON_LIST_SPEC.list.spacingRatio;
}
/**
*
*
* @param left -
* @param right -
*/
export function compareCardIconListCards(
left: CardIconListSortTarget,
right: CardIconListSortTarget,
) {
if (left.rarity !== right.rarity) {
return right.rarity - left.rarity;
}
const priorityTypes = BANGDREAM_CARD_ICON_LIST_SPEC.card
.priorityTypes as readonly string[];
const leftTypeIndex = priorityTypes.indexOf(left.type);
const rightTypeIndex = priorityTypes.indexOf(right.type);
const leftHasPriority = leftTypeIndex !== -1;
const rightHasPriority = rightTypeIndex !== -1;
if (leftHasPriority && !rightHasPriority) {
return -1;
}
if (!leftHasPriority && rightHasPriority) {
return 1;
}
if (leftHasPriority && rightHasPriority) {
return leftTypeIndex - rightTypeIndex;
}
return left.cardId - right.cardId;
}
/**
*
*
* @param cards -
*/
export function sortCardIconListCards<T extends CardIconListSortTarget>(
cards: T[],
): T[] {
return cards.sort(compareCardIconListCards);
}

View File

@ -0,0 +1,71 @@
import { BangDreamCardType } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
import {
BANGDREAM_CARD_ICON_LIST_SPEC,
compareCardIconListCards,
getCardIconListSpacing,
getCardIconListTextSize,
sortCardIconListCards,
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec';
describe('BangDream card icon list spec', () => {
it('keeps card icon list text and spacing ratios stable', () => {
const lineHeight = BANGDREAM_CARD_ICON_LIST_SPEC.list.defaultLineHeight;
expect(lineHeight).toBe(200);
expect(getCardIconListTextSize(lineHeight)).toBe(180);
expect(getCardIconListSpacing(lineHeight)).toBe(13);
});
it('sorts higher rarity cards before lower rarity cards', () => {
const cards = [
{ cardId: 1, rarity: 3, type: BangDreamCardType.permanent },
{ cardId: 2, rarity: 5, type: BangDreamCardType.permanent },
{ cardId: 3, rarity: 4, type: BangDreamCardType.permanent },
];
expect(sortCardIconListCards(cards).map((card) => card.cardId)).toEqual([
2, 3, 1,
]);
});
it('keeps priority card types before normal cards at the same rarity', () => {
const cards = [
{ cardId: 1, rarity: 5, type: BangDreamCardType.permanent },
{ cardId: 2, rarity: 5, type: BangDreamCardType.limited },
{ cardId: 3, rarity: 5, type: BangDreamCardType.dreamfes },
{ cardId: 4, rarity: 5, type: BangDreamCardType.kirafes },
{ cardId: 5, rarity: 5, type: BangDreamCardType.birthday },
];
expect(sortCardIconListCards(cards).map((card) => card.cardId)).toEqual([
4, 3, 2, 5, 1,
]);
});
it('sorts normal cards by card id when rarity is the same', () => {
const cards = [
{ cardId: 10, rarity: 4, type: BangDreamCardType.event },
{ cardId: 8, rarity: 4, type: BangDreamCardType.permanent },
{ cardId: 9, rarity: 4, type: BangDreamCardType.initial },
];
expect(sortCardIconListCards(cards).map((card) => card.cardId)).toEqual([
8, 9, 10,
]);
});
it('exposes the same comparison result used by in-place sorting', () => {
const limited = {
cardId: 1,
rarity: 5,
type: BangDreamCardType.limited,
};
const permanent = {
cardId: 2,
rarity: 5,
type: BangDreamCardType.permanent,
};
expect(compareCardIconListCards(limited, permanent)).toBeLessThan(0);
});
});