@@ -0,0 +1,236 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
AnimationState,
|
||||
type Character,
|
||||
type QuestLogEntry,
|
||||
type StoryMoment,
|
||||
type StoryOption,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import { RpgAdventurePanel } from './RpgAdventurePanel';
|
||||
|
||||
function createCharacter(): Character {
|
||||
return {
|
||||
id: 'hero',
|
||||
name: '沈行',
|
||||
title: '试剑客',
|
||||
description: '测试主角',
|
||||
backstory: '测试背景',
|
||||
avatar: '/hero.png',
|
||||
portrait: '/hero.png',
|
||||
assetFolder: 'hero',
|
||||
assetVariant: 'default',
|
||||
attributes: {
|
||||
strength: 10,
|
||||
agility: 10,
|
||||
intelligence: 8,
|
||||
spirit: 9,
|
||||
},
|
||||
personality: 'calm',
|
||||
skills: [],
|
||||
adventureOpenings: {},
|
||||
} as Character;
|
||||
}
|
||||
|
||||
function createOption(functionId: string, actionText: string): StoryOption {
|
||||
return {
|
||||
functionId,
|
||||
actionText,
|
||||
text: actionText,
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
playerMoveMeters: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPendingQuest(): QuestLogEntry {
|
||||
return {
|
||||
id: 'quest-liu-1',
|
||||
issuerNpcId: 'npc-liu',
|
||||
issuerNpcName: '柳无声',
|
||||
sceneId: 'scene-bamboo',
|
||||
title: '竹林密信',
|
||||
description: '替柳无声查清竹林中的密信来源。',
|
||||
summary: '去竹林查清密信来源。',
|
||||
objective: {
|
||||
kind: 'inspect_treasure',
|
||||
requiredCount: 1,
|
||||
},
|
||||
progress: 0,
|
||||
status: 'active',
|
||||
reward: {
|
||||
affinityBonus: 5,
|
||||
currency: 10,
|
||||
items: [],
|
||||
},
|
||||
rewardText: '完成后可获得报酬。',
|
||||
};
|
||||
}
|
||||
|
||||
function createPendingQuestStory(quest: QuestLogEntry): StoryMoment {
|
||||
const viewOption = createOption('npc_chat_quest_offer_view', '查看任务');
|
||||
viewOption.runtimePayload = {
|
||||
npcChatQuestOfferAction: 'view',
|
||||
};
|
||||
const replaceOption = createOption('npc_chat_quest_offer_replace', '更换任务');
|
||||
replaceOption.runtimePayload = {
|
||||
npcChatQuestOfferAction: 'replace',
|
||||
};
|
||||
const abandonOption = createOption('npc_chat_quest_offer_abandon', '放弃任务');
|
||||
abandonOption.runtimePayload = {
|
||||
npcChatQuestOfferAction: 'abandon',
|
||||
};
|
||||
|
||||
return {
|
||||
text: '柳无声把真正的委托说了出来。',
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{ speaker: 'npc', speakerName: '柳无声', text: '这件事我只想正式托付给你。' },
|
||||
],
|
||||
options: [viewOption, replaceOption, abandonOption],
|
||||
npcChatState: {
|
||||
npcId: 'npc-liu',
|
||||
npcName: '柳无声',
|
||||
turnCount: 2,
|
||||
customInputPlaceholder: '输入你想对 TA 说的话',
|
||||
pendingQuestOffer: {
|
||||
quest,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createAcceptedQuestStory(quest: QuestLogEntry): StoryMoment {
|
||||
return {
|
||||
text: '柳无声把接下来的线索正式交给了你。',
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{ speaker: 'npc', speakerName: '柳无声', text: '这件事我只想正式托付给你。' },
|
||||
{ speaker: 'player', text: '这件事我愿意接下,你把关键要点交给我。' },
|
||||
{ speaker: 'npc', speakerName: '柳无声', text: '先去竹林查清密信来源。' },
|
||||
],
|
||||
options: [
|
||||
createOption('npc_chat', '这件事里你最担心哪一步'),
|
||||
createOption('npc_chat', '我回来时你最想先知道什么'),
|
||||
],
|
||||
npcChatState: {
|
||||
npcId: 'npc-liu',
|
||||
npcName: '柳无声',
|
||||
turnCount: 2,
|
||||
customInputPlaceholder: '输入你想对 TA 说的话',
|
||||
pendingQuestOffer: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function QuestOfferHarness() {
|
||||
const pendingQuest = createPendingQuest();
|
||||
const [currentStory, setCurrentStory] = useState<StoryMoment>(
|
||||
createPendingQuestStory(pendingQuest),
|
||||
);
|
||||
const [quests, setQuests] = useState<QuestLogEntry[]>([]);
|
||||
const acceptPendingOffer = vi.fn(() => {
|
||||
queueMicrotask(() => {
|
||||
setQuests([pendingQuest]);
|
||||
setCurrentStory(createAcceptedQuestStory(pendingQuest));
|
||||
});
|
||||
return pendingQuest.id;
|
||||
});
|
||||
|
||||
return (
|
||||
<RpgAdventurePanel
|
||||
aiError={null}
|
||||
currentStory={currentStory}
|
||||
isLoading={false}
|
||||
displayedOptions={currentStory.options}
|
||||
hideOptions={false}
|
||||
canRefreshOptions={false}
|
||||
onRefreshOptions={() => undefined}
|
||||
onChoice={() => undefined}
|
||||
onSubmitNpcChatInput={() => true}
|
||||
onExitNpcChat={() => true}
|
||||
onOpenCharacter={() => undefined}
|
||||
onOpenInventory={() => undefined}
|
||||
playerCharacter={createCharacter()}
|
||||
worldType={WorldType.WUXIA}
|
||||
quests={quests}
|
||||
questUi={{
|
||||
acknowledgeQuestCompletion: () => undefined,
|
||||
claimQuestReward: () => null,
|
||||
}}
|
||||
npcChatQuestOfferUi={{
|
||||
replacePendingOffer: () => false,
|
||||
abandonPendingOffer: () => false,
|
||||
acceptPendingOffer,
|
||||
}}
|
||||
goalStack={{
|
||||
northStarGoal: null,
|
||||
activeGoal: null,
|
||||
immediateStepGoal: null,
|
||||
supportGoals: [],
|
||||
}}
|
||||
goalPulse={null}
|
||||
onDismissGoalPulse={() => undefined}
|
||||
battleRewardUi={{
|
||||
reward: null,
|
||||
dismiss: () => undefined,
|
||||
}}
|
||||
playerHp={100}
|
||||
playerMaxHp={100}
|
||||
playerMana={20}
|
||||
playerMaxMana={20}
|
||||
playerSkillCooldowns={{}}
|
||||
inBattle={false}
|
||||
currentNpcBattleMode={null}
|
||||
statistics={{
|
||||
playTimeMs: 0,
|
||||
hostileNpcsDefeated: 0,
|
||||
questsAccepted: 0,
|
||||
questsCompleted: 0,
|
||||
questsTurnedIn: 0,
|
||||
itemsUsed: 0,
|
||||
scenesTraveled: 0,
|
||||
currentSceneName: '竹林古道',
|
||||
playerCurrency: 0,
|
||||
inventoryItemCount: 0,
|
||||
inventoryStackCount: 0,
|
||||
activeCompanionCount: 0,
|
||||
rosterCompanionCount: 0,
|
||||
}}
|
||||
musicVolume={0.6}
|
||||
onMusicVolumeChange={() => undefined}
|
||||
onSaveAndExit={() => undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
if (!HTMLElement.prototype.scrollTo) {
|
||||
HTMLElement.prototype.scrollTo = () => undefined;
|
||||
}
|
||||
});
|
||||
|
||||
test('quest offer accept button reuses the shared accepted-quest follow-up chain', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<QuestOfferHarness />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /查看任务/u }));
|
||||
await user.click(await screen.findByRole('button', { name: '领取任务' }));
|
||||
|
||||
expect(await screen.findByText('任务进度:0/1')).toBeTruthy();
|
||||
expect(screen.getAllByText('竹林密信').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('待领取')).toBeNull();
|
||||
expect(screen.getByText('这件事里你最担心哪一步')).toBeTruthy();
|
||||
});
|
||||
@@ -1724,6 +1724,9 @@ export function RpgAdventurePanel({
|
||||
onAcceptPendingNpcQuestOffer={() => {
|
||||
const acceptedQuestId = npcChatQuestOfferUi.acceptPendingOffer();
|
||||
if (!acceptedQuestId) return null;
|
||||
// 中文注释:待领取任务详情弹层走的是异步服务端接取链路,
|
||||
// 这里先记录 questId,等 quest 真正进入日志后再由 effect 统一收口面板状态。
|
||||
setPendingAcceptedQuestId(acceptedQuestId);
|
||||
setSelectedQuestId(null);
|
||||
return acceptedQuestId;
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user