refactor: 收口 BangDream 乐队编成等级资源

This commit is contained in:
sunlei 2026-06-07 03:29:48 +08:00
parent 5b0d2f754d
commit 7a9d808f8b
4 changed files with 129 additions and 17 deletions

View File

@ -311,6 +311,7 @@ export interface TsuguHook {
- 已新增 `models/item-resource-repository.ts`,把 `Item` 的道具缩略图路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository`item-resource-repository.spec.ts` 覆盖 material、star 和 common item 三类路径,本地 `/查卡池 259` 图片 smoke 输出非空。
- 已新增 `models/cutoff-event-top-repository.ts``render-blocks/player-ranking-resource-repository.ts`,把前十榜 eventtop 数据请求与排名徽章素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFileCache` 收口到 provider-backed repository相关 spec 覆盖 eventtop query path、排名徽章路径和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
- 已新增 `models/degree-resource-repository.ts`,把 `Degree` 的称号缩略图、称号框、称号图标、动态称号脚本和纹理素材路径从 `bestdoriUrl + downloadFile/downloadFileCache` 收口到 provider-backed repository`degree-resource-repository.spec.ts` 覆盖旧/新缩略图 fallback、动态称号旧纹理白名单和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
- 已新增 `render-blocks/deck-rank-resource-repository.ts`把玩家详情“乐队编成等级”Rank 图片从 `list-band-detail.ts` 的本地路径和 Bestdori URL 直拼收口到 repository渲染层只负责 `Buffer -> Image``deck-rank-resource-repository.spec.ts` 覆盖本地素材优先、远端 `/res/icon/*.png` 兜底和 provider 调用,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。
- 固化Windows 下不要用反斜杠测试路径直接调用 Jest pattern容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/<file>.spec.ts ...`,路径用正斜杠。
### Phase 4搜索 specification 和 matcher

View File

@ -0,0 +1,51 @@
import { existsSync, readFileSync } from 'node:fs';
import * as path from 'node:path';
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
export class DeckRankResourceRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
private readonly localRankRootPath: string = path.join(
assetsRootPath,
'Rank',
),
) {}
/**
*
*
* @param rankImageName - Rank
*/
getLocalRankImagePath(rankImageName: string): string {
return path.join(this.localRankRootPath, `${rankImageName}.png`);
}
/**
*
*
* @param rankImageName - Rank
*/
getRemoteRankImagePath(rankImageName: string): string {
return `/res/icon/${rankImageName}.png`;
}
/**
* 退 Bestdori
*
* @param rankImageName - Rank
*/
async getRankImageBuffer(rankImageName: string): Promise<Buffer> {
const localImagePath = this.getLocalRankImagePath(rankImageName);
if (existsSync(localImagePath)) {
return readFileSync(localImagePath);
}
return await this.provider.getAsset(
this.getRemoteRankImagePath(rankImageName),
);
}
}
export const deckRankResourceRepository = new DeckRankResourceRepository();

View File

@ -1,19 +1,16 @@
import { Player } from '@/qqbot/plugins/bangDream/tsugu/models/player';
import { Canvas, Image } from 'skia-canvas';
import { Canvas, Image, loadImage } from 'skia-canvas';
import { drawList } from './list-frame';
import { resizeImage } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band';
import { drawTextWithImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import { starList } from './list-rarity';
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import * as path from 'path';
import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils';
import {
BANGDREAM_DECK_TOTAL_RATING_ID,
BANGDREAM_STAGE_CHALLENGE_BAND_ID,
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
import { deckRankResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/deck-rank-resource-repository';
interface drawBandDetailsInListOptions {
[bandId: number]: Array<Canvas | Image | string>;
@ -114,7 +111,7 @@ export async function drawPlayerStageChallengeRankInList(
}
//画玩家信息内乐队卡组最高等级
const rankImage: { [rankImageName: string]: Image } = {};
const rankImageCache: { [rankImageName: string]: Image } = {};
/**
* Rank图片
*
@ -122,18 +119,12 @@ const rankImage: { [rankImageName: string]: Image } = {};
* @returns
*/
async function loadRankImage(rankImageName: string): Promise<Image> {
if (rankImage[rankImageName] == undefined) {
try {
rankImage[rankImageName] = await loadImageFromPath(
path.join(assetsRootPath, `/Rank/${rankImageName}.png`),
);
} catch {
rankImage[rankImageName] = await loadImageFromPath(
`${bestdoriUrl}/res/icon/${rankImageName}.png`,
);
}
if (rankImageCache[rankImageName] == undefined) {
const rankImageBuffer =
await deckRankResourceRepository.getRankImageBuffer(rankImageName);
rankImageCache[rankImageName] = await loadImage(rankImageBuffer);
}
return rankImage[rankImageName];
return rankImageCache[rankImageName];
}
/**

View File

@ -0,0 +1,69 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import * as path from 'node:path';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { DeckRankResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/deck-rank-resource-repository';
function createProviderMock(): jest.Mocked<BangDreamDataProvider> {
return {
getAsset: jest.fn(),
getJson: jest.fn(),
getTracker: jest.fn(),
name: 'MockBestdori',
resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`),
};
}
describe('BangDream deck rank resource repository', () => {
it('builds local and remote rank image paths', () => {
const repository = new DeckRankResourceRepository(
createProviderMock(),
'D:/KT/assets/Rank',
);
expect(repository.getLocalRankImagePath('rank_4_1')).toBe(
path.join('D:/KT/assets/Rank', 'rank_4_1.png'),
);
expect(repository.getRemoteRankImagePath('rank_4_1')).toBe(
'/res/icon/rank_4_1.png',
);
});
it('uses local rank image buffers before remote fallback', async () => {
const provider = createProviderMock();
const localRootPath = mkdtempSync(path.join(tmpdir(), 'kt-rank-'));
const localImagePath = path.join(localRootPath, 'rank_1.png');
const localBuffer = Buffer.from('local-rank');
writeFileSync(localImagePath, localBuffer);
const repository = new DeckRankResourceRepository(
provider,
localRootPath,
);
try {
await expect(repository.getRankImageBuffer('rank_1')).resolves.toEqual(
localBuffer,
);
expect(provider.getAsset).not.toHaveBeenCalled();
} finally {
rmSync(localRootPath, { force: true, recursive: true });
}
});
it('downloads rank image buffers through the provider when local asset is missing', async () => {
const provider = createProviderMock();
const remoteBuffer = Buffer.from('remote-rank');
provider.getAsset.mockResolvedValue(remoteBuffer);
const repository = new DeckRankResourceRepository(
provider,
path.join(tmpdir(), 'missing-rank-root'),
);
await expect(repository.getRankImageBuffer('rank_7')).resolves.toBe(
remoteBuffer,
);
expect(provider.getAsset).toHaveBeenCalledWith('/res/icon/rank_7.png');
});
});