1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 11:30:19 +08:00
parent 50759f3c1e
commit 8a7bd90458
85 changed files with 7290 additions and 1903 deletions

View File

@@ -7,10 +7,7 @@ import {
resolveHydratedSnapshotState,
} from './runtimeSnapshot';
function createStory(
text: string,
streaming = false,
): StoryMoment {
function createStory(text: string, streaming = false): StoryMoment {
return {
text,
options: [],
@@ -63,6 +60,13 @@ function createHydratedBattleSnapshot(
hp: 18,
maxHp: 32,
description: '拦路的刀客',
levelProfile: {
level: 4,
referenceStrength: 202,
progressionRole: 'rival',
source: 'manual',
},
experienceReward: 20,
},
],
playerX: 0,
@@ -160,6 +164,14 @@ describe('runtimeSnapshot', () => {
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);
@@ -180,6 +192,13 @@ describe('runtimeSnapshot', () => {
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',
@@ -210,6 +229,13 @@ describe('runtimeSnapshot', () => {
speed: 7,
hp: 18,
maxHp: 32,
levelProfile: {
level: 4,
referenceStrength: 202,
progressionRole: 'rival',
source: 'manual',
},
experienceReward: 20,
renderKind: 'npc',
encounter: {
kind: 'npc',

View File

@@ -2,6 +2,7 @@ import {
buildInitialNpcState,
createNpcBattleMonster,
} from '../data/npcInteractions';
import { normalizePlayerProgressionState } from '../data/playerProgression';
import type {
Encounter,
GameState,
@@ -18,9 +19,7 @@ import type {
SnapshotState,
} from './runtimeSnapshotTypes';
function normalizeBottomTab(
bottomTab: string | null | undefined,
): BottomTab {
function normalizeBottomTab(bottomTab: string | null | undefined): BottomTab {
return bottomTab === 'character' || bottomTab === 'inventory'
? bottomTab
: 'adventure';
@@ -106,6 +105,8 @@ function normalizeRuntimeBattleEncounter(
typeof encounter.npcAvatar === 'string' ? encounter.npcAvatar : '',
context: typeof encounter.context === 'string' ? encounter.context : '',
hostile: true,
levelProfile: encounter.levelProfile,
experienceReward: encounter.experienceReward,
} satisfies Encounter;
}
@@ -126,9 +127,7 @@ function resolveRuntimeNpcBattleState(
}
const npcStateKey =
gameState.currentBattleNpcId ??
encounter.id ??
encounter.npcName;
gameState.currentBattleNpcId ?? encounter.id ?? encounter.npcName;
const npcState =
gameState.npcStates[npcStateKey] ??
buildInitialNpcState(
@@ -161,9 +160,13 @@ function hydrateRuntimeNpcBattleMonster(params: {
);
const candidate = params.hostileNpc as Partial<SceneHostileNpc>;
const xMeters =
typeof candidate.xMeters === 'number' ? candidate.xMeters : template.xMeters;
typeof candidate.xMeters === 'number'
? candidate.xMeters
: template.xMeters;
const yOffset =
typeof candidate.yOffset === 'number' ? candidate.yOffset : template.yOffset;
typeof candidate.yOffset === 'number'
? candidate.yOffset
: template.yOffset;
return {
...template,
@@ -198,6 +201,11 @@ function hydrateRuntimeNpcBattleMonster(params: {
: template.attackRange,
speed:
typeof candidate.speed === 'number' ? candidate.speed : template.speed,
levelProfile: candidate.levelProfile ?? template.levelProfile,
experienceReward:
typeof candidate.experienceReward === 'number'
? candidate.experienceReward
: template.experienceReward,
encounter: {
...template.encounter,
xMeters,
@@ -263,6 +271,9 @@ export function normalizeSavedGameState(gameState: GameState) {
return hydrateRuntimeNpcBattleGameState({
...hydratableState,
playerProgression: normalizePlayerProgressionState(
hydratableState.playerProgression ?? null,
),
playerMaxHp,
playerHp: Math.min(hydratableState.playerHp, playerMaxHp),
playerMaxMana,
@@ -305,14 +316,19 @@ export function isHydratedSnapshotState(
(gameState.runtimeSessionId === null ||
typeof gameState.runtimeSessionId === 'string') &&
(!gameState.playerCharacter ||
Boolean(gameState.playerEquipment && typeof gameState.playerEquipment === 'object')),
Boolean(
gameState.playerEquipment &&
typeof gameState.playerEquipment === 'object',
)),
);
}
export function rehydrateSavedSnapshot<T extends HydratedSnapshotState>(
snapshot: T,
): T {
const hydratedGameState = hydrateRuntimeNpcBattleGameState(snapshot.gameState);
const hydratedGameState = hydrateRuntimeNpcBattleGameState(
snapshot.gameState,
);
if (hydratedGameState === snapshot.gameState) {
return snapshot;