237 lines
6.6 KiB
TypeScript
237 lines
6.6 KiB
TypeScript
/* @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();
|
||
});
|