Files
Genarrative/src/components/rpg-runtime-shell/useRpgRuntimeShellViewModel.test.ts
T
kdletters cbc27bad4a
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

294 lines
7.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { CharacterChatRecord, CompanionRenderState, GameState, StoryMoment } from '../../types';
import { AnimationState, WorldType } from '../../types';
import {
buildAdventureStatistics,
buildCanvasCompanionRenderStates,
buildCharacterChatSummaries,
buildRpgRuntimeDialogueIndicator,
} from '../rpg-runtime-shell/useRpgRuntimeShellViewModel';
function createBaseGameState(): GameState {
return {
worldType: WorldType.WUXIA,
customWorldProfile: null,
playerCharacter: null,
runtimeStats: {
playTimeMs: 0,
lastPlayTickAt: null,
hostileNpcsDefeated: 3,
questsAccepted: 2,
itemsUsed: 4,
scenesTraveled: 5,
},
currentScene: 'Story',
storyHistory: [],
characterChats: {},
animationState: AnimationState.IDLE,
currentEncounter: null,
npcInteractionActive: false,
currentScenePreset: {
id: 'scene-1',
name: '断桥旧哨',
description: '测试场景',
imageSrc: '/scene.png',
treasureHints: [],
npcs: [],
},
sceneHostileNpcs: [],
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
playerActionMode: 'idle',
scrollWorld: false,
inBattle: false,
playerHp: 100,
playerMaxHp: 100,
playerMana: 20,
playerMaxMana: 20,
playerSkillCooldowns: {},
activeBuildBuffs: [],
activeCombatEffects: [],
playerCurrency: 18,
playerInventory: [
{
id: 'item-1',
name: '药草',
description: '恢复道具',
quantity: 2,
category: 'consumable',
rarity: 'common',
tags: [],
value: 1,
},
{
id: 'item-2',
name: '布料',
description: '材料',
quantity: 3,
category: 'material',
rarity: 'common',
tags: [],
value: 1,
},
],
playerEquipment: {
weapon: null,
armor: null,
relic: null,
},
npcStates: {},
quests: [
{
id: 'quest-1',
issuerNpcId: 'npc-1',
issuerNpcName: '老周',
sceneId: 'scene-1',
title: '寻回包裹',
description: '找回丢失的包裹',
summary: '一项测试任务',
objective: {
kind: 'deliver_item',
targetItemId: 'item-1',
requiredCount: 1,
},
progress: 1,
status: 'completed',
reward: {
affinityBonus: 2,
currency: 10,
items: [],
},
rewardText: '测试奖励',
},
{
id: 'quest-2',
issuerNpcId: 'npc-2',
issuerNpcName: '阿青',
sceneId: 'scene-1',
title: '护送商队',
description: '保护商队通行',
summary: '另一项测试任务',
objective: {
kind: 'reach_scene',
targetSceneId: 'scene-2',
requiredCount: 1,
},
progress: 1,
status: 'turned_in',
reward: {
affinityBonus: 3,
currency: 20,
items: [],
},
rewardText: '测试奖励',
},
],
roster: [
{
npcId: 'npc-roster',
characterId: 'char-roster',
joinedAtAffinity: 10,
hp: 90,
maxHp: 90,
mana: 12,
maxMana: 12,
skillCooldowns: {},
},
],
companions: [
{
npcId: 'npc-active',
characterId: 'char-active',
joinedAtAffinity: 18,
hp: 100,
maxHp: 100,
mana: 16,
maxMana: 16,
skillCooldowns: {},
},
{
npcId: 'npc-encounter',
characterId: 'char-encounter',
joinedAtAffinity: 22,
hp: 88,
maxHp: 88,
mana: 14,
maxMana: 14,
skillCooldowns: {},
},
],
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
};
}
describe('useRpgRuntimeShellViewModel helpers', () => {
it('builds a dialogue indicator only for active npc dialogue playback', () => {
const state = {
...createBaseGameState(),
currentEncounter: {
id: 'npc-encounter',
kind: 'npc' as const,
npcName: '山道客',
npcDescription: '拦路人',
npcAvatar: '/npc.png',
context: '山道相遇',
},
};
const story = {
text: '继续对话',
displayMode: 'dialogue' as const,
dialogue: [
{
speaker: 'npc' as const,
text: '先别急着出手。',
},
{
speaker: 'player' as const,
text: '那你想说什么?',
},
],
options: [],
} satisfies StoryMoment;
expect(
buildRpgRuntimeDialogueIndicator({
isLoading: true,
visibleGameState: state,
visibleCurrentStory: story,
}),
).toEqual({
showPlayer: true,
showEncounter: true,
activeSpeaker: 'player',
});
expect(
buildRpgRuntimeDialogueIndicator({
isLoading: false,
visibleGameState: state,
visibleCurrentStory: story,
}),
).toBeNull();
});
it('derives compact chat summaries and hides the active encounter companion from canvas renders', () => {
const chatSummaries = buildCharacterChatSummaries({
'char-active': {
history: [],
summary: '已经建立起稳定默契。',
updatedAt: null,
},
'char-roster': {
history: [],
summary: '仍在营地观望局势。',
updatedAt: null,
},
} satisfies Record<string, CharacterChatRecord>);
expect(chatSummaries).toEqual({
'char-active': '已经建立起稳定默契。',
'char-roster': '仍在营地观望局势。',
});
const visibleCompanionRenderStates = [
{ npcId: 'npc-active' },
{ npcId: 'npc-encounter' },
] as CompanionRenderState[];
const visibleGameState = {
...createBaseGameState(),
currentEncounter: {
id: 'npc-encounter',
kind: 'npc' as const,
npcName: '山道客',
npcDescription: '拦路人',
npcAvatar: '/npc.png',
context: '山道相遇',
},
};
expect(
buildCanvasCompanionRenderStates({
visibleCompanionRenderStates,
visibleGameState,
}),
).toEqual([{ npcId: 'npc-active' }]);
});
it('aggregates adventure statistics from runtime and visible state slices', () => {
const gameState = createBaseGameState();
const statistics = buildAdventureStatistics({
gameState,
visibleGameState: gameState,
livePlayTimeMs: 3210,
});
expect(statistics).toEqual({
playTimeMs: 3210,
hostileNpcsDefeated: 3,
questsAccepted: 2,
questsCompleted: 2,
questsTurnedIn: 1,
itemsUsed: 4,
scenesTraveled: 5,
currentSceneName: '断桥旧哨',
playerCurrency: 18,
playerLevel: 1,
playerCurrentLevelXp: 0,
playerXpToNextLevel: 60,
playerTotalXp: 0,
inventoryItemCount: 5,
inventoryStackCount: 2,
activeCompanionCount: 2,
rosterCompanionCount: 1,
});
});
});