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

@@ -4,6 +4,7 @@ import type { BottomTab } from '../../hooks/useGameFlow';
import type {
BattleRewardUi,
CharacterChatUi,
GoalFlowUi,
InventoryFlowUi,
QuestFlowUi,
} from '../../hooks/useStoryGeneration';
@@ -62,6 +63,7 @@ export function GameShellMainContent({
inventoryUi,
battleRewardUi,
questUi,
goalUi,
companionRenderStates,
characterChatSummaries,
openOverlayPanel,
@@ -98,6 +100,7 @@ export function GameShellMainContent({
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
questUi: QuestFlowUi;
goalUi: GoalFlowUi;
companionRenderStates: CompanionRenderState[];
characterChatSummaries: Record<string, string>;
openOverlayPanel: (panel: 'character' | 'inventory') => void;
@@ -171,6 +174,7 @@ export function GameShellMainContent({
inventoryUi={inventoryUi}
battleRewardUi={battleRewardUi}
questUi={questUi}
goalUi={goalUi}
companionRenderStates={companionRenderStates}
characterChatSummaries={characterChatSummaries}
openOverlayPanel={openOverlayPanel}

View File

@@ -33,6 +33,7 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
inventoryUi,
battleRewardUi,
questUi,
goalUi,
} = story;
const {
hasSavedGame,
@@ -43,7 +44,12 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
handleBackToWorldSelect,
handleCharacterSelect,
} = entry;
const {companionRenderStates, onBenchCompanion, onActivateRosterCompanion} = companions;
const {
companionRenderStates,
buildCompanionRenderStates,
onBenchCompanion,
onActivateRosterCompanion,
} = companions;
const {musicVolume, onMusicVolumeChange} = audio;
const [clockNow, setClockNow] = useState(() => Date.now());
@@ -119,13 +125,18 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
[gameState.characterChats],
);
const visibleCompanionRenderStates = useMemo(
() => buildCompanionRenderStates(visibleGameState),
[buildCompanionRenderStates, visibleGameState],
);
const canvasCompanionRenderStates = useMemo(() => {
const activeEncounterNpcId = visibleGameState.currentEncounter?.kind === 'npc'
? visibleGameState.currentEncounter.id ?? null
: null;
if (!activeEncounterNpcId) return companionRenderStates;
return companionRenderStates.filter(companion => companion.npcId !== activeEncounterNpcId);
}, [companionRenderStates, visibleGameState.currentEncounter]);
if (!activeEncounterNpcId) return visibleCompanionRenderStates;
return visibleCompanionRenderStates.filter(companion => companion.npcId !== activeEncounterNpcId);
}, [visibleCompanionRenderStates, visibleGameState.currentEncounter]);
const livePlayTimeMs = useMemo(
() => getLiveGamePlayTimeMs(gameState.runtimeStats, clockNow),
@@ -229,6 +240,7 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
inventoryUi={inventoryUi}
battleRewardUi={battleRewardUi}
questUi={questUi}
goalUi={goalUi}
companionRenderStates={companionRenderStates}
characterChatSummaries={characterChatSummaries}
openOverlayPanel={openOverlayPanel}

View File

@@ -4,6 +4,7 @@ import type { BottomTab } from '../../hooks/useGameFlow';
import type {
BattleRewardUi,
CharacterChatUi,
GoalFlowUi,
InventoryFlowUi,
QuestFlowUi,
} from '../../hooks/useStoryGeneration';
@@ -69,6 +70,7 @@ export function GameShellStoryPanels({
inventoryUi,
battleRewardUi,
questUi,
goalUi,
companionRenderStates,
characterChatSummaries,
openOverlayPanel,
@@ -94,6 +96,7 @@ export function GameShellStoryPanels({
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
questUi: QuestFlowUi;
goalUi: GoalFlowUi;
companionRenderStates: CompanionRenderState[];
characterChatSummaries: Record<string, string>;
openOverlayPanel: (panel: 'character' | 'inventory') => void;
@@ -199,6 +202,9 @@ export function GameShellStoryPanels({
worldType={visibleGameState.worldType}
quests={visibleGameState.quests}
questUi={questUi}
goalStack={goalUi.goalStack}
goalPulse={goalUi.pulse}
onDismissGoalPulse={goalUi.dismissPulse}
battleRewardUi={battleRewardUi}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
@@ -211,15 +217,6 @@ export function GameShellStoryPanels({
journeyBeat={
visibleGameState.storyEngineMemory?.currentJourneyBeat ?? null
}
recentChronicleSummary={
visibleGameState.storyEngineMemory?.continueGameDigest ?? null
}
currentCampEvent={
visibleGameState.storyEngineMemory?.currentCampEvent ?? null
}
setpieceDirective={
visibleGameState.storyEngineMemory?.currentSetpieceDirective ?? null
}
statistics={adventureStatistics}
musicVolume={musicVolume}
onMusicVolumeChange={onMusicVolumeChange}

View File

@@ -8,6 +8,7 @@ import {
readSavedCustomWorldProfiles,
upsertSavedCustomWorldProfile,
} from '../../data/customWorldLibrary';
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
import { getScenePreset } from '../../data/scenePresets';
import {
type CustomWorldGenerationProgress,
@@ -18,6 +19,7 @@ import {
buildCustomWorldCreatorIntentGenerationText,
createEmptyCustomWorldCreatorIntent,
} from '../../services/customWorldCreatorIntent';
import { detectCustomWorldThemeMode } from '../../services/customWorldTheme';
import {
type CustomWorldCreatorIntent,
type CustomWorldGenerationMode,
@@ -217,30 +219,24 @@ export function PreGameSelectionFlow({
const savedCustomWorldCards = useMemo(
() =>
savedCustomWorldProfiles.map((profile, index) => {
const anchorWorldType = profile.templateWorldType;
savedCustomWorldProfiles.map((profile) => {
const themeMode = detectCustomWorldThemeMode(profile);
const leadCharacter =
buildCustomWorldPlayableCharacters(profile)[0] ?? null;
return {
id: profile.id,
profile,
texture:
anchorWorldType === WorldType.WUXIA
? UI_CHROME.worldButtonWuxia
: UI_CHROME.worldButtonXianxia,
sceneImage:
profile.landmarks[0]?.imageSrc ??
getScenePreset(anchorWorldType, (index % 3) + 1)?.imageSrc ??
getScenePreset(anchorWorldType, 0)?.imageSrc ??
'',
texture: UI_CHROME.panel,
sceneImage: resolveCustomWorldCampSceneImage(profile) ?? '',
featurePortrait: leadCharacter?.portrait ?? '',
featureIcon:
anchorWorldType === WorldType.WUXIA
themeMode === 'martial'
? WORLD_SELECT_ICONS.wuxia
: WORLD_SELECT_ICONS.xianxia,
accentLabel:
anchorWorldType === WorldType.WUXIA ? '武侠基础' : '仙侠基础',
: themeMode === 'arcane'
? WORLD_SELECT_ICONS.xianxia
: CHROME_ICONS.refreshOptions,
accentLabel: '自定义世界',
};
}),
[savedCustomWorldProfiles],
@@ -900,10 +896,8 @@ export function PreGameSelectionFlow({
<div className="rounded-full border border-sky-300/20 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
</div>
<div className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
{world.accentLabel === '武侠基础'
? '武侠'
: '仙侠'}
<div className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
{world.accentLabel}
</div>
</div>
<div className="mt-auto">

View File

@@ -2,6 +2,7 @@ import type { BottomTab } from '../../hooks/useGameFlow';
import type {
BattleRewardUi,
CharacterChatUi,
GoalFlowUi,
InventoryFlowUi,
QuestFlowUi,
StoryGenerationNpcUi,
@@ -37,6 +38,7 @@ export interface GameShellStoryProps {
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
questUi: QuestFlowUi;
goalUi: GoalFlowUi;
}
export interface GameShellEntryProps {
@@ -51,6 +53,7 @@ export interface GameShellEntryProps {
export interface GameShellCompanionProps {
companionRenderStates: CompanionRenderState[];
buildCompanionRenderStates: (state: GameState) => CompanionRenderState[];
onBenchCompanion: (npcId: string) => void;
onActivateRosterCompanion: (npcId: string, swapNpcId?: string | null) => void;
}