From 77658f1eb40c84bee38f9881569d435de20b3513 Mon Sep 17 00:00:00 2001 From: Akirami <66513481+A-kirami@users.noreply.github.com> Date: Thu, 14 May 2026 04:24:51 +0800 Subject: [PATCH 1/2] feat: add textbox theme support for say dialogue --- .../src/Core/Modules/stage/stageInterface.ts | 1 + .../Core/Modules/stage/stageStateManager.ts | 1 + .../resolveDialogDisplayState.test.ts | 94 +++++++++++++++++++ .../gameScripts/resolveDialogDisplayState.ts | 40 ++++++++ packages/webgal/src/Core/gameScripts/say.ts | 20 ++-- .../webgal/src/Stage/TextBox/IMSSTextbox.tsx | 3 +- packages/webgal/src/Stage/TextBox/TextBox.tsx | 2 + packages/webgal/src/Stage/TextBox/types.ts | 1 + .../Menu/Options/TextPreview/TextPreview.tsx | 1 + 9 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.test.ts create mode 100644 packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts diff --git a/packages/webgal/src/Core/Modules/stage/stageInterface.ts b/packages/webgal/src/Core/Modules/stage/stageInterface.ts index d5ae57247..3ee72dc40 100644 --- a/packages/webgal/src/Core/Modules/stage/stageInterface.ts +++ b/packages/webgal/src/Core/Modules/stage/stageInterface.ts @@ -219,6 +219,7 @@ export interface IStageState { showText: string; // 文字 showTextSize: number; // 文字 showName: string; // 人物名 + textboxTheme: string; // 对话框主题 command: string; // 语句指令 choose: Array; // 选项列表 vocal: string; // 语音 文件地址(相对或绝对) diff --git a/packages/webgal/src/Core/Modules/stage/stageStateManager.ts b/packages/webgal/src/Core/Modules/stage/stageStateManager.ts index c71b5eb55..57d7972dc 100644 --- a/packages/webgal/src/Core/Modules/stage/stageStateManager.ts +++ b/packages/webgal/src/Core/Modules/stage/stageStateManager.ts @@ -45,6 +45,7 @@ export const initState: IStageState = { showText: '', showTextSize: -1, showName: '', + textboxTheme: '', command: '', choose: [], vocal: '', diff --git a/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.test.ts b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.test.ts new file mode 100644 index 000000000..7981fa57b --- /dev/null +++ b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import { resolveDialogDisplayState } from './resolveDialogDisplayState'; + +describe('resolveDialogDisplayState', () => { + it('uses speaker as the default show name and textbox theme', () => { + expect( + resolveDialogDisplayState({ + previousShowName: '', + previousTextboxTheme: '', + speaker: 'anon', + textboxThemeOverride: null, + clear: false, + }), + ).toEqual({ + showName: 'anon', + textboxTheme: 'anon', + }); + }); + + it('keeps the speaker name and lets textboxTheme override the default theme', () => { + expect( + resolveDialogDisplayState({ + previousShowName: '', + previousTextboxTheme: '', + speaker: 'anon', + textboxThemeOverride: 'system', + clear: false, + }), + ).toEqual({ + showName: 'anon', + textboxTheme: 'system', + }); + }); + + it('inherits the previous state when the sentence does not provide a new speaker or theme', () => { + expect( + resolveDialogDisplayState({ + previousShowName: 'anon', + previousTextboxTheme: 'system', + speaker: null, + textboxThemeOverride: null, + clear: false, + }), + ).toEqual({ + showName: 'anon', + textboxTheme: 'system', + }); + }); + + it('resets the textbox theme to the new speaker when the previous sentence used an override', () => { + expect( + resolveDialogDisplayState({ + previousShowName: 'anon', + previousTextboxTheme: 'system', + speaker: 'heroine', + textboxThemeOverride: null, + clear: false, + }), + ).toEqual({ + showName: 'heroine', + textboxTheme: 'heroine', + }); + }); + + it('clears both show name and textbox theme by default', () => { + expect( + resolveDialogDisplayState({ + previousShowName: 'anon', + previousTextboxTheme: 'system', + speaker: null, + textboxThemeOverride: null, + clear: true, + }), + ).toEqual({ + showName: '', + textboxTheme: '', + }); + }); + + it('allows textboxTheme to override the cleared default', () => { + expect( + resolveDialogDisplayState({ + previousShowName: 'anon', + previousTextboxTheme: 'anon', + speaker: null, + textboxThemeOverride: 'system', + clear: true, + }), + ).toEqual({ + showName: '', + textboxTheme: 'system', + }); + }); +}); diff --git a/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts new file mode 100644 index 000000000..5c4465b43 --- /dev/null +++ b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts @@ -0,0 +1,40 @@ +interface ResolveDialogDisplayStateOptions { + previousShowName: string; + previousTextboxTheme: string; + speaker: string | null; + textboxThemeOverride: string | null; + clear: boolean; +} + +interface DialogDisplayState { + showName: string; + textboxTheme: string; +} + +export function resolveDialogDisplayState(options: ResolveDialogDisplayStateOptions): DialogDisplayState { + const { previousShowName, previousTextboxTheme, speaker, textboxThemeOverride, clear } = options; + + let showName = previousShowName; + if (speaker !== null) { + showName = speaker; + } + if (clear) { + showName = ''; + } + + let textboxTheme = previousTextboxTheme; + if (speaker !== null) { + textboxTheme = speaker; + } + if (clear) { + textboxTheme = ''; + } + if (textboxThemeOverride !== null) { + textboxTheme = textboxThemeOverride; + } + + return { + showName, + textboxTheme, + }; +} diff --git a/packages/webgal/src/Core/gameScripts/say.ts b/packages/webgal/src/Core/gameScripts/say.ts index 9e2fd94bf..46da4d29e 100644 --- a/packages/webgal/src/Core/gameScripts/say.ts +++ b/packages/webgal/src/Core/gameScripts/say.ts @@ -11,6 +11,7 @@ import { compileSentence } from '@/Stage/TextBox/TextBox'; import { performMouthAnimation } from '@/Core/gameScripts/vocal/vocalAnimation'; import { match } from '@/Core/util/match'; import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; +import { resolveDialogDisplayState } from './resolveDialogDisplayState'; /** * 进行普通对话的显示 @@ -29,6 +30,7 @@ export const say = (sentence: ISentence): IPerform => { const isNotend = getBooleanArgByKey(sentence, 'notend') ?? false; // 是否有 notend 参数 const speaker = getStringArgByKey(sentence, 'speaker'); // 获取说话者 const clear = getBooleanArgByKey(sentence, 'clear') ?? false; // 是否清除说话者 + const textboxThemeOverride = getStringArgByKey(sentence, 'theme'); // 覆盖本句对话框主题 const vocal = getStringArgByKey(sentence, 'vocal'); // 是否播放语音 // 如果是concat,那么就继承上一句的key,并且继承上一句对话。 @@ -75,15 +77,15 @@ export const say = (sentence: ISentence): IPerform => { break; } - // 设置显示的角色名称 - let showName = stageState.showName; // 先默认继承 - if (speaker !== null) { - showName = speaker; - } - if (clear) { - showName = ''; - } - stageStateManager.setStage('showName', showName); + const dialogDisplayState = resolveDialogDisplayState({ + previousShowName: stageState.showName, + previousTextboxTheme: stageState.textboxTheme, + speaker, + textboxThemeOverride, + clear, + }); + stageStateManager.setStage('showName', dialogDisplayState.showName); + stageStateManager.setStage('textboxTheme', dialogDisplayState.textboxTheme); // 模拟说话 let performSimulateVocalTimeout: ReturnType | null = null; diff --git a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx index 1a8ec0c64..d9c485a8a 100644 --- a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx +++ b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx @@ -22,6 +22,7 @@ export default function IMSSTextbox(props: ITextboxProps) { miniAvatar, isHasName, showName, + textboxTheme, font, textDuration, isUseStroke, @@ -214,7 +215,7 @@ export default function IMSSTextbox(props: ITextboxProps) { return ( <> {isText && ( -
+
{ const textArray = compileSentence(stageState.showText, lineLimit); const isHasName = stageState.showName !== ''; const showName = compileSentence(stageState.showName, lineLimit); + const textboxTheme = stageState.textboxTheme; const currentConcatDialogPrev = stageState.currentConcatDialogPrev; const currentDialogKey = stageState.currentDialogKey; const miniAvatar = stageState.miniAvatar; @@ -95,6 +96,7 @@ export const TextBox = () => { textDelay={textDelay} showName={showName} isHasName={isHasName} + textboxTheme={textboxTheme} currentConcatDialogPrev={currentConcatDialogPrev} fontSize={size} currentDialogKey={currentDialogKey} diff --git a/packages/webgal/src/Stage/TextBox/types.ts b/packages/webgal/src/Stage/TextBox/types.ts index 0cb509069..b8e30ebec 100644 --- a/packages/webgal/src/Stage/TextBox/types.ts +++ b/packages/webgal/src/Stage/TextBox/types.ts @@ -13,6 +13,7 @@ export interface ITextboxProps { miniAvatar: string; showName: EnhancedNode[][]; isHasName: boolean; + textboxTheme: string; font: string; textDuration: number; textSizeState: number; diff --git a/packages/webgal/src/UI/Menu/Options/TextPreview/TextPreview.tsx b/packages/webgal/src/UI/Menu/Options/TextPreview/TextPreview.tsx index e46068bd0..2a52bae45 100644 --- a/packages/webgal/src/UI/Menu/Options/TextPreview/TextPreview.tsx +++ b/packages/webgal/src/UI/Menu/Options/TextPreview/TextPreview.tsx @@ -43,6 +43,7 @@ export const TextPreview = (props: any) => { textDelay: textDelay, isHasName: isHasName, showName: showNameArray, + textboxTheme: '', currentConcatDialogPrev: '', fontSize: size, currentDialogKey: String(previewKey), From 614d6066cd0391141761eae48406e785a2ba94bf Mon Sep 17 00:00:00 2001 From: Akirami <66513481+A-kirami@users.noreply.github.com> Date: Thu, 14 May 2026 04:41:11 +0800 Subject: [PATCH 2/2] refactor: simplify dialog display state resolution --- .../gameScripts/resolveDialogDisplayState.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts index 5c4465b43..a52d744e2 100644 --- a/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts +++ b/packages/webgal/src/Core/gameScripts/resolveDialogDisplayState.ts @@ -14,24 +14,8 @@ interface DialogDisplayState { export function resolveDialogDisplayState(options: ResolveDialogDisplayStateOptions): DialogDisplayState { const { previousShowName, previousTextboxTheme, speaker, textboxThemeOverride, clear } = options; - let showName = previousShowName; - if (speaker !== null) { - showName = speaker; - } - if (clear) { - showName = ''; - } - - let textboxTheme = previousTextboxTheme; - if (speaker !== null) { - textboxTheme = speaker; - } - if (clear) { - textboxTheme = ''; - } - if (textboxThemeOverride !== null) { - textboxTheme = textboxThemeOverride; - } + const showName = clear ? '' : (speaker ?? previousShowName); + const textboxTheme = textboxThemeOverride ?? (clear ? '' : (speaker ?? previousTextboxTheme)); return { showName,