Files
Genarrative/src/hooks/rpg-runtime-story/storyEncounterState.test.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

165 lines
4.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
AnimationState,
type Character,
type Encounter,
type GameState,
type StoryMoment,
} from '../../types';
import { createStoryStateResolvers } from './storyEncounterState';
function createCharacter(): Character {
return {
id: 'hero',
name: '沈行',
title: '试剑客',
description: '测试主角',
personality: 'calm',
skills: [],
} as unknown as Character;
}
function createGameState(overrides: Partial<GameState> = {}): GameState {
return {
worldType: null,
customWorldProfile: null,
playerCharacter: createCharacter(),
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,
...overrides,
} as GameState;
}
function createNpcEncounter(
overrides: Partial<Encounter> = {},
): Encounter {
return {
id: 'npc-guard',
kind: 'npc',
npcName: '山道客',
npcDescription: '守在路口的陌生人',
npcAvatar: '/npc.png',
context: '山道相遇',
...overrides,
} as Encounter;
}
describe('storyEncounterState', () => {
it('uses preview talk options for regular npc encounters before formal interaction starts', () => {
const character = createCharacter();
const state = createGameState({
currentEncounter: createNpcEncounter(),
});
const buildNpcStory = vi.fn();
const { getAvailableOptionsForState } = createStoryStateResolvers({
buildNpcStory,
});
expect(getAvailableOptionsForState(state, character)).toEqual([
expect.objectContaining({
functionId: 'npc_preview_talk',
}),
]);
expect(buildNpcStory).not.toHaveBeenCalled();
});
it('uses normal npc story options after the npc interaction has started', () => {
const character = createCharacter();
const npcStory: StoryMoment = {
text: '普通 NPC 正常对话',
options: [
{
functionId: 'npc_chat',
actionText: '继续交谈',
text: '继续交谈',
visuals: {
playerAnimation: AnimationState.IDLE,
playerMoveMeters: 0,
playerOffsetY: 0,
playerFacing: 'right',
scrollWorld: false,
monsterChanges: [],
},
},
],
};
const state = createGameState({
currentEncounter: createNpcEncounter(),
npcInteractionActive: true,
});
const buildNpcStory = vi.fn(() => npcStory);
const { getAvailableOptionsForState } = createStoryStateResolvers({
buildNpcStory,
});
expect(getAvailableOptionsForState(state, character)).toEqual(
npcStory.options,
);
expect(buildNpcStory).toHaveBeenCalledWith(
state,
character,
state.currentEncounter,
undefined,
);
});
it('preserves explicit fallback text when the state falls back to the generic story moment', () => {
const state = createGameState();
const character = createCharacter();
const { buildFallbackStoryForState } = createStoryStateResolvers({
buildNpcStory: vi.fn(),
});
const story = buildFallbackStoryForState(state, character, '手动兜底文本');
expect(story.text).toBe('手动兜底文本');
expect(story.options.length).toBeGreaterThan(0);
});
});