From e1ace51d6e99f6393918396ec332471dfc05337e Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 7 Jun 2026 04:22:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20BangDream=20?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E5=8D=A1=E7=89=8C=E5=88=97=E8=A1=A8=E8=A7=84?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot-bangdream-tsugu-global-refactor-plan.md | 1 + .../render-blocks/list-card-icon-list.ts | 39 +++------ .../render-blocks/list-card-icon-spec.ts | 84 +++++++++++++++++++ .../tsugu/list-card-icon-spec.spec.ts | 71 ++++++++++++++++ 4 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/list-card-icon-spec.spec.ts diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index ce4c8b1..461633a 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -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 和时间/档线规则 diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-list.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-list.ts index 1201771..bd18149 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-list.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-list.ts @@ -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 = []; for (let i = 0; i < cardList.length; i++) { const element: Card = cardList[i]; diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec.ts new file mode 100644 index 0000000..e803032 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-card-icon-spec.ts @@ -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( + cards: T[], +): T[] { + return cards.sort(compareCardIconListCards); +} diff --git a/test/qqbot/plugins/bangDream/tsugu/list-card-icon-spec.spec.ts b/test/qqbot/plugins/bangDream/tsugu/list-card-icon-spec.spec.ts new file mode 100644 index 0000000..d9b68ca --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/list-card-icon-spec.spec.ts @@ -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); + }); +});