diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 01b855d..31080ef 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -339,6 +339,13 @@ export interface TsuguHook { - 关键图片命令输出非空,尺寸在既有范围。 - 视觉 token 修改只改 theme/spec 文件。 +当前进度: + +- 已新增 `render-blocks/theme.ts`,先收口公共文字颜色、分割线颜色、简易背景色、图表背景/文字色和默认字体名。 +- 已新增 `render-blocks/layout-spec.ts`,统一横向/纵向虚线分割规格,歌曲列表、活动列表、活动详情和通用列表框架不再重复维护分割线宽高/颜色。 +- 已新增 `runtime/asset-manifest.ts`,收口本地 `BG`、`Card`、`Skill`、`SongChart`、字体和标题资源路径;`canvas/text.ts`、`canvas/rect.ts`、`canvas/output.ts`、`canvas/background.ts`、`card-art.ts`、`list-rarity.ts`、`skill-text.ts`、`title.ts`、`song-chart-preview.ts` 已改走 manifest。 +- 已生成查曲、查活动和查谱面 smoke 图片,验证 theme/spec/manifest 第一段迁移后本地图片输出非空,谱面预览资源路径未断。 + ### Phase 6:策略 policy 和时间/档线规则 目标:服务器优先级、国服预估、档位、时区和活动状态从工具函数变成可测试规则。 diff --git a/src/qqbot/plugins/bangDream/tsugu/canvas/background.ts b/src/qqbot/plugins/bangDream/tsugu/canvas/background.ts index 81e6877..5f25e8b 100644 --- a/src/qqbot/plugins/bangDream/tsugu/canvas/background.ts +++ b/src/qqbot/plugins/bangDream/tsugu/canvas/background.ts @@ -2,9 +2,9 @@ import { createBlurredTrianglePattern } from '@/qqbot/plugins/bangDream/tsugu/ca import { scatterImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-star-scatter'; import { drawTextOnCanvas } from '@/qqbot/plugins/bangDream/tsugu/canvas/background-text'; import { loadImage, Image, Canvas } from 'skia-canvas'; -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 { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; interface BackgroundOptions { image?: Image | Canvas | any; @@ -118,14 +118,10 @@ let defaultBGTexture: Image; * 在底层绘图工具层中加载图片Once。 */ async function loadImageOnce() { - star.push( - await loadImageFromPath(path.join(assetsRootPath, '/BG/star1.png')), - ); - star.push( - await loadImageFromPath(path.join(assetsRootPath, '/BG/star2.png')), - ); + star.push(await loadImageFromPath(getBangDreamAssetPath('backgroundStar1'))); + star.push(await loadImageFromPath(getBangDreamAssetPath('backgroundStar2'))); defaultBGTexture = await loadImageFromPath( - path.join(assetsRootPath, '/BG/bg_object_big.png'), + getBangDreamAssetPath('backgroundObjectBig'), ); } loadImageOnce(); @@ -136,7 +132,7 @@ loadImageOnce(); * @param options1 - options1参数。 */ export async function createEasyBackground({ width, height }) { - const bgColor = '#fef3ef'; + const bgColor = BANGDREAM_RENDER_THEME.color.backgroundEasy; const canvas: Canvas = new Canvas(width, height); const ctx = canvas.getContext('2d'); ctx.fillStyle = bgColor; diff --git a/src/qqbot/plugins/bangDream/tsugu/canvas/output.ts b/src/qqbot/plugins/bangDream/tsugu/canvas/output.ts index 502e0c1..7dea89c 100644 --- a/src/qqbot/plugins/bangDream/tsugu/canvas/output.ts +++ b/src/qqbot/plugins/bangDream/tsugu/canvas/output.ts @@ -4,9 +4,8 @@ import { createEasyBackground, createImageBackground, } from '@/qqbot/plugins/bangDream/tsugu/canvas/background'; -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 { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; let BGDefaultImage: Image; /** @@ -14,7 +13,7 @@ let BGDefaultImage: Image; */ async function loadImageOnce() { BGDefaultImage = await loadImageFromPath( - path.join(assetsRootPath, '/BG/live.png'), + getBangDreamAssetPath('backgroundLive'), ); } loadImageOnce(); diff --git a/src/qqbot/plugins/bangDream/tsugu/canvas/rect.ts b/src/qqbot/plugins/bangDream/tsugu/canvas/rect.ts index 23890fc..ed787be 100644 --- a/src/qqbot/plugins/bangDream/tsugu/canvas/rect.ts +++ b/src/qqbot/plugins/bangDream/tsugu/canvas/rect.ts @@ -1,7 +1,9 @@ import { Canvas, FontLibrary } from 'skia-canvas'; -import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { getTextWidth } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; -FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]); +import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; + +FontLibrary.use('old', [getBangDreamAssetPath('fontOld')]); interface RoundedRect { width: number; @@ -24,7 +26,7 @@ export function drawRoundedRect({ width, height, radius = 25, - color = '#ffffff', + color = BANGDREAM_RENDER_THEME.color.surface, opacity = 0.9, strokeColor = '#bbbbbb', strokeWidth = 0, @@ -119,14 +121,14 @@ interface RoundedRectWithText { */ export function drawRoundedRectWithText({ text, - font = 'old', - textColor = '#ffffff', + font = BANGDREAM_RENDER_THEME.font.body, + textColor = BANGDREAM_RENDER_THEME.color.surface, textSize, textAlign = 'center', height = (textSize * 4) / 3, width = getTextWidth(text, textSize, font) + height, radius = height / 2, - color = '#5b5b5b', + color = BANGDREAM_RENDER_THEME.color.labelBackground, opacity = 1, strokeColor = color, strokeWidth = 0, @@ -144,7 +146,7 @@ export function drawRoundedRectWithText({ ctx.fillStyle = textColor; ctx.textBaseline = 'alphabetic'; - ctx.font = `${textSize}px old,Microsoft Yahei`; + ctx.font = `${textSize}px ${font},${BANGDREAM_RENDER_THEME.font.fallback}`; let x = 0, y = 0; diff --git a/src/qqbot/plugins/bangDream/tsugu/canvas/text.ts b/src/qqbot/plugins/bangDream/tsugu/canvas/text.ts index bb15587..971b494 100644 --- a/src/qqbot/plugins/bangDream/tsugu/canvas/text.ts +++ b/src/qqbot/plugins/bangDream/tsugu/canvas/text.ts @@ -4,10 +4,12 @@ import { Canvas, CanvasRenderingContext2D, } from 'skia-canvas'; -import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; -FontLibrary.use('old', [`${assetsRootPath}/Fonts/old.ttf`]); +import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; + +FontLibrary.use('old', [getBangDreamAssetPath('fontOld')]); FontLibrary.use('FangZhengHeiTi', [ - `${assetsRootPath}/Fonts/FangZhengHeiTi_GBK.ttf`, + getBangDreamAssetPath('fontFangZhengHeiTi'), ]); interface WrapTextOptions { @@ -31,8 +33,8 @@ export function drawText({ textSize = 40, maxWidth, lineHeight = (textSize * 4) / 3, - color = '#505050', - font = 'old', + color = BANGDREAM_RENDER_THEME.color.primaryText, + font = BANGDREAM_RENDER_THEME.font.body, }: WrapTextOptions): Canvas { const wrappedTextData = wrapText({ text, maxWidth, lineHeight, textSize }); let canvas: Canvas; @@ -131,8 +133,8 @@ export function drawTextWithImages({ lineHeight = (textSize * 4) / 3, content, spacing = textSize / 3, - color = '#505050', - font = 'old', + color = BANGDREAM_RENDER_THEME.color.primaryText, + font = BANGDREAM_RENDER_THEME.font.body, }: TextWithImagesOptions) { const wrappedTextData = wrapTextWithImages({ textSize, @@ -305,5 +307,5 @@ export const setFontStyle = function ( font: string, ) { //设置字体大小 - ctx.font = textSize + 'px ' + font + ',Microsoft Yahei'; + ctx.font = `${textSize}px ${font},${BANGDREAM_RENDER_THEME.font.fallback}`; }; diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts index 54e041a..cc389cd 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts @@ -36,18 +36,12 @@ import { drawSongListInList, } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; +import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; -const songSeparatorLine = drawDottedLine({ - width: 800, - height: 10, - startX: 5, - startY: 5, - endX: 795, - endY: 5, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +const songSeparatorLine = drawDottedLine( + createHorizontalSeparatorSpec({ height: 10 }), +); /** * 在QQBot 图片视图层中绘制歌曲列表数据块。 @@ -393,7 +387,7 @@ export async function drawEventDetail( const eventBannerImage = await event.getBannerImage(); const eventBannerImageCanvas = drawBannerImageCanvas(eventBannerImage); list.push(eventBannerImageCanvas); - list.push(new Canvas(800, 30)); + list.push(new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 30)); //标题 list.push( diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-list.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-list.ts index a2c4eaf..f950d70 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-list.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-list.ts @@ -38,22 +38,15 @@ import { statConfig } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-s import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher'; import { eventRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-repository'; +import { createVerticalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; const maxHeight = 7000; const maxColumns = 7; //表格用默认虚线 -export const line2: Canvas = drawDottedLine({ - width: 30, - height: 7000, - startX: 5, - startY: 0, - endX: 15, - endY: 6995, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +export const line2: Canvas = drawDottedLine( + createVerticalSeparatorSpec(7000, { startX: 5 }), +); /** * 在QQBot 图片视图层中绘制活动列表。 diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-list.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-list.ts index fc8a372..a42f755 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-list.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/song-list.ts @@ -15,32 +15,16 @@ import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/con import { drawSongDetail } from './song-detail'; import { createTsuguEntityMatcher } from '@/qqbot/plugins/bangDream/tsugu/search/entity-list-matcher'; import { songRepository } from '@/qqbot/plugins/bangDream/tsugu/models/song-repository'; +import { + createHorizontalSeparatorSpec, + createVerticalSeparatorSpec, +} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; // 紧凑化虚线分割 -const line = drawDottedLine({ - width: 800, - height: 10, - startX: 5, - startY: 5, - endX: 795, - endY: 5, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 })); //表格用默认竖向虚线 -const line2: Canvas = drawDottedLine({ - width: 30, - height: 6000, - startX: 10, - startY: 0, - endX: 15, - endY: 5990, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +const line2: Canvas = drawDottedLine(createVerticalSeparatorSpec(6000)); /** * 在QQBot 图片视图层中绘制歌曲列表。 diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/card-art.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/card-art.ts index 4db961b..ec10d99 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/card-art.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/card-art.ts @@ -1,4 +1,3 @@ -import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { setFontStyle } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band'; import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute'; @@ -6,10 +5,11 @@ import { Card } from '@/qqbot/plugins/bangDream/tsugu/models/card'; import { Image, Canvas, loadImage } from 'skia-canvas'; import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client'; import { drawCardIconSkill } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text'; -import * as path from 'path'; import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill'; import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; +import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; const cardTypeIconList: { [type: string]: Image } = {}; const starList: { [type: string]: Image } = {}; @@ -20,25 +20,23 @@ let limitBreakIcon: Image; */ async function loadImageOnce() { cardTypeIconList.limited = await loadImageFromPath( - path.join(assetsRootPath, '/Card/L.png'), + getBangDreamAssetPath('cardLimited'), ); cardTypeIconList.dreamfes = await loadImageFromPath( - path.join(assetsRootPath, '/Card/D.png'), + getBangDreamAssetPath('cardDreamfes'), ); cardTypeIconList.kirafes = await loadImageFromPath( - path.join(assetsRootPath, '/Card/K.png'), + getBangDreamAssetPath('cardKirafes'), ); cardTypeIconList.birthday = await loadImageFromPath( - path.join(assetsRootPath, '/Card/B.png'), - ); - starList.normal = await loadImageFromPath( - path.join(assetsRootPath, '/Card/star.png'), + getBangDreamAssetPath('cardBirthday'), ); + starList.normal = await loadImageFromPath(getBangDreamAssetPath('cardStar')); starList.trained = await loadImageFromPath( - path.join(assetsRootPath, '/Card/star_trained.png'), + getBangDreamAssetPath('cardStarTrained'), ); limitBreakIcon = await loadImageFromPath( - path.join(assetsRootPath, '/Card/limitBreakRank.png'), + getBangDreamAssetPath('cardLimitBreakRank'), ); } @@ -136,17 +134,17 @@ export async function drawCardIcon({ if (cardIdVisible) { ctx.textAlign = 'start'; ctx.textBaseline = 'middle'; - setFontStyle(ctx, 30, 'old'); - ctx.fillStyle = '#a7a7a7'; + setFontStyle(ctx, 30, BANGDREAM_RENDER_THEME.font.body); + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.mutedText; ctx.fillText(`ID:${card.cardId}`, 4, 195); } //如果显示技能类型,在右上显示 if (skillLevel != undefined) { - ctx.fillStyle = '#ff0000'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.skillLevelBackground; ctx.fillRect(138, 91, 35, 39); - ctx.fillStyle = '#ffffff'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface; ctx.textAlign = 'center'; - setFontStyle(ctx, 35, 'old'); + setFontStyle(ctx, 35, BANGDREAM_RENDER_THEME.font.body); ctx.fillText(skillLevel.toString(), 155.5, 107.5); } //如果显示技能类型,在右上显示 @@ -169,8 +167,8 @@ export async function drawCardIcon({ ctx.drawImage(bandIcon, 0, 0, 45, 45); if (limitBreakRank != 0) { ctx.drawImage(limitBreakIcon, 137, 51, 39, 39); - setFontStyle(ctx, 25, 'old'); - ctx.fillStyle = '#ffffff'; + setFontStyle(ctx, 25, BANGDREAM_RENDER_THEME.font.body); + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.surface; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(limitBreakRank.toString(), 155, 70); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks.ts index 53c4243..7fc28b6 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks.ts @@ -47,30 +47,22 @@ import { } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { Canvas, Image } from 'skia-canvas'; import { Band } from '@/qqbot/plugins/bangDream/tsugu/models/band'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; +import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; -const songDetailSeparator = drawDottedLine({ - width: 365, - height: 20, - startX: 5, - startY: 10, - endX: 360, - endY: 10, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +const songDetailSeparator = drawDottedLine( + createHorizontalSeparatorSpec({ + width: 365, + height: 20, + endX: 360, + }), +); -const songMetaSeparator = drawDottedLine({ - width: 800, - height: 10, - startX: 5, - startY: 5, - endX: 795, - endY: 5, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +const songMetaSeparator = drawDottedLine( + createHorizontalSeparatorSpec({ + height: 10, + }), +); /** * 在图片布局层中绘制横幅Info块。 @@ -327,7 +319,7 @@ export async function drawCharacterHalfBlock( await character.initFull(false); const color = character.colorCode ? character.colorCode.toLowerCase() - : '#ffffff'; + : BANGDREAM_RENDER_THEME.color.surface; ctx.drawImage( drawRoundedRect({ width, @@ -382,14 +374,14 @@ export async function drawCharacterHalfBlock( const nameTextImage = drawText({ text: character.characterName[server], textSize: 40, - color: '#ffffff', + color: BANGDREAM_RENDER_THEME.color.surface, maxWidth: width, }); list.push(drawImageListCenter([nameTextImage], width)); const idTextImage = drawText({ text: `ID: ${character.characterId}`, textSize: 30, - color: '#ffffff', + color: BANGDREAM_RENDER_THEME.color.surface, maxWidth: width, }); list.push(drawImageListCenter([idTextImage], width)); @@ -409,13 +401,13 @@ export async function drawPlayerDetailBlockWithIllustration( const list: Array = []; const playerText = drawText({ text: player.profile.userName, - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, textSize: 75, }); list.push(drawImageListCenter([playerText])); const levelText = drawText({ text: `等级 ${player.profile.rank}`, - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, textSize: 35, }); list.push(drawImageListCenter([levelText])); @@ -436,7 +428,7 @@ export async function drawPlayerDetailBlockWithIllustration( const introductionText = drawText({ text: player.profile.introduction, - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, textSize: 35, }); list.push(drawImageListCenter([introductionText])); @@ -447,7 +439,7 @@ export async function drawPlayerDetailBlockWithIllustration( : 'ID未公开'; const idText = drawTextWithImages({ content: [await getIcon(player.server), userId], - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, textSize: 35, }); list.push(drawImageListCenter([idText])); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec.ts new file mode 100644 index 0000000..ba046d7 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec.ts @@ -0,0 +1,57 @@ +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; + +interface SeparatorSpecOptions { + width?: number; + height?: number; + startX?: number; + endX?: number; +} + +/** + * 创建横向虚线分割规格。 + * + * @param options - 分割线宽高和横向端点。 + */ +export function createHorizontalSeparatorSpec({ + width = BANGDREAM_RENDER_THEME.layout.contentWidth, + height = 30, + startX = 5, + endX = width - 5, +}: SeparatorSpecOptions = {}) { + const y = height / 2; + return { + width, + height, + startX, + startY: y, + endX, + endY: y, + radius: 2, + gap: 10, + color: BANGDREAM_RENDER_THEME.color.separator, + }; +} + +/** + * 创建纵向虚线分割规格。 + * + * @param height - 分割线高度。 + * @param options - 纵向端点配置。 + */ +export function createVerticalSeparatorSpec( + height: number, + options: Pick = {}, +) { + const { startX = 10, endX = 15 } = options; + return { + width: 30, + height, + startX, + startY: 0, + endX, + endY: height - 10, + radius: 2, + gap: 10, + color: BANGDREAM_RENDER_THEME.color.separator, + }; +} diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame.ts index 64e4f31..17c7196 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame.ts @@ -12,19 +12,14 @@ import { } from '@/qqbot/plugins/bangDream/tsugu/models/server'; import { stackImageHorizontal } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; +import { + createHorizontalSeparatorSpec, + createVerticalSeparatorSpec, +} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; //表格用默认虚线 -export const line: Canvas = drawDottedLine({ - width: 800, - height: 30, - startX: 5, - startY: 15, - endX: 795, - endY: 15, - radius: 2, - gap: 10, - color: '#a8a8a8', -}); +export const line: Canvas = drawDottedLine(createHorizontalSeparatorSpec()); interface ListOptions { key?: string; @@ -51,8 +46,8 @@ export function drawList({ textSize = 40, lineHeight = textSize * 1.5, spacing = textSize / 3, - color = '#505050', - maxWidth = 800, + color = BANGDREAM_RENDER_THEME.color.primaryText, + maxWidth = BANGDREAM_RENDER_THEME.layout.contentWidth, }: ListOptions): Canvas { const xmax = maxWidth - 40; const keyImage = drawRoundedRectWithText({ @@ -76,14 +71,21 @@ export function drawList({ textImage = new Canvas(0, 0); } if (key == undefined) { - return stackImageHorizontal([new Canvas(20, 1), textImage]); + return stackImageHorizontal([ + new Canvas(BANGDREAM_RENDER_THEME.layout.listIndent, 1), + textImage, + ]); } const ymax = textImage.height + keyImage.height + 10; const canvas = new Canvas(maxWidth, ymax); const ctx = canvas.getContext('2d'); ctx.drawImage(keyImage, 0, 0); if (textImage.height != 0) { - ctx.drawImage(textImage, 20, keyImage.height + 10); + ctx.drawImage( + textImage, + BANGDREAM_RENDER_THEME.layout.listIndent, + keyImage.height + 10, + ); } return canvas; } @@ -107,7 +109,9 @@ export function drawTipsInList({ lineHeight = textSize * 1.5, spacing = textSize / 3, }: tipsOptions) { - const xmax = 760; + const xmax = + BANGDREAM_RENDER_THEME.layout.contentWidth - + BANGDREAM_RENDER_THEME.layout.listIndent * 2; let textImage: Canvas; if (typeof text == 'string') { textImage = drawText({ text, textSize, maxWidth: xmax, lineHeight }); @@ -122,11 +126,19 @@ export function drawTipsInList({ } else { textImage = new Canvas(1, 1); } - const canvas = new Canvas(800, textImage.height + 10); + const canvas = new Canvas( + BANGDREAM_RENDER_THEME.layout.contentWidth, + textImage.height + 10, + ); const ctx = canvas.getContext('2d'); - ctx.fillStyle = '#f1f1f1'; - ctx.fillRect(0, 10, 800, textImage.height); - ctx.drawImage(textImage, 20, 10); + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.subtlePanel; + ctx.fillRect( + 0, + 10, + BANGDREAM_RENDER_THEME.layout.contentWidth, + textImage.height, + ); + ctx.drawImage(textImage, BANGDREAM_RENDER_THEME.layout.listIndent, 10); return canvas; } @@ -142,7 +154,7 @@ export async function drawListByServerList( content: Array, key?: string, serverList: Server[] = globalDefaultServer, - maxWidth = 800, + maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth, ) { const tempcontent: Array = []; @@ -211,13 +223,16 @@ export function drawListMerge(imageList: Array): Canvas { maxHeight = element.height; } } - const canvas = new Canvas(800, maxHeight); + const canvas = new Canvas( + BANGDREAM_RENDER_THEME.layout.contentWidth, + maxHeight, + ); const ctx = canvas.getContext('2d'); let x = 0; for (let i = 0; i < imageList.length; i++) { const element = imageList[i]; ctx.drawImage(element, x, 0); - x += 800 / imageList.length; + x += BANGDREAM_RENDER_THEME.layout.contentWidth / imageList.length; } return canvas; } @@ -232,7 +247,7 @@ export function drawListMerge(imageList: Array): Canvas { */ export function drawImageListCenter( imageList: Array, - maxWidth = 800, + maxWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth, ): Canvas { interface imageLine { imageList: Array; @@ -320,10 +335,14 @@ export function drawListWithLine(textImageList: Array): Canvas { const element = textImageList[i]; height += element.height; } - const canvas = new Canvas(800, height + 10); + const canvas = new Canvas( + BANGDREAM_RENDER_THEME.layout.contentWidth, + height + 10, + ); const ctx = canvas.getContext('2d'); - ctx.fillStyle = '#a8a8a8'; - ctx.fillRect(10, 10, 5, height + 20); + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.separator; + const lineSpec = createVerticalSeparatorSpec(height + 20); + ctx.fillRect(lineSpec.startX, 10, 5, height + 20); for (let i = 0; i < textImageList.length; i++) { const element = textImageList[i]; ctx.drawImage(element, x, y); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-rarity.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-rarity.ts index 73cb1c6..49902c0 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-rarity.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-rarity.ts @@ -1,8 +1,7 @@ import { Canvas, Image } from 'skia-canvas'; import { drawList } from './list-frame'; -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 { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; interface RarityInListOptions { key?: string; @@ -16,11 +15,9 @@ export const starList: { [type: string]: Image } = {}; * 在图片布局层中加载图片Once。 */ async function loadImageOnce() { - starList.normal = await loadImageFromPath( - path.join(assetsRootPath, '/Card/star.png'), - ); + starList.normal = await loadImageFromPath(getBangDreamAssetPath('cardStar')); starList.trained = await loadImageFromPath( - path.join(assetsRootPath, '/Card/star_trained.png'), + getBangDreamAssetPath('cardStarTrained'), ); } loadImageOnce(); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts index 6f7a567..cd7e4c4 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts @@ -11,6 +11,8 @@ import { drawDifficultyList, drawDifficulty } from './list-difficulty'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { drawList } from './list-frame'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; +import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; /** * 在图片布局层中绘制歌曲In列表。 @@ -34,7 +36,7 @@ export async function drawSongInList( heightMax: 80, }); - const canvas = new Canvas(800, 75); + const canvas = new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 75); const ctx = canvas.getContext('2d'); ctx.drawImage(songImage, 50, 5, 65, 65); //id @@ -42,7 +44,7 @@ export async function drawSongInList( text: song.songId.toString(), textSize: 23, lineHeight: 37.5, - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, }); ctx.drawImage(idImage, 0, 0); //曲名与乐队名 @@ -58,7 +60,7 @@ export async function drawSongInList( text: fullText, textSize: 23, lineHeight: 37.5, - maxWidth: 800, + maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, }); ctx.drawImage(textImage, 120, 0); @@ -69,7 +71,7 @@ export async function drawSongInList( : drawDifficulty(difficulty, song.difficulty[difficulty].playLevel, 45); ctx.drawImage( difficultyImage, - 800 - difficultyImage.width, + BANGDREAM_RENDER_THEME.layout.contentWidth - difficultyImage.width, 75 / 2 - difficultyImage.height / 2, ); return canvas; @@ -96,17 +98,7 @@ export async function drawSongListInList( const x = 0; let y = 0; const views: Canvas[] = []; - const line = drawDottedLine({ - width: 800, - height: 10, - startX: 5, - startY: 5, - endX: 795, - endY: 5, - radius: 2, - gap: 10, - color: '#a8a8a8', - }); + const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 })); for (let i = 0; i < songs.length; i++) { views.push( resizeImage({ diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text.ts index a4c48df..99e67c5 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/skill-text.ts @@ -1,23 +1,21 @@ import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill'; import { Image, Canvas } from 'skia-canvas'; -import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { drawTextWithImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; -import * as path from 'path'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; +import { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; const skillIcon: { [skillType: string]: Image } = {}; /** * 在图片布局层中加载图片Once。 */ async function loadImageOnce() { - skillIcon.life = await loadImageFromPath( - path.join(assetsRootPath, '/Skill/life.png'), - ); + skillIcon.life = await loadImageFromPath(getBangDreamAssetPath('skillLife')); skillIcon.judge = await loadImageFromPath( - path.join(assetsRootPath, '/Skill/judge.png'), + getBangDreamAssetPath('skillJudge'), ); skillIcon.damage = await loadImageFromPath( - path.join(assetsRootPath, '/Skill/damage.png'), + getBangDreamAssetPath('skillDamage'), ); } loadImageOnce(); @@ -70,11 +68,11 @@ export async function drawCardIconSkill(skill: Skill): Promise { textSize: 27, lineHeight: 30, spacing: 3, - color: '#ffffff', - font: 'old', + color: BANGDREAM_RENDER_THEME.color.surface, + font: BANGDREAM_RENDER_THEME.font.body, }); const textbase = await loadImageFromPath( - path.join(assetsRootPath, '/Card/text.png'), + getBangDreamAssetPath('cardSkillTextBase'), ); const canvas = new Canvas(stringWithImage.width + 15, 45); const ctx = canvas.getContext('2d'); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts index 8c87c58..fd5287b 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/song-chart-preview.ts @@ -1,6 +1,10 @@ import { Canvas, Image, loadImage } from 'skia-canvas'; -import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { loadImageFromPath } from '@/qqbot/plugins/bangDream/tsugu/canvas/image-utils'; +import { + BangDreamLocalAssetKey, + getBangDreamAssetPath, +} from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; interface BestdoriPreviewPayload { id: number; @@ -70,6 +74,23 @@ const NOTE_IMAGE_KEYS = [ 'RightArrow', 'RightArrowEnd', ] as const; +const NOTE_IMAGE_ASSET_KEYS: Record< + (typeof NOTE_IMAGE_KEYS)[number], + BangDreamLocalAssetKey +> = { + Flick: 'songChartNoteFlick', + FlickTop: 'songChartNoteFlickTop', + LeftArrow: 'songChartNoteLeftArrow', + LeftArrowEnd: 'songChartNoteLeftArrowEnd', + Long: 'songChartNoteLong', + RightArrow: 'songChartNoteRightArrow', + RightArrowEnd: 'songChartNoteRightArrowEnd', + Sim: 'songChartNoteSim', + Single: 'songChartNoteSingle', + SingleOff: 'songChartNoteSingleOff', + Skill: 'songChartNoteSkill', + Tick: 'songChartNoteTick', +}; const DISPLAY_NOTE_TYPES = [ 'Single', 'SingleOff', @@ -451,7 +472,9 @@ async function loadNoteImages(): Promise> { const entries = await Promise.all( NOTE_IMAGE_KEYS.map(async (key) => [ key, - await loadImageFromPath(`${assetsRootPath}/SongChart/note/${key}.png`), + await loadImageFromPath( + getBangDreamAssetPath(NOTE_IMAGE_ASSET_KEYS[key]), + ), ]), ); return Object.fromEntries(entries); @@ -467,7 +490,7 @@ async function loadCoverImage(cover: string | Buffer): Promise { try { return await loadImage(cover); } catch { - return await loadImageFromPath(`${assetsRootPath}/SongChart/jacket.png`); + return await loadImageFromPath(getBangDreamAssetPath('songChartJacket')); } } @@ -526,7 +549,7 @@ function drawBaseInfo( ): void { const { id, diff, level } = payload; ctx.save(); - ctx.fillStyle = '#000'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBackground; ctx.fillRect(0, 0, layout.width, layout.height); ctx.restore(); @@ -539,10 +562,10 @@ function drawBaseInfo( ); ctx.save(); - ctx.fillStyle = '#1f1e33'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartPanel; ctx.fillRect(OFFSET - 8, OFFSET - 8, 128, 24); - ctx.fillStyle = '#FFF'; - ctx.font = '16px "Arial"'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText; + ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(`${id}`, OFFSET + 56, OFFSET + 4, 128); @@ -550,10 +573,12 @@ function drawBaseInfo( const coverWidth = layout.infoAreaWidth - 16; ctx.save(); - ctx.fillStyle = DIFFICULTY_COLOR_LIST[diff] ?? '#777'; + ctx.fillStyle = + DIFFICULTY_COLOR_LIST[diff] ?? + BANGDREAM_RENDER_THEME.color.chartDifficultyFallback; ctx.fillRect(8 + coverWidth - 116, 8 + coverWidth - 12, 128, 24); - ctx.fillStyle = '#FFF'; - ctx.font = '16px "Arial"'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText; + ctx.font = `16px "${BANGDREAM_RENDER_THEME.font.chart}"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(`${diff} ${level}`, 8 + coverWidth - 52, 8 + coverWidth, 128); @@ -657,8 +682,8 @@ function drawBeatLines( */ function drawTimeline(ctx: any, layout: PreviewLayout): void { ctx.save(); - ctx.font = '18px "Arial"'; - ctx.fillStyle = '#FFF'; + ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText; ctx.textAlign = 'right'; for (let i = 0; i <= layout.chartLength; i += 5) { const { x, y } = getTimePosition(layout, i); @@ -692,20 +717,20 @@ function drawCountAndBpmLines( if (count % 50 !== 0) { continue; } - ctx.font = '18px "Arial"'; + ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`; ctx.fillStyle = 'rgba(128, 128, 128, 0.5)'; ctx.textAlign = 'left'; ctx.fillRect(x, y - 1, w, 2); - ctx.fillStyle = '#FFF'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartText; setAdaptiveTextBaseline(ctx, layout, 18, y); ctx.fillText(`${count}`, x + w + 8, y); continue; } if (note.type === 'BPM') { - ctx.fillStyle = '#C34FBB'; + ctx.fillStyle = BANGDREAM_RENDER_THEME.color.chartBpm; ctx.fillRect(x, y - 1, w, 2); - ctx.font = '18px "Arial"'; + ctx.font = `18px "${BANGDREAM_RENDER_THEME.font.chart}"`; ctx.textAlign = 'left'; setAdaptiveTextBaseline(ctx, layout, 18, y); ctx.fillText(`${note.bpm}`, x + w + 8, y); diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/theme.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/theme.ts new file mode 100644 index 0000000..d73e602 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/theme.ts @@ -0,0 +1,31 @@ +export const BANGDREAM_RENDER_THEME = { + color: { + backgroundEasy: '#fef3ef', + chartBackground: '#000', + chartBpm: '#C34FBB', + chartDifficultyFallback: '#777', + chartPanel: '#1f1e33', + chartText: '#FFF', + labelBackground: '#5b5b5b', + mutedText: '#a7a7a7', + primaryText: '#505050', + separator: '#a8a8a8', + skillLevelBackground: '#ff0000', + subtlePanel: '#f1f1f1', + surface: '#ffffff', + }, + font: { + body: 'old', + chart: 'Arial', + chinese: 'FangZhengHeiTi', + fallback: 'Microsoft Yahei', + }, + layout: { + contentWidth: 800, + defaultGap: 20, + listIndent: 20, + }, +} as const; + +export type BangDreamRenderFont = + (typeof BANGDREAM_RENDER_THEME.font)[keyof typeof BANGDREAM_RENDER_THEME.font]; diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/title.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/title.ts index e11e41b..6406f7a 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/title.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/title.ts @@ -1,15 +1,15 @@ import { Canvas, Image } from 'skia-canvas'; import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; -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 { getBangDreamAssetPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; let titleImage: Image; /** * 在图片布局层中加载图片Once。 */ async function loadImageOnce() { - titleImage = await loadImageFromPath(path.join(assetsRootPath, '/title.png')); + titleImage = await loadImageFromPath(getBangDreamAssetPath('title')); } loadImageOnce(); @@ -29,16 +29,16 @@ export function drawTitle(title1: string, title2: string): Canvas { maxWidth: 900, lineHeight: 50, textSize: 30, - color: '#ffffff', - font: 'old', + color: BANGDREAM_RENDER_THEME.color.surface, + font: BANGDREAM_RENDER_THEME.font.body, }); const text2 = drawText({ text: title2, maxWidth: 900, lineHeight: 68, textSize: 40, - color: '#5b5b5b', - font: 'old', + color: BANGDREAM_RENDER_THEME.color.labelBackground, + font: BANGDREAM_RENDER_THEME.font.body, }); ctx.drawImage(text1, 74, 0); ctx.drawImage(text2, 74, 42); diff --git a/src/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest.ts b/src/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest.ts new file mode 100644 index 0000000..8215078 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/runtime/asset-manifest.ts @@ -0,0 +1,49 @@ +import * as path from 'path'; +import { assetsRootPath } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; + +export const BANGDREAM_LOCAL_ASSETS = { + backgroundLive: 'BG/live.png', + backgroundObjectBig: 'BG/bg_object_big.png', + backgroundStar1: 'BG/star1.png', + backgroundStar2: 'BG/star2.png', + cardBirthday: 'Card/B.png', + cardDreamfes: 'Card/D.png', + cardKirafes: 'Card/K.png', + cardLimited: 'Card/L.png', + cardLimitBreakRank: 'Card/limitBreakRank.png', + cardSkillTextBase: 'Card/text.png', + cardStar: 'Card/star.png', + cardStarTrained: 'Card/star_trained.png', + fontFangZhengHeiTi: 'Fonts/FangZhengHeiTi_GBK.ttf', + fontOld: 'Fonts/old.ttf', + skillDamage: 'Skill/damage.png', + skillJudge: 'Skill/judge.png', + skillLife: 'Skill/life.png', + songChartJacket: 'SongChart/jacket.png', + songChartNoteBar: 'SongChart/note/Bar.png', + songChartNoteFlick: 'SongChart/note/Flick.png', + songChartNoteFlickTop: 'SongChart/note/FlickTop.png', + songChartNoteLeftArrow: 'SongChart/note/LeftArrow.png', + songChartNoteLeftArrowEnd: 'SongChart/note/LeftArrowEnd.png', + songChartNoteLong: 'SongChart/note/Long.png', + songChartNoteRightArrow: 'SongChart/note/RightArrow.png', + songChartNoteRightArrowEnd: 'SongChart/note/RightArrowEnd.png', + songChartNoteSim: 'SongChart/note/Sim.png', + songChartNoteSingle: 'SongChart/note/Single.png', + songChartNoteSingleOff: 'SongChart/note/SingleOff.png', + songChartNoteSkill: 'SongChart/note/Skill.png', + songChartNoteTick: 'SongChart/note/Tick.png', + title: 'title.png', + twServerIcon: 'tw.png', +} as const; + +export type BangDreamLocalAssetKey = keyof typeof BANGDREAM_LOCAL_ASSETS; + +/** + * 解析 Tsugu 本地资源路径。 + * + * @param key - 本地资源 manifest 键名。 + */ +export function getBangDreamAssetPath(key: BangDreamLocalAssetKey): string { + return path.join(assetsRootPath, BANGDREAM_LOCAL_ASSETS[key]); +}