Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-08 11:58:47 +08:00
parent 9d2fc9e4b8
commit bd9fdcbe31
170 changed files with 18259 additions and 1049 deletions

View 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);
});
});