1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-18 17:28:23 +08:00
parent b3066c7bc1
commit 54b3d3c490
21 changed files with 731 additions and 156 deletions

View File

@@ -62,6 +62,8 @@ interface AdventurePanelProps {
canRefreshOptions: boolean;
onRefreshOptions: () => void;
onChoice: (option: StoryOption) => void;
onSubmitNpcChatInput?: (input: string) => boolean;
onExitNpcChat?: () => boolean;
onOpenCharacter: () => void;
onOpenInventory: () => void;
playerCharacter: Character;
@@ -149,12 +151,22 @@ function getOptionActionTextClass(option: StoryOption) {
function getDialogueTurnAlignmentClass(
turn: NonNullable<StoryMoment['dialogue']>[number],
) {
if (turn.speaker === 'system') {
return 'justify-center';
}
return turn.speaker === 'player' ? 'justify-end' : 'justify-start';
}
function getDialogueTurnBubbleClass(
turn: NonNullable<StoryMoment['dialogue']>[number],
) {
if (turn.speaker === 'system') {
return turn.affinityDelta && turn.affinityDelta > 0
? 'border-rose-400/30 bg-rose-500/12 text-rose-50'
: 'border-white/12 bg-white/[0.06] text-zinc-100';
}
if (turn.speaker === 'player') {
return 'border-sky-400/20 bg-sky-500/10 text-sky-50';
}
@@ -169,6 +181,10 @@ function getDialogueTurnBubbleClass(
function getDialogueTurnBubbleShapeClass(
turn: NonNullable<StoryMoment['dialogue']>[number],
) {
if (turn.speaker === 'system') {
return 'rounded-full';
}
if (turn.speaker === 'player') {
return 'rounded-2xl rounded-br-none';
}
@@ -183,6 +199,10 @@ function getDialogueTurnBubbleShapeClass(
function getDialogueTurnLabel(
turn: NonNullable<StoryMoment['dialogue']>[number],
) {
if (turn.speaker === 'system') {
return turn.affinityDelta && turn.affinityDelta > 0 ? '关系变化' : '系统';
}
if (turn.speaker === 'player') {
return '\u4f60';
}
@@ -597,6 +617,8 @@ export function AdventurePanel({
canRefreshOptions,
onRefreshOptions,
onChoice,
onSubmitNpcChatInput,
onExitNpcChat,
onOpenCharacter,
onOpenInventory,
playerCharacter,
@@ -622,6 +644,8 @@ export function AdventurePanel({
}: AdventurePanelProps) {
const isDialogueStory = currentStory.displayMode === 'dialogue';
const dialogueTurns = currentStory.dialogue ?? [];
const npcChatState = currentStory.npcChatState ?? null;
const isNpcChatMode = Boolean(npcChatState);
const isStoryStreaming = Boolean(currentStory.streaming);
const shouldHideChoiceUi = hideOptions;
const storyScrollContainerRef = useRef<HTMLDivElement | null>(null);
@@ -656,6 +680,7 @@ export function AdventurePanel({
const [selectedBattleRewardItemId, setSelectedBattleRewardItemId] = useState<
string | null
>(null);
const [npcChatDraft, setNpcChatDraft] = useState('');
const lastAutoOpenedGoalRef = useRef<string | null>(null);
const lastAutoOpenedPulseRef = useRef<string | null>(null);
const battleReward = battleRewardUi.reward;
@@ -734,6 +759,10 @@ export function AdventurePanel({
setSelectedBattleRewardItemId(null);
}, [battleReward]);
useEffect(() => {
setNpcChatDraft('');
}, [npcChatState?.npcId, npcChatState?.turnCount]);
useEffect(() => {
if (!primaryQuestGoal) {
return;
@@ -887,6 +916,18 @@ export function AdventurePanel({
onDismissGoalPulse();
};
const submitNpcChatDraft = () => {
const nextInput = npcChatDraft.trim();
if (!nextInput || !onSubmitNpcChatInput) {
return;
}
const submitted = onSubmitNpcChatInput(nextInput);
if (submitted) {
setNpcChatDraft('');
}
};
return (
<div className="relative flex min-h-0 flex-1 flex-col">
<button

View File

@@ -45,6 +45,8 @@ interface GameShellStoryProps {
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
handleChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
handleMapTravelToScene: (sceneId: string) => boolean;
npcUi: StoryGenerationNpcUi;
characterChatUi: CharacterChatUi;
@@ -200,6 +202,8 @@ export function GameShell({session, story, entry, companions, audio}: GameShellP
canRefreshOptions,
handleRefreshOptions,
handleChoice,
handleNpcChatInput,
exitNpcChat,
handleMapTravelToScene,
npcUi,
characterChatUi,
@@ -533,6 +537,8 @@ export function GameShell({session, story, entry, companions, audio}: GameShellP
canRefreshOptions={canRefreshOptions}
onRefreshOptions={handleRefreshOptions}
onChoice={handleSceneTransitionChoice}
onSubmitNpcChatInput={handleNpcChatInput}
onExitNpcChat={exitNpcChat}
onOpenCharacter={() => openOverlayPanel('character')}
onOpenInventory={() => openOverlayPanel('inventory')}
playerCharacter={visibleGameState.playerCharacter}

View File

@@ -74,6 +74,8 @@ export function GameShellMainContent({
canRefreshOptions,
handleRefreshOptions,
handleSceneTransitionChoice,
handleNpcChatInput,
exitNpcChat,
characterChatUi,
inventoryUi,
battleRewardUi,
@@ -112,6 +114,8 @@ export function GameShellMainContent({
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
handleSceneTransitionChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
characterChatUi: CharacterChatUi;
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
@@ -198,6 +202,8 @@ export function GameShellMainContent({
canRefreshOptions={canRefreshOptions}
handleRefreshOptions={handleRefreshOptions}
handleSceneTransitionChoice={handleSceneTransitionChoice}
handleNpcChatInput={handleNpcChatInput}
exitNpcChat={exitNpcChat}
characterChatUi={characterChatUi}
inventoryUi={inventoryUi}
battleRewardUi={battleRewardUi}

View File

@@ -34,6 +34,8 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
displayedOptions,
canRefreshOptions,
handleRefreshOptions,
handleNpcChatInput,
exitNpcChat,
handleMapTravelToScene,
npcUi,
characterChatUi,
@@ -151,6 +153,8 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
canRefreshOptions={canRefreshOptions}
handleRefreshOptions={handleRefreshOptions}
handleSceneTransitionChoice={handleSceneTransitionChoice}
handleNpcChatInput={handleNpcChatInput}
exitNpcChat={exitNpcChat}
characterChatUi={characterChatUi}
inventoryUi={inventoryUi}
battleRewardUi={battleRewardUi}

View File

@@ -51,6 +51,8 @@ export function GameShellStoryPanels({
canRefreshOptions,
handleRefreshOptions,
handleSceneTransitionChoice,
handleNpcChatInput,
exitNpcChat,
characterChatUi,
inventoryUi,
battleRewardUi,
@@ -77,6 +79,8 @@ export function GameShellStoryPanels({
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
handleSceneTransitionChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
characterChatUi: CharacterChatUi;
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
@@ -181,6 +185,8 @@ export function GameShellStoryPanels({
canRefreshOptions={canRefreshOptions}
onRefreshOptions={handleRefreshOptions}
onChoice={handleSceneTransitionChoice}
onSubmitNpcChatInput={handleNpcChatInput}
onExitNpcChat={exitNpcChat}
onOpenCharacter={() => openOverlayPanel('character')}
onOpenInventory={() => openOverlayPanel('inventory')}
playerCharacter={playerCharacter}

View File

@@ -33,6 +33,8 @@ export interface GameShellStoryProps {
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
handleChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
handleMapTravelToScene: (sceneId: string) => boolean;
npcUi: StoryGenerationNpcUi;
characterChatUi: CharacterChatUi;