init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { expect, test } from 'vitest';
import { type Character, type StoryMoment, WorldType } from '../../types';
import { RpgAdventurePanel } from './RpgAdventurePanel';
function createCharacter(): Character {
return {
id: 'hero',
name: '沈行',
title: '试剑客',
description: '测试主角',
backstory: '测试背景',
avatar: '/hero.png',
portrait: '/hero.png',
assetFolder: 'hero',
assetVariant: 'default',
attributes: {
strength: 10,
agility: 10,
intelligence: 8,
spirit: 9,
},
personality: 'calm',
skills: [],
adventureOpenings: {},
} as Character;
}
test('adventure panel renders system turns without special relationship labels', () => {
const currentStory: StoryMoment = {
text: '你们的语气忽然冷了下来。',
displayMode: 'dialogue',
dialogue: [
{ speaker: 'npc', speakerName: '柳无声', text: '这件事你最好别再追问。' },
{ speaker: 'system', text: '这轮交谈先在这里收束。' },
],
options: [],
};
const html = renderToStaticMarkup(
<RpgAdventurePanel
aiError={null}
currentStory={currentStory}
isLoading={false}
displayedOptions={[]}
hideOptions={false}
canRefreshOptions={false}
onRefreshOptions={() => undefined}
onChoice={() => undefined}
onOpenCharacter={() => undefined}
onOpenInventory={() => undefined}
playerCharacter={createCharacter()}
worldType={WorldType.WUXIA}
quests={[]}
questUi={{
acknowledgeQuestCompletion: () => undefined,
claimQuestReward: () => null,
}}
npcChatQuestOfferUi={{
replacePendingOffer: () => false,
abandonPendingOffer: () => false,
acceptPendingOffer: () => null,
}}
goalStack={{
northStarGoal: null,
activeGoal: null,
immediateStepGoal: null,
supportGoals: [],
}}
goalPulse={null}
onDismissGoalPulse={() => undefined}
battleRewardUi={{
reward: null,
dismiss: () => undefined,
}}
playerHp={100}
playerMaxHp={100}
playerMana={20}
playerMaxMana={20}
playerSkillCooldowns={{}}
inBattle={false}
currentNpcBattleMode={null}
statistics={{
playTimeMs: 0,
hostileNpcsDefeated: 0,
questsAccepted: 0,
questsCompleted: 0,
questsTurnedIn: 0,
itemsUsed: 0,
scenesTraveled: 0,
currentSceneName: '竹林古道',
playerCurrency: 0,
inventoryItemCount: 0,
inventoryStackCount: 0,
activeCompanionCount: 0,
rosterCompanionCount: 0,
}}
musicVolume={0.6}
onMusicVolumeChange={() => undefined}
onSaveAndExit={() => undefined}
/>,
);
expect(html).toContain('系统');
expect(html).toContain('这轮交谈先在这里收束。');
expect(html).not.toContain('关系变化');
});
test('adventure panel shows current act label without fixed hostile chat turns', () => {
const currentStory: StoryMoment = {
text: '断桥客仍在压着最后那半句真相。',
displayMode: 'dialogue',
dialogue: [
{ speaker: 'player', text: '你到底还在替谁守着这座桥?' },
{ speaker: 'npc', speakerName: '断桥客', text: '你还没资格知道全名。' },
],
options: [],
npcChatState: {
npcId: 'npc-rival',
npcName: '断桥客',
turnCount: 3,
customInputPlaceholder: '输入你想对 TA 说的话',
sceneActId: 'scene-bridge-act-1',
turnLimit: null,
remainingTurns: null,
limitReason: 'negative_affinity',
forceExitAfterTurn: false,
terminationMode: 'hostile_model',
isHostileChat: true,
},
};
const html = renderToStaticMarkup(
<RpgAdventurePanel
aiError={null}
currentStory={currentStory}
isLoading={false}
displayedOptions={[]}
hideOptions={false}
canRefreshOptions={false}
onRefreshOptions={() => undefined}
onChoice={() => undefined}
onSubmitNpcChatInput={() => true}
onExitNpcChat={() => true}
onOpenCharacter={() => undefined}
onOpenInventory={() => undefined}
playerCharacter={createCharacter()}
worldType={WorldType.WUXIA}
quests={[]}
questUi={{
acknowledgeQuestCompletion: () => undefined,
claimQuestReward: () => null,
}}
npcChatQuestOfferUi={{
replacePendingOffer: () => false,
abandonPendingOffer: () => false,
acceptPendingOffer: () => null,
}}
goalStack={{
northStarGoal: null,
activeGoal: null,
immediateStepGoal: null,
supportGoals: [],
}}
goalPulse={null}
onDismissGoalPulse={() => undefined}
battleRewardUi={{
reward: null,
dismiss: () => undefined,
}}
playerHp={100}
playerMaxHp={100}
playerMana={20}
playerMaxMana={20}
playerSkillCooldowns={{}}
inBattle={false}
currentNpcBattleMode={null}
statistics={{
playTimeMs: 0,
hostileNpcsDefeated: 0,
questsAccepted: 0,
questsCompleted: 0,
questsTurnedIn: 0,
itemsUsed: 0,
scenesTraveled: 0,
currentSceneName: '断桥口',
playerCurrency: 0,
inventoryItemCount: 0,
inventoryStackCount: 0,
activeCompanionCount: 0,
rosterCompanionCount: 0,
}}
musicVolume={0.6}
onMusicVolumeChange={() => undefined}
onSaveAndExit={() => undefined}
currentSceneActTitle="断桥口 · 对峙幕"
currentSceneActIndex={1}
currentSceneActCount={3}
/>,
);
expect(html).toContain('当前幕');
expect(html).toContain('断桥口 · 对峙幕');
expect(html).toContain('1/3');
expect(html).not.toContain('剩余交谈');
});

View File

@@ -0,0 +1,285 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { expect, test } from 'vitest';
import { AnimationState, type Character, type StoryMoment, type StoryOption, WorldType } from '../../types';
import { RpgAdventurePanel } from './RpgAdventurePanel';
function createCharacter(): Character {
return {
id: 'hero',
name: '沈行',
title: '试剑客',
description: '测试主角',
backstory: '测试背景',
avatar: '/hero.png',
portrait: '/hero.png',
assetFolder: 'hero',
assetVariant: 'default',
attributes: {
strength: 10,
agility: 10,
intelligence: 8,
spirit: 9,
},
personality: 'calm',
skills: [],
adventureOpenings: {},
} as Character;
}
function createOption(functionId: string, actionText: string): StoryOption {
return {
functionId,
actionText,
text: actionText,
visuals: {
playerAnimation: AnimationState.IDLE,
playerMoveMeters: 0,
playerOffsetY: 0,
playerFacing: 'right',
scrollWorld: false,
monsterChanges: [],
},
};
}
function renderPanel(
currentStory: StoryMoment,
displayedOptions: StoryOption[],
overrides: {
canRefreshOptions?: boolean;
hideOptions?: boolean;
isLoading?: boolean;
onSubmitNpcChatInput?: (input: string) => boolean;
onExitNpcChat?: () => boolean;
} = {},
) {
return renderToStaticMarkup(
<RpgAdventurePanel
aiError={null}
currentStory={currentStory}
isLoading={overrides.isLoading ?? false}
displayedOptions={displayedOptions}
hideOptions={overrides.hideOptions ?? false}
canRefreshOptions={overrides.canRefreshOptions ?? false}
onRefreshOptions={() => undefined}
onChoice={() => undefined}
onSubmitNpcChatInput={overrides.onSubmitNpcChatInput}
onExitNpcChat={overrides.onExitNpcChat}
onOpenCharacter={() => undefined}
onOpenInventory={() => undefined}
playerCharacter={createCharacter()}
worldType={WorldType.WUXIA}
quests={[]}
questUi={{
acknowledgeQuestCompletion: () => undefined,
claimQuestReward: () => null,
}}
npcChatQuestOfferUi={{
replacePendingOffer: () => false,
abandonPendingOffer: () => false,
acceptPendingOffer: () => null,
}}
goalStack={{
northStarGoal: null,
activeGoal: null,
immediateStepGoal: null,
supportGoals: [],
}}
goalPulse={null}
onDismissGoalPulse={() => undefined}
battleRewardUi={{
reward: null,
dismiss: () => undefined,
}}
playerHp={100}
playerMaxHp={100}
playerMana={20}
playerMaxMana={20}
playerSkillCooldowns={{}}
inBattle={false}
currentNpcBattleMode={null}
statistics={{
playTimeMs: 0,
hostileNpcsDefeated: 0,
questsAccepted: 0,
questsCompleted: 0,
questsTurnedIn: 0,
itemsUsed: 0,
scenesTraveled: 0,
currentSceneName: '竹林古道',
playerCurrency: 0,
inventoryItemCount: 0,
inventoryStackCount: 0,
activeCompanionCount: 0,
rosterCompanionCount: 0,
}}
musicVolume={0.6}
onMusicVolumeChange={() => undefined}
onSaveAndExit={() => undefined}
/>,
);
}
test('adventure panel recognizes story_continue_adventure by function id instead of action text', () => {
const continueOption = createOption('story_continue_adventure', '查看后续');
const currentStory: StoryMoment = {
text: '你们交换完这一轮判断。',
options: [continueOption],
deferredOptions: [createOption('idle_explore_forward', '继续向前探索')],
};
const html = renderPanel(currentStory, [continueOption]);
expect(html).toContain('剧情推理完成,继续后显示新的冒险选项');
});
test('adventure panel does not show deferred hint for non-continue options with the same text', () => {
const misleadingOption = createOption('npc_chat', '查看后续');
const currentStory: StoryMoment = {
text: '你们交换完这一轮判断。',
options: [misleadingOption],
deferredOptions: [createOption('idle_explore_forward', '继续向前探索')],
};
const html = renderPanel(currentStory, [misleadingOption]);
expect(html).not.toContain('剧情推理完成,继续后显示新的冒险选项');
});
test('adventure panel renders compact function tags before option text', () => {
const chatOption = createOption('npc_chat', '继续追问桥上的旧账');
const questOption = createOption('npc_quest_accept', '接下断桥客的委托');
const giftOption = createOption('npc_gift', '把玉牌递给柳无声');
const currentStory: StoryMoment = {
text: '你看向眼前的人。',
options: [chatOption, questOption, giftOption],
};
const html = renderPanel(currentStory, [chatOption, questOption, giftOption]);
expect(html).toContain('聊天');
expect(html).toContain('继续追问桥上的旧账');
expect(html).toContain('任务');
expect(html).toContain('接下断桥客的委托');
expect(html).toContain('送礼');
expect(html).toContain('把玉牌递给柳无声');
});
test('adventure panel shows npc chat custom input and exit button in chat mode', () => {
const optionA = createOption('npc_chat', '先听对方把话说完');
const optionB = createOption('npc_chat', '顺着这个问题继续追问');
const optionC = createOption('npc_chat', '换个更轻松的语气回应');
const currentStory: StoryMoment = {
text: '你们的对话正在继续。',
displayMode: 'dialogue',
dialogue: [
{ speaker: 'player', text: '你刚才那句话是什么意思?' },
{ speaker: 'npc', speakerName: '柳无声', text: '意思是这件事还没结束。' },
],
options: [optionA, optionB, optionC],
npcAffinityEffect: {
eventId: 'effect-liu-1',
npcId: 'npc-liu',
delta: 3,
},
npcChatState: {
npcId: 'npc-liu',
npcName: '柳无声',
turnCount: 2,
customInputPlaceholder: '输入你想对 TA 说的话',
},
};
const html = renderPanel(currentStory, [optionA, optionB, optionC], {
canRefreshOptions: true,
onSubmitNpcChatInput: () => true,
onExitNpcChat: () => true,
});
expect(html).toContain('退出聊天');
expect(html).toContain('输入你想对 TA 说的话');
expect(html).toContain('发送');
expect(html).toContain('换一换');
expect(html).not.toContain('关系升温');
});
test('adventure panel hides custom input and shows quest offer actions during npc quest offer mode', () => {
const viewOption = createOption('npc_chat_quest_offer_view', '查看任务');
viewOption.runtimePayload = {
npcChatQuestOfferAction: 'view',
};
const replaceOption = createOption('npc_chat_quest_offer_replace', '更换任务');
replaceOption.runtimePayload = {
npcChatQuestOfferAction: 'replace',
};
const abandonOption = createOption('npc_chat_quest_offer_abandon', '放弃任务');
abandonOption.runtimePayload = {
npcChatQuestOfferAction: 'abandon',
};
const currentStory: StoryMoment = {
text: '柳无声把真正的委托说了出来。',
displayMode: 'dialogue',
dialogue: [
{ speaker: 'player', text: '你像是还有别的话想说。' },
{ speaker: 'npc', speakerName: '柳无声', text: '确实有一件事想正式托付给你。' },
],
options: [viewOption, replaceOption, abandonOption],
npcChatState: {
npcId: 'npc-liu',
npcName: '柳无声',
turnCount: 2,
customInputPlaceholder: '输入你想对 TA 说的话',
pendingQuestOffer: {
quest: {
id: 'quest-liu-1',
issuerNpcId: 'npc-liu',
issuerNpcName: '柳无声',
sceneId: 'scene-bamboo',
title: '竹林密信',
description: '替柳无声查清竹林中的密信来源。',
summary: '去竹林查清密信来源。',
objective: {
kind: 'inspect_treasure',
requiredCount: 1,
},
progress: 0,
status: 'active',
reward: {
affinityBonus: 5,
currency: 10,
items: [],
},
rewardText: '完成后可获得报酬。',
},
},
},
};
const html = renderPanel(currentStory, [viewOption, replaceOption, abandonOption], {
onSubmitNpcChatInput: () => true,
onExitNpcChat: () => true,
});
expect(html).toContain('查看任务');
expect(html).toContain('更换任务');
expect(html).toContain('放弃任务');
expect(html).not.toContain('发送');
expect(html).not.toContain('输入你想对 TA 说的话');
});
test('adventure panel renders narrative story text without italics and hides option detail text', () => {
const option = createOption('idle_observe_signs', '观察风里残下的痕迹');
option.detailText = '这段说明不应该继续出现在 UI 里。';
const currentStory: StoryMoment = {
text: '风从桥洞里灌过来,你把注意力重新放回脚下与前路。',
options: [option],
};
const html = renderPanel(currentStory, [option]);
expect(html).toContain('font-serif');
expect(html).not.toContain('italic');
expect(html).toContain('text-[15px]');
expect(html).not.toContain('这段说明不应该继续出现在 UI 里。');
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
import { lazy, Suspense } from 'react';
import type {
BattleRewardUi,
CharacterChatUi,
GoalFlowUi,
InventoryFlowUi,
NpcChatQuestOfferUi,
QuestFlowUi,
} from '../../hooks/rpg-runtime-story';
import type { BottomTab } from '../../hooks/rpg-session';
import type {
CompanionRenderState,
GameState,
StoryMoment,
StoryOption,
} from '../../types';
import { getNineSliceStyle, TAB_ICONS, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import { PanelLoadingFallback } from '../rpg-runtime-shell/rpgRuntimeLoaders';
import type { RpgAdventureStatistics } from '../rpg-runtime-shell/types';
const RpgAdventurePanel = lazy(async () => {
const module = await import('./RpgAdventurePanel');
return {
default: module.RpgAdventurePanel,
};
});
const CharacterPanel = lazy(async () => {
const module = await import('../CharacterPanel');
return {
default: module.CharacterPanel,
};
});
const InventoryPanel = lazy(async () => {
const module = await import('../InventoryPanel');
return {
default: module.InventoryPanel,
};
});
export interface RpgRuntimePanelRouterProps {
visibleGameState: GameState;
visibleCurrentStory: StoryMoment;
isLoading: boolean;
aiError: string | null;
bottomTab: BottomTab;
setBottomTab: (tab: BottomTab) => void;
displayedOptions: StoryOption[];
hideStoryOptions: boolean;
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
refreshNpcChatOptions: () => boolean;
handleSceneTransitionChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
characterChatUi: CharacterChatUi;
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
questUi: QuestFlowUi;
npcChatQuestOfferUi: NpcChatQuestOfferUi;
goalUi: GoalFlowUi;
companionRenderStates: CompanionRenderState[];
characterChatSummaries: Record<string, string>;
openOverlayPanel: (panel: 'character' | 'inventory') => void;
openCampModal: () => void;
openPartyMemberDetails: (selection: GameCanvasEntitySelection) => void;
adventureStatistics: RpgAdventureStatistics;
musicVolume: number;
onMusicVolumeChange: (value: number) => void;
onSaveAndExit: () => void;
}
/**
* RPG 运行态主面板路由器。
* 只负责冒险 / 角色 / 背包三个主标签的切换和装配。
*/
export function RpgRuntimePanelRouter({
visibleGameState,
visibleCurrentStory,
isLoading,
aiError,
bottomTab,
setBottomTab,
displayedOptions,
hideStoryOptions,
canRefreshOptions,
handleRefreshOptions,
refreshNpcChatOptions,
handleSceneTransitionChoice,
handleNpcChatInput,
exitNpcChat,
characterChatUi,
inventoryUi,
battleRewardUi,
questUi,
npcChatQuestOfferUi,
goalUi,
companionRenderStates,
characterChatSummaries,
openOverlayPanel,
openCampModal,
openPartyMemberDetails,
adventureStatistics,
musicVolume,
onMusicVolumeChange,
onSaveAndExit,
}: RpgRuntimePanelRouterProps) {
const playerCharacter = visibleGameState.playerCharacter;
if (!playerCharacter) {
return null;
}
return (
<>
<div className="story-top-tabs mb-3 grid grid-cols-3 gap-2 sm:gap-3">
<button
onClick={() => setBottomTab('character')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'character' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'character'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'character'
? TAB_ICONS.character.active
: TAB_ICONS.character.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'character' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
<button
onClick={() => setBottomTab('adventure')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'adventure' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'adventure'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'adventure'
? TAB_ICONS.adventure.active
: TAB_ICONS.adventure.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'adventure' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
<button
onClick={() => setBottomTab('inventory')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'inventory' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'inventory'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'inventory'
? TAB_ICONS.inventory.active
: TAB_ICONS.inventory.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'inventory' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
</div>
{bottomTab === 'character' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载角色面板" />}>
<CharacterPanel
worldType={visibleGameState.worldType}
customWorldProfile={visibleGameState.customWorldProfile}
playerCharacter={playerCharacter}
playerProgression={visibleGameState.playerProgression ?? null}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
playerEquipment={visibleGameState.playerEquipment}
activeBuildBuffs={visibleGameState.activeBuildBuffs}
companionRenderStates={companionRenderStates}
npcStates={visibleGameState.npcStates}
quests={visibleGameState.quests}
companionArcStates={
visibleGameState.storyEngineMemory?.companionArcStates ?? []
}
companionResolutions={
visibleGameState.storyEngineMemory?.companionResolutions ?? []
}
onOpenCamp={openCampModal}
onOpenCharacterChat={characterChatUi.openChat}
chatSummaries={characterChatSummaries}
onInspectMember={openPartyMemberDetails}
/>
</Suspense>
)}
{bottomTab === 'adventure' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载冒险面板" />}>
<RpgAdventurePanel
aiError={aiError}
currentStory={visibleCurrentStory}
isLoading={isLoading}
displayedOptions={displayedOptions}
hideOptions={hideStoryOptions}
canRefreshOptions={
visibleCurrentStory.npcChatState
? visibleCurrentStory.options.length > 1
: canRefreshOptions
}
onRefreshOptions={() => {
if (visibleCurrentStory.npcChatState) {
refreshNpcChatOptions();
return;
}
handleRefreshOptions();
}}
onChoice={handleSceneTransitionChoice}
onSubmitNpcChatInput={handleNpcChatInput}
onExitNpcChat={exitNpcChat}
onOpenCharacter={() => openOverlayPanel('character')}
onOpenInventory={() => openOverlayPanel('inventory')}
playerCharacter={playerCharacter}
worldType={visibleGameState.worldType}
quests={visibleGameState.quests}
questUi={questUi}
npcChatQuestOfferUi={npcChatQuestOfferUi}
goalStack={goalUi.goalStack}
goalPulse={goalUi.pulse}
onDismissGoalPulse={goalUi.dismissPulse}
battleRewardUi={battleRewardUi}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
playerSkillCooldowns={visibleGameState.playerSkillCooldowns}
inBattle={visibleGameState.inBattle}
currentNpcBattleMode={visibleGameState.currentNpcBattleMode}
chapterState={visibleGameState.chapterState ?? null}
journeyBeat={
visibleGameState.storyEngineMemory?.currentJourneyBeat ?? null
}
statistics={adventureStatistics}
musicVolume={musicVolume}
onMusicVolumeChange={onMusicVolumeChange}
onSaveAndExit={onSaveAndExit}
/>
</Suspense>
)}
{bottomTab === 'inventory' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载背包面板" />}>
<InventoryPanel
playerCharacter={playerCharacter}
worldType={visibleGameState.worldType}
playerInventory={visibleGameState.playerInventory}
playerCurrency={visibleGameState.playerCurrency}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
inBattle={visibleGameState.inBattle}
onUseItem={inventoryUi.useInventoryItem}
onEquipItem={inventoryUi.equipInventoryItem}
forgeRecipes={inventoryUi.forgeRecipes}
onCraftRecipe={inventoryUi.craftRecipe}
onDismantleItem={inventoryUi.dismantleItem}
onReforgeItem={inventoryUi.reforgeItem}
continueGameDigest={
visibleGameState.storyEngineMemory?.continueGameDigest ?? null
}
narrativeCodex={
visibleGameState.storyEngineMemory?.narrativeCodex ?? []
}
narrativeQaReport={
visibleGameState.storyEngineMemory?.narrativeQaReport ?? null
}
/>
</Suspense>
)}
</>
);
}
export default RpgRuntimePanelRouter;

View File

@@ -0,0 +1,8 @@
export {
RpgAdventurePanel,
type RpgAdventurePanelProps,
} from './RpgAdventurePanel';
export {
RpgRuntimePanelRouter,
type RpgRuntimePanelRouterProps,
} from './RpgRuntimePanelRouter';