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

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getCharacterAnimationDurationMs } from '../data/characterCombat';
import { getCharacterById } from '../data/characterPresets';
@@ -12,6 +12,9 @@ type CompanionRecruitPresentation = {
recruitToken: number;
};
type CompanionPresentationMap = Record<string, CompanionRecruitPresentation>;
type CompanionObserveFacingMap = Record<string, 'left' | 'right'>;
const RECRUIT_ENTRY_OFFSET_X = 148;
const RECRUIT_ENTRY_OFFSET_Y = 10;
const MIN_RECRUIT_PHASE_MS = 180;
@@ -26,6 +29,53 @@ function randomObservePause() {
return Math.round(OBSERVE_MIN_PAUSE_MS + Math.random() * (OBSERVE_MAX_PAUSE_MS - OBSERVE_MIN_PAUSE_MS));
}
export function buildCompanionRenderStatesForGameState(params: {
gameState: GameState;
presentationByNpcId?: CompanionPresentationMap;
observeFacingByNpcId?: CompanionObserveFacingMap;
}) {
const {
gameState,
presentationByNpcId = {},
observeFacingByNpcId = {},
} = params;
return (gameState.companions ?? [])
.map((companion, index) => {
const character = getCharacterById(companion.characterId);
if (!character) return null;
const presentation = presentationByNpcId[companion.npcId];
return {
npcId: companion.npcId,
character,
hp: companion.hp,
maxHp: companion.maxHp,
mana: companion.mana,
maxMana: companion.maxMana,
skillCooldowns: companion.skillCooldowns,
animationState: presentation?.animationState ?? (
companion.hp <= 0
? companion.animationState ?? AnimationState.DIE
: gameState.scrollWorld
? AnimationState.RUN
: gameState.inBattle
? companion.animationState ?? AnimationState.IDLE
: gameState.animationState
),
actionMode: companion.actionMode ?? 'idle',
slot: index % 2 === 0 ? 'upper' : 'lower',
facing: observeFacingByNpcId[companion.npcId] ?? gameState.playerFacing,
entryOffsetX: (presentation?.entryOffsetX ?? 0) + (companion.offsetX ?? 0),
entryOffsetY: (presentation?.entryOffsetY ?? 0) + (companion.offsetY ?? 0),
transitionMs: presentation?.transitionMs ?? companion.transitionMs ?? 0,
recruitToken: presentation?.recruitToken,
} satisfies CompanionRenderState;
})
.filter(Boolean) as CompanionRenderState[];
}
export function useNpcInteractionFlow(gameState: GameState) {
const [presentationByNpcId, setPresentationByNpcId] = useState<Record<string, CompanionRecruitPresentation>>({});
const [observeFacingByNpcId, setObserveFacingByNpcId] = useState<Record<string, 'left' | 'right'>>({});
@@ -216,42 +266,16 @@ export function useNpcInteractionFlow(gameState: GameState) {
});
}, [gameState.ambientIdleMode, gameState.companions]);
const companionRenderStates: CompanionRenderState[] = (gameState.companions ?? [])
.map((companion, index) => {
const character = getCharacterById(companion.characterId);
if (!character) return null;
const buildCompanionRenderStates = useCallback((state: GameState) => buildCompanionRenderStatesForGameState({
gameState: state,
presentationByNpcId,
observeFacingByNpcId,
}), [observeFacingByNpcId, presentationByNpcId]);
const presentation = presentationByNpcId[companion.npcId];
return {
npcId: companion.npcId,
character,
hp: companion.hp,
maxHp: companion.maxHp,
mana: companion.mana,
maxMana: companion.maxMana,
skillCooldowns: companion.skillCooldowns,
animationState: presentation?.animationState ?? (
companion.hp <= 0
? companion.animationState ?? AnimationState.DIE
: gameState.scrollWorld
? AnimationState.RUN
: gameState.inBattle
? companion.animationState ?? AnimationState.IDLE
: gameState.animationState
),
actionMode: companion.actionMode ?? 'idle',
slot: index % 2 === 0 ? 'upper' : 'lower',
facing: observeFacingByNpcId[companion.npcId] ?? gameState.playerFacing,
entryOffsetX: (presentation?.entryOffsetX ?? 0) + (companion.offsetX ?? 0),
entryOffsetY: (presentation?.entryOffsetY ?? 0) + (companion.offsetY ?? 0),
transitionMs: presentation?.transitionMs ?? companion.transitionMs ?? 0,
recruitToken: presentation?.recruitToken,
};
})
.filter(Boolean) as CompanionRenderState[];
const companionRenderStates = buildCompanionRenderStates(gameState);
return {
companionRenderStates,
buildCompanionRenderStates,
};
}