Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
209
src/hooks/useNpcInteractionFlow.test.ts
Normal file
209
src/hooks/useNpcInteractionFlow.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
import {getCharacterById} from '../data/characterPresets';
|
||||
import {AnimationState, type Character, type GameState,WorldType} from '../types';
|
||||
import {buildCompanionRenderStatesForGameState} from './useNpcInteractionFlow';
|
||||
|
||||
vi.mock('../data/characterPresets', () => ({
|
||||
getCharacterById: vi.fn(),
|
||||
}));
|
||||
|
||||
function createTestCharacter(id: string, name: string): Character {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
title: '测试同伴',
|
||||
description: '用于测试的角色',
|
||||
backstory: '测试背景',
|
||||
avatar: '/test-avatar.png',
|
||||
portrait: '/test-portrait.png',
|
||||
assetFolder: 'test-character',
|
||||
assetVariant: 'default',
|
||||
attributes: {
|
||||
strength: 10,
|
||||
agility: 10,
|
||||
intelligence: 10,
|
||||
spirit: 10,
|
||||
},
|
||||
personality: 'steady',
|
||||
skills: [
|
||||
{
|
||||
id: 'basic-strike',
|
||||
name: '试探一击',
|
||||
animation: AnimationState.ATTACK,
|
||||
damage: 10,
|
||||
manaCost: 0,
|
||||
cooldownTurns: 0,
|
||||
range: 1,
|
||||
style: 'steady',
|
||||
},
|
||||
],
|
||||
adventureOpenings: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createBaseState(): GameState {
|
||||
return {
|
||||
worldType: WorldType.WUXIA,
|
||||
customWorldProfile: null,
|
||||
playerCharacter: createTestCharacter('player', '主角'),
|
||||
runtimeStats: {
|
||||
playTimeMs: 0,
|
||||
lastPlayTickAt: null,
|
||||
hostileNpcsDefeated: 0,
|
||||
questsAccepted: 0,
|
||||
itemsUsed: 0,
|
||||
scenesTraveled: 0,
|
||||
},
|
||||
currentScene: 'Story',
|
||||
storyHistory: [],
|
||||
characterChats: {},
|
||||
animationState: AnimationState.IDLE,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
currentScenePreset: null,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
playerActionMode: 'idle',
|
||||
scrollWorld: false,
|
||||
inBattle: false,
|
||||
playerHp: 100,
|
||||
playerMaxHp: 100,
|
||||
playerMana: 20,
|
||||
playerMaxMana: 20,
|
||||
playerSkillCooldowns: {},
|
||||
activeCombatEffects: [],
|
||||
playerCurrency: 0,
|
||||
playerInventory: [],
|
||||
playerEquipment: {
|
||||
weapon: null,
|
||||
armor: null,
|
||||
relic: null,
|
||||
},
|
||||
npcStates: {},
|
||||
quests: [],
|
||||
roster: [],
|
||||
companions: [],
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
sparReturnEncounter: null,
|
||||
sparPlayerHpBefore: null,
|
||||
sparPlayerMaxHpBefore: null,
|
||||
sparStoryHistoryBefore: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildCompanionRenderStatesForGameState', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getCharacterById).mockReset();
|
||||
});
|
||||
|
||||
it('builds render states from the provided transition snapshot', () => {
|
||||
const companionCharacter = createTestCharacter('companion-a', '阿青');
|
||||
vi.mocked(getCharacterById).mockImplementation((characterId: string) => {
|
||||
if (characterId === companionCharacter.id || characterId === 'player') {
|
||||
return companionCharacter;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const transitionSnapshot: GameState = {
|
||||
...createBaseState(),
|
||||
playerFacing: 'left',
|
||||
animationState: AnimationState.ATTACK,
|
||||
companions: [
|
||||
{
|
||||
npcId: 'npc-aqing',
|
||||
characterId: companionCharacter.id,
|
||||
joinedAtAffinity: 10,
|
||||
hp: 36,
|
||||
maxHp: 48,
|
||||
mana: 12,
|
||||
maxMana: 18,
|
||||
skillCooldowns: {basicStrike: 1},
|
||||
offsetX: 14,
|
||||
offsetY: -6,
|
||||
transitionMs: 90,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const renderStates = buildCompanionRenderStatesForGameState({
|
||||
gameState: transitionSnapshot,
|
||||
presentationByNpcId: {
|
||||
'npc-aqing': {
|
||||
animationState: AnimationState.ACQUIRE,
|
||||
entryOffsetX: 28,
|
||||
entryOffsetY: 12,
|
||||
transitionMs: 240,
|
||||
recruitToken: 42,
|
||||
},
|
||||
},
|
||||
observeFacingByNpcId: {
|
||||
'npc-aqing': 'right',
|
||||
},
|
||||
});
|
||||
|
||||
expect(renderStates).toHaveLength(1);
|
||||
expect(renderStates[0]).toMatchObject({
|
||||
npcId: 'npc-aqing',
|
||||
character: companionCharacter,
|
||||
hp: 36,
|
||||
maxHp: 48,
|
||||
mana: 12,
|
||||
maxMana: 18,
|
||||
animationState: AnimationState.ACQUIRE,
|
||||
slot: 'upper',
|
||||
facing: 'right',
|
||||
entryOffsetX: 42,
|
||||
entryOffsetY: 6,
|
||||
transitionMs: 240,
|
||||
recruitToken: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it('lets callers render a visible snapshot even if the live state already changed', () => {
|
||||
const companionCharacter = createTestCharacter('companion-b', '小舟');
|
||||
vi.mocked(getCharacterById).mockImplementation((characterId: string) => {
|
||||
if (characterId === companionCharacter.id || characterId === 'player') {
|
||||
return companionCharacter;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const visibleSnapshot: GameState = {
|
||||
...createBaseState(),
|
||||
scrollWorld: true,
|
||||
companions: [
|
||||
{
|
||||
npcId: 'npc-xiaozhou',
|
||||
characterId: companionCharacter.id,
|
||||
joinedAtAffinity: 18,
|
||||
hp: 30,
|
||||
maxHp: 30,
|
||||
mana: 9,
|
||||
maxMana: 12,
|
||||
skillCooldowns: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const liveState: GameState = {
|
||||
...createBaseState(),
|
||||
companions: [],
|
||||
};
|
||||
|
||||
const visibleRenderStates = buildCompanionRenderStatesForGameState({
|
||||
gameState: visibleSnapshot,
|
||||
});
|
||||
const liveRenderStates = buildCompanionRenderStatesForGameState({
|
||||
gameState: liveState,
|
||||
});
|
||||
|
||||
expect(visibleRenderStates).toHaveLength(1);
|
||||
expect(visibleRenderStates[0]?.animationState).toBe(AnimationState.RUN);
|
||||
expect(liveRenderStates).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user