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

256 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { GameState, StoryMoment } from '../types';
import { WorldType } from '../types';
import {
rehydrateSavedSnapshot,
resolveHydratedSnapshotState,
} from './runtimeSnapshot';
function createStory(text: string, streaming = false): StoryMoment {
return {
text,
options: [],
streaming,
};
}
function createHydratedBattleSnapshot(
gameStateOverrides: Partial<GameState> = {},
) {
return {
version: 3,
savedAt: '2026-04-14T00:00:00.000Z',
bottomTab: 'adventure' as const,
currentStory: createStory('战斗故事'),
gameState: {
worldType: WorldType.WUXIA,
customWorldProfile: null,
playerCharacter: {
id: 'hero',
},
runtimeActionVersion: 3,
runtimeSessionId: 'runtime-main',
currentScene: 'Story',
runtimeStats: {
playTimeMs: 0,
lastPlayTickAt: null,
hostileNpcsDefeated: 0,
questsAccepted: 0,
itemsUsed: 0,
scenesTraveled: 0,
},
storyHistory: [],
characterChats: {},
animationState: 'idle',
currentEncounter: {
kind: 'npc',
id: 'npc-fighter',
npcName: '断桥客',
npcDescription: '拦路的刀客',
context: '断桥对峙',
hostile: false,
},
npcInteractionActive: false,
currentScenePreset: null,
sceneHostileNpcs: [
{
id: 'npc-fighter',
name: '断桥客',
hp: 18,
maxHp: 32,
description: '拦路的刀客',
levelProfile: {
level: 4,
referenceStrength: 202,
progressionRole: 'rival',
source: 'manual',
},
experienceReward: 20,
},
],
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
playerActionMode: 'idle',
scrollWorld: false,
inBattle: true,
playerHp: 40,
playerMaxHp: 40,
playerMana: 18,
playerMaxMana: 18,
playerSkillCooldowns: {},
activeCombatEffects: [],
playerCurrency: 0,
playerInventory: [],
playerEquipment: {
weapon: null,
armor: null,
relic: null,
},
npcStates: {
'npc-fighter': {
affinity: -16,
chattedCount: 0,
helpUsed: false,
giftsGiven: 0,
inventory: [],
recruited: false,
},
},
quests: [],
roster: [],
companions: [],
currentBattleNpcId: 'npc-fighter',
currentNpcBattleMode: 'fight',
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
...gameStateOverrides,
} as unknown as GameState,
};
}
describe('runtimeSnapshot', () => {
it('keeps server-hydrated snapshots unchanged', () => {
const snapshot = {
gameState: {
playerCharacter: {
id: 'sword-princess',
},
playerEquipment: {
weapon: null,
armor: null,
relic: null,
},
runtimeActionVersion: 3,
runtimeSessionId: 'runtime-main',
} as GameState,
currentStory: createStory('服务端恢复故事'),
bottomTab: 'inventory',
};
expect(resolveHydratedSnapshotState(snapshot)).toBe(snapshot);
});
it('only applies minimal local shape hydration for non-hydrated legacy snapshots', () => {
const snapshot = {
gameState: {
worldType: 'WUXIA',
customWorldProfile: null,
playerCharacter: {
id: 'sword-princess',
},
playerHp: 999,
playerMaxHp: 12,
playerMana: 999,
playerMaxMana: 12,
runtimeActionVersion: undefined,
runtimeSessionId: undefined,
playerEquipment: null,
} as unknown as GameState,
currentStory: createStory('旧快照故事', true),
bottomTab: 'unknown',
};
const hydrated = resolveHydratedSnapshotState(snapshot);
expect(hydrated.bottomTab).toBe('adventure');
expect(hydrated.currentStory?.streaming).toBe(false);
expect(hydrated.gameState.playerEquipment).toEqual({
weapon: null,
armor: null,
relic: null,
});
expect(hydrated.gameState.playerProgression).toEqual({
level: 1,
currentLevelXp: 0,
totalXp: 0,
xpToNextLevel: 60,
pendingLevelUps: 0,
lastGrantedSource: null,
});
expect(hydrated.gameState.playerMaxHp).toBe(12);
expect(hydrated.gameState.playerHp).toBe(12);
expect(hydrated.gameState.playerMaxMana).toBe(12);
expect(hydrated.gameState.playerMana).toBe(12);
});
it('rehydrates minimal npc battle snapshots into renderable combatants', () => {
const snapshot = createHydratedBattleSnapshot();
const hydrated = rehydrateSavedSnapshot(snapshot);
const hostileNpc = hydrated.gameState.sceneHostileNpcs[0];
expect(hydrated).not.toBe(snapshot);
expect(hostileNpc).toEqual(
expect.objectContaining({
id: 'npc-fighter',
name: '断桥客',
description: '拦路的刀客',
hp: 18,
maxHp: 32,
levelProfile: {
level: 4,
referenceStrength: 202,
progressionRole: 'rival',
source: 'manual',
},
experienceReward: 20,
attackRange: expect.any(Number),
speed: expect.any(Number),
animation: 'idle',
renderKind: 'npc',
encounter: expect.objectContaining({
kind: 'npc',
id: 'npc-fighter',
npcName: '断桥客',
xMeters: expect.any(Number),
}),
}),
);
});
it('does not rewrite already renderable npc battle snapshots', () => {
const snapshot = createHydratedBattleSnapshot({
sceneHostileNpcs: [
{
id: 'npc-fighter',
name: '断桥客',
action: '摆开架势,随时准备出手',
description: '拦路的刀客',
animation: 'idle',
xMeters: 3.2,
yOffset: 0,
facing: 'left',
attackRange: 1.8,
speed: 7,
hp: 18,
maxHp: 32,
levelProfile: {
level: 4,
referenceStrength: 202,
progressionRole: 'rival',
source: 'manual',
},
experienceReward: 20,
renderKind: 'npc',
encounter: {
kind: 'npc',
id: 'npc-fighter',
npcName: '断桥客',
npcDescription: '拦路的刀客',
npcAvatar: '',
context: '断桥对峙',
xMeters: 3.2,
},
},
],
});
expect(rehydrateSavedSnapshot(snapshot)).toBe(snapshot);
});
});