1
This commit is contained in:
@@ -1383,9 +1383,6 @@ export function AdventureEntityModal({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] leading-5 text-zinc-500">
|
||||
{attribute.definition}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -331,7 +331,7 @@ export function CharacterAttributeGrid({
|
||||
boostedCombatStats,
|
||||
resourceLabels,
|
||||
)
|
||||
: slot.combatUseText,
|
||||
: '',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -364,9 +364,11 @@ export function CharacterAttributeGrid({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] leading-relaxed text-sky-200/85">
|
||||
{effectText}
|
||||
</div>
|
||||
{effectText ? (
|
||||
<div className="mt-2 text-[10px] leading-relaxed text-sky-200/85">
|
||||
{effectText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -561,9 +561,6 @@ export function CharacterPanel({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] leading-5 text-zinc-500">
|
||||
{attribute.definition}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -545,16 +545,6 @@ function buildLandmarkSearchText(
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function buildAttributeSlotSummary(
|
||||
slot: CustomWorldProfile['attributeSchema']['slots'][number],
|
||||
) {
|
||||
return compactTextList([
|
||||
slot.combatUseText,
|
||||
slot.socialUseText,
|
||||
slot.explorationUseText,
|
||||
]).join(' / ');
|
||||
}
|
||||
|
||||
export function CustomWorldEntityCatalog({
|
||||
profile,
|
||||
previewCharacters,
|
||||
@@ -984,11 +974,6 @@ export function CustomWorldEntityCatalog({
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
角色维度
|
||||
</div>
|
||||
{profile.attributeSchema?.schemaName ? (
|
||||
<div className="text-xs leading-5 text-zinc-500">
|
||||
{profile.attributeSchema.schemaName}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3 xl:grid-cols-6">
|
||||
{attributeSlots.map((slot) => (
|
||||
@@ -999,9 +984,6 @@ export function CustomWorldEntityCatalog({
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{slot.name}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-[11px] leading-5 text-zinc-400">
|
||||
{buildAttributeSlotSummary(slot) || slot.definition}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
@@ -198,7 +197,6 @@ function createProfile(): CustomWorldProfile {
|
||||
attributeSchema: {
|
||||
id: 'schema-1',
|
||||
worldId: 'world-1',
|
||||
schemaName: '潮雾六维',
|
||||
schemaVersion: 1,
|
||||
generatedFrom: {
|
||||
worldType: 'WUXIA',
|
||||
@@ -211,62 +209,26 @@ function createProfile(): CustomWorldProfile {
|
||||
{
|
||||
slotId: 'axis_a',
|
||||
name: '骨势',
|
||||
definition: '扛住压力并正面推进的底子。',
|
||||
positiveSignals: ['硬顶'],
|
||||
negativeSignals: ['畏缩'],
|
||||
combatUseText: '正面承压与破阵。',
|
||||
socialUseText: '在谈判里稳住立场。',
|
||||
explorationUseText: '穿过危险地形。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_b',
|
||||
name: '身法',
|
||||
definition: '抢位、转场与把握节奏的能力。',
|
||||
positiveSignals: ['灵动'],
|
||||
negativeSignals: ['迟滞'],
|
||||
combatUseText: '移动换位。',
|
||||
socialUseText: '捕捉话锋。',
|
||||
explorationUseText: '快速穿行。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_c',
|
||||
name: '眼脉',
|
||||
definition: '看破破绽、拆解局势的能力。',
|
||||
positiveSignals: ['洞察'],
|
||||
negativeSignals: ['误判'],
|
||||
combatUseText: '识破招式。',
|
||||
socialUseText: '辨别谎言。',
|
||||
explorationUseText: '发现线索。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_d',
|
||||
name: '心焰',
|
||||
definition: '决断、压迫与坚持意志的能力。',
|
||||
positiveSignals: ['果断'],
|
||||
negativeSignals: ['犹疑'],
|
||||
combatUseText: '强行压制。',
|
||||
socialUseText: '立威推进。',
|
||||
explorationUseText: '面对险境不退。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_e',
|
||||
name: '尘缘',
|
||||
definition: '处理人情、承诺和关系牵引的能力。',
|
||||
positiveSignals: ['守信'],
|
||||
negativeSignals: ['冷漠'],
|
||||
combatUseText: '协作配合。',
|
||||
socialUseText: '建立信任。',
|
||||
explorationUseText: '借助人脉。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_f',
|
||||
name: '玄息',
|
||||
definition: '调息、稳态和久战的能力。',
|
||||
positiveSignals: ['沉稳'],
|
||||
negativeSignals: ['浮躁'],
|
||||
combatUseText: '续战恢复。',
|
||||
socialUseText: '保持耐心。',
|
||||
explorationUseText: '长线跋涉。',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -753,7 +715,7 @@ test('基本设定目标打开独立编辑面板', () => {
|
||||
expect(screen.queryByText('编辑世界信息')).toBeNull();
|
||||
});
|
||||
|
||||
test('世界信息面板可以编辑六个角色维度信息', async () => {
|
||||
test('基本设定面板只编辑六个角色维度名称', async () => {
|
||||
const user = userEvent.setup();
|
||||
const savedProfileRef: { current: CustomWorldProfile | null } = {
|
||||
current: null,
|
||||
@@ -762,7 +724,7 @@ test('世界信息面板可以编辑六个角色维度信息', async () => {
|
||||
render(
|
||||
<RpgCreationEntityEditorModal
|
||||
profile={createProfile()}
|
||||
target={{ kind: 'world' }}
|
||||
target={{ kind: 'foundation' }}
|
||||
onClose={() => {}}
|
||||
onProfileChange={(profile) => {
|
||||
savedProfileRef.current = profile;
|
||||
@@ -775,33 +737,15 @@ test('世界信息面板可以编辑六个角色维度信息', async () => {
|
||||
await user.clear(nameInputs[0]!);
|
||||
await user.type(nameInputs[0]!, '潮骨');
|
||||
|
||||
const definitionFields = screen.getAllByLabelText('定义');
|
||||
await user.clear(definitionFields[0]!);
|
||||
await user.type(definitionFields[0]!, '顶住潮压并正面推进的角色底色。');
|
||||
|
||||
const positiveSignalFields = screen.getAllByLabelText('正向信号');
|
||||
fireEvent.change(positiveSignalFields[0]!, {
|
||||
target: { value: '硬顶, 护阵' },
|
||||
});
|
||||
|
||||
const combatFields = screen.getAllByLabelText('战斗体现');
|
||||
await user.clear(combatFields[0]!);
|
||||
await user.type(combatFields[0]!, '正面压线与护住阵脚。');
|
||||
expect(screen.queryByLabelText('定义')).toBeNull();
|
||||
expect(screen.queryByLabelText('正向信号')).toBeNull();
|
||||
expect(screen.queryByLabelText('战斗体现')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /保存修改/u }));
|
||||
|
||||
expect(savedProfileRef.current?.attributeSchema.slots[0]?.name).toBe(
|
||||
'潮骨',
|
||||
);
|
||||
expect(savedProfileRef.current?.attributeSchema.slots[0]?.definition).toBe(
|
||||
'顶住潮压并正面推进的角色底色。',
|
||||
);
|
||||
expect(
|
||||
savedProfileRef.current?.attributeSchema.slots[0]?.positiveSignals,
|
||||
).toEqual(['硬顶', '护阵']);
|
||||
expect(savedProfileRef.current?.attributeSchema.slots[0]?.combatUseText).toBe(
|
||||
'正面压线与护住阵脚。',
|
||||
);
|
||||
});
|
||||
|
||||
test('可扮演角色列表使用缩略卡片并点击进入编辑', async () => {
|
||||
|
||||
@@ -55,8 +55,8 @@ test('creation hub reflects updated draft title summary and counts after rerende
|
||||
expect(screen.getByText('角色 3')).toBeTruthy();
|
||||
expect(screen.getByText('地点 4')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /角色扮演 RPG/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /大鱼吃小鱼/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /拼图玩法/u })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||||
|
||||
rerender(
|
||||
<CustomWorldCreationHub
|
||||
|
||||
@@ -43,8 +43,8 @@ test('creation hub draft card renders compiled work summary fields', () => {
|
||||
expect(html).toContain('玩家是失职返乡的守灯人');
|
||||
expect(html).toContain('守灯会与沉船商盟争夺航道解释权');
|
||||
expect(html).toContain('角色扮演 RPG');
|
||||
expect(html).toContain('大鱼吃小鱼');
|
||||
expect(html).toContain('拼图玩法');
|
||||
expect(html).not.toContain('大鱼吃小鱼');
|
||||
});
|
||||
|
||||
test('creation hub renders puzzle works in the same unified list with puzzle tag', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import {
|
||||
PLATFORM_CREATION_TYPES,
|
||||
getVisiblePlatformCreationTypes,
|
||||
type PlatformCreationTypeId,
|
||||
} from '../platform-entry/platformEntryCreationTypes';
|
||||
|
||||
@@ -16,6 +16,10 @@ export function CustomWorldCreationStartCard({
|
||||
error = null,
|
||||
onCreateType,
|
||||
}: CustomWorldCreationStartCardProps) {
|
||||
// 创作首页首屏卡带与创作类型弹层保持同一份展示口径,
|
||||
// 避免某个玩法只在其中一个入口被隐藏而出现状态漂移。
|
||||
const visibleCreationTypes = getVisiblePlatformCreationTypes();
|
||||
|
||||
return (
|
||||
// 移动端限制模块高度,模板入口改为横向滚动,避免挤占作品列表首屏空间。
|
||||
<div className="platform-surface platform-surface--hero relative max-h-[33svh] overflow-hidden px-3 py-3 sm:max-h-none sm:px-5 sm:py-5 xl:px-5 xl:py-4">
|
||||
@@ -34,7 +38,7 @@ export function CustomWorldCreationStartCard({
|
||||
</div>
|
||||
|
||||
<div className="-mx-1 flex snap-x gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:grid sm:gap-3 sm:overflow-visible sm:px-0 sm:pb-0 sm:grid-cols-2 xl:grid-cols-5 xl:gap-2.5">
|
||||
{PLATFORM_CREATION_TYPES.map((item) => {
|
||||
{visibleCreationTypes.map((item) => {
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import { PLATFORM_CREATION_TYPES } from './platformEntryCreationTypes';
|
||||
import { getVisiblePlatformCreationTypes } from './platformEntryCreationTypes';
|
||||
|
||||
export interface PlatformEntryCreationTypeModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +14,7 @@ export interface PlatformEntryCreationTypeModalProps {
|
||||
}
|
||||
|
||||
function CreationTypeCard(props: {
|
||||
item: (typeof PLATFORM_CREATION_TYPES)[number];
|
||||
item: ReturnType<typeof getVisiblePlatformCreationTypes>[number];
|
||||
busy: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
@@ -81,9 +81,7 @@ export function PlatformEntryCreationTypeModal({
|
||||
|
||||
// 平台入口只渲染当前允许展示的创作类型;
|
||||
// 被隐藏的玩法仍保留既有实现与路由,不在这里删除能力本体。
|
||||
const visibleCreationTypes = PLATFORM_CREATION_TYPES.filter(
|
||||
(item) => !item.hidden,
|
||||
);
|
||||
const visibleCreationTypes = getVisiblePlatformCreationTypes();
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
|
||||
@@ -122,6 +122,7 @@ import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultA
|
||||
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
import { isPlatformCreationTypeVisible } from './platformEntryCreationTypes';
|
||||
import {
|
||||
PlatformEntryHomeView,
|
||||
type PlatformHomeTab,
|
||||
@@ -473,6 +474,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
|
||||
const hadReadableProtectedDataRef = useRef(false);
|
||||
const hasInitialAgentSession = Boolean(
|
||||
readCustomWorldAgentUiState().activeSessionId &&
|
||||
@@ -660,7 +662,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
await Promise.allSettled([
|
||||
platformBootstrap.refreshPublishedGallery(),
|
||||
platformBootstrap.refreshCustomWorldWorks(),
|
||||
refreshBigFishGallery(),
|
||||
isBigFishCreationVisible
|
||||
? refreshBigFishGallery()
|
||||
: Promise.resolve([] as BigFishWorkSummary[]),
|
||||
refreshPuzzleGallery(),
|
||||
]);
|
||||
return latestSession;
|
||||
@@ -716,9 +720,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [agentResultPreview]);
|
||||
|
||||
const featuredGalleryEntries = useMemo(() => {
|
||||
const bigFishPublicEntries = bigFishGalleryEntries.map(
|
||||
mapBigFishWorkToPlatformGalleryCard,
|
||||
);
|
||||
const bigFishPublicEntries = isBigFishCreationVisible
|
||||
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
|
||||
: [];
|
||||
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
||||
mapPuzzleWorkToPlatformGalleryCard,
|
||||
);
|
||||
@@ -727,6 +731,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[...bigFishPublicEntries, ...puzzlePublicEntries],
|
||||
).slice(0, 6);
|
||||
}, [
|
||||
isBigFishCreationVisible,
|
||||
bigFishGalleryEntries,
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
puzzleGalleryEntries,
|
||||
@@ -736,11 +741,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
mergePlatformPublicGalleryEntries(
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
[
|
||||
...bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard),
|
||||
...(isBigFishCreationVisible
|
||||
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
|
||||
: []),
|
||||
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
|
||||
],
|
||||
),
|
||||
[
|
||||
isBigFishCreationVisible,
|
||||
bigFishGalleryEntries,
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
puzzleGalleryEntries,
|
||||
@@ -986,7 +994,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
const bigFishError = bigFishFlow.error;
|
||||
const setBigFishError = bigFishFlow.setError;
|
||||
const isBigFishBusy = bigFishFlow.isBusy;
|
||||
const setIsBigFishBusy = bigFishFlow.setIsBusy;
|
||||
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
|
||||
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
|
||||
|
||||
@@ -1892,10 +1899,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionStage === 'platform') {
|
||||
void refreshBigFishGallery();
|
||||
if (isBigFishCreationVisible) {
|
||||
void refreshBigFishGallery();
|
||||
}
|
||||
void refreshPuzzleGallery();
|
||||
}
|
||||
}, [refreshBigFishGallery, refreshPuzzleGallery, selectionStage]);
|
||||
}, [
|
||||
isBigFishCreationVisible,
|
||||
refreshBigFishGallery,
|
||||
refreshPuzzleGallery,
|
||||
selectionStage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -1914,6 +1928,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isBigFishCreationVisible &&
|
||||
(platformBootstrap.platformTab === 'create' ||
|
||||
selectionStage === 'platform') &&
|
||||
platformBootstrap.canReadProtectedData
|
||||
@@ -1921,6 +1936,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshBigFishShelf();
|
||||
}
|
||||
}, [
|
||||
isBigFishCreationVisible,
|
||||
platformBootstrap.canReadProtectedData,
|
||||
platformBootstrap.platformTab,
|
||||
refreshBigFishShelf,
|
||||
@@ -1955,7 +1971,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
|
||||
);
|
||||
});
|
||||
void refreshBigFishShelf();
|
||||
if (isBigFishCreationVisible) {
|
||||
void refreshBigFishShelf();
|
||||
}
|
||||
void refreshPuzzleShelf();
|
||||
}}
|
||||
createError={
|
||||
@@ -1991,20 +2009,32 @@ export function PlatformEntryFlowShellImpl({
|
||||
handleExperienceRpgWork(item);
|
||||
}}
|
||||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||||
bigFishItems={bigFishWorks}
|
||||
onOpenBigFishDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
void openBigFishDraft(item);
|
||||
});
|
||||
}}
|
||||
onExperienceBigFish={(item) => {
|
||||
runProtectedAction(() => {
|
||||
void startBigFishRunFromWork(item);
|
||||
});
|
||||
}}
|
||||
onDeleteBigFish={(item) => {
|
||||
handleDeleteBigFishWork(item);
|
||||
}}
|
||||
bigFishItems={isBigFishCreationVisible ? bigFishWorks : []}
|
||||
onOpenBigFishDetail={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
runProtectedAction(() => {
|
||||
void openBigFishDraft(item);
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onExperienceBigFish={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
runProtectedAction(() => {
|
||||
void startBigFishRunFromWork(item);
|
||||
});
|
||||
}
|
||||
: null
|
||||
}
|
||||
onDeleteBigFish={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
handleDeleteBigFishWork(item);
|
||||
}
|
||||
: null
|
||||
}
|
||||
puzzleItems={puzzleWorks}
|
||||
onOpenPuzzleDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
|
||||
@@ -14,6 +14,21 @@ export type PlatformCreationTypeCard = {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回当前平台入口允许展示的创作类型。
|
||||
* 平台层的入口、首屏卡带与初始化请求都应基于这份结果统一判断。
|
||||
*/
|
||||
export function getVisiblePlatformCreationTypes() {
|
||||
return PLATFORM_CREATION_TYPES.filter((item) => !item.hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个创作类型当前是否仍暴露在平台入口中。
|
||||
*/
|
||||
export function isPlatformCreationTypeVisible(id: PlatformCreationTypeId) {
|
||||
return PLATFORM_CREATION_TYPES.some((item) => item.id === id && !item.hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
|
||||
* `hidden` 只控制平台入口是否展示,不影响既有玩法链路和路由能力。
|
||||
|
||||
@@ -5007,83 +5007,19 @@ function WorldAttributeSchemaEditor({
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionPanel title="角色维度" subtitle={value.schemaName || '世界能力维度'}>
|
||||
<div className="space-y-3">
|
||||
<SectionPanel title="角色维度">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{value.slots.map((slot) => (
|
||||
<div
|
||||
key={slot.slotId}
|
||||
className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-[10rem_minmax(0,1fr)]">
|
||||
<Field label="维度名称">
|
||||
<TextInput
|
||||
value={slot.name}
|
||||
onChange={(name) => updateSlot(slot.slotId, { name })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="定义">
|
||||
<TextArea
|
||||
value={slot.definition}
|
||||
onChange={(definition) =>
|
||||
updateSlot(slot.slotId, { definition })
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<Field label="正向信号">
|
||||
<TextArea
|
||||
value={commaText(slot.positiveSignals)}
|
||||
onChange={(text) =>
|
||||
updateSlot(slot.slotId, {
|
||||
positiveSignals: parseCommaText(text),
|
||||
})
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="负向信号">
|
||||
<TextArea
|
||||
value={commaText(slot.negativeSignals)}
|
||||
onChange={(text) =>
|
||||
updateSlot(slot.slotId, {
|
||||
negativeSignals: parseCommaText(text),
|
||||
})
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-3">
|
||||
<Field label="战斗体现">
|
||||
<TextArea
|
||||
value={slot.combatUseText}
|
||||
onChange={(combatUseText) =>
|
||||
updateSlot(slot.slotId, { combatUseText })
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="社交体现">
|
||||
<TextArea
|
||||
value={slot.socialUseText}
|
||||
onChange={(socialUseText) =>
|
||||
updateSlot(slot.slotId, { socialUseText })
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="探索体现">
|
||||
<TextArea
|
||||
value={slot.explorationUseText}
|
||||
onChange={(explorationUseText) =>
|
||||
updateSlot(slot.slotId, { explorationUseText })
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="维度名称">
|
||||
<TextInput
|
||||
value={slot.name}
|
||||
onChange={(name) => updateSlot(slot.slotId, { name })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -133,62 +133,26 @@ test('custom world character selection stays stable when character ids are empty
|
||||
{
|
||||
slotId: 'axis_a',
|
||||
name: '潮骨',
|
||||
definition: '扛住潮压与正面冲击的底子。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '顶住正面浪涌。',
|
||||
socialUseText: '给人能扛事的可靠感。',
|
||||
explorationUseText: '在风浪里稳住自己。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_b',
|
||||
name: '浪步',
|
||||
definition: '顺潮借势、换位穿行的能力。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '借势切线。',
|
||||
socialUseText: '谈吐灵活。',
|
||||
explorationUseText: '穿越复杂地形。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_c',
|
||||
name: '舟识',
|
||||
definition: '辨流向、识潮眼的能力。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '抓住变化时机。',
|
||||
socialUseText: '看懂局势留白。',
|
||||
explorationUseText: '辨认水路与遗痕。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_d',
|
||||
name: '潮魄',
|
||||
definition: '在剧烈变化中仍敢推进的胆气。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '顶着压力推进。',
|
||||
socialUseText: '在冲突里压住场子。',
|
||||
explorationUseText: '面对异变继续前探。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_e',
|
||||
name: '契汐',
|
||||
definition: '与人和约定形成牵引的能力。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '借协同形成连锁。',
|
||||
socialUseText: '结盟、安抚与交换。',
|
||||
explorationUseText: '从旧约中打开局面。',
|
||||
},
|
||||
{
|
||||
slotId: 'axis_f',
|
||||
name: '回澜',
|
||||
definition: '在漫长消耗中回稳状态的能力。',
|
||||
positiveSignals: [],
|
||||
negativeSignals: [],
|
||||
combatUseText: '久战不乱。',
|
||||
socialUseText: '遇事沉静。',
|
||||
explorationUseText: '在恶劣天气里保有余力。',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ApiClientError } from '../../services/apiClient';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
createBigFishCreationSession,
|
||||
executeBigFishCreationAction,
|
||||
getBigFishCreationSession,
|
||||
} from '../../services/big-fish-creation';
|
||||
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||
@@ -1175,6 +1174,21 @@ test('create hub exposes direct template entry, keeps AIRP and visual novel lock
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('platform create hub does not prefetch hidden big fish platform data', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /角色扮演 RPG/u }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||||
expect(listBigFishWorks).not.toHaveBeenCalled();
|
||||
expect(listBigFishGallery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -1540,14 +1554,13 @@ test('creation hub clears all private work shelves immediately after logout stat
|
||||
const createPanel = getPlatformTabPanel('create');
|
||||
|
||||
expect(await within(createPanel).findByText('RPG 退出缓存作品')).toBeTruthy();
|
||||
expect(await within(createPanel).findByText('大鱼退出缓存作品')).toBeTruthy();
|
||||
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
|
||||
expect(await within(createPanel).findByText('拼图退出缓存作品')).toBeTruthy();
|
||||
|
||||
rerender(<TestWrapper authValue={loggedOutAuth} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
|
||||
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
|
||||
expect(within(createPanel).queryByText('拼图退出缓存作品')).toBeNull();
|
||||
});
|
||||
expect(within(createPanel).getByText('还没有作品')).toBeTruthy();
|
||||
@@ -1597,7 +1610,7 @@ test('published puzzle works appear on home and category public shelves', async
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('published big fish works appear on home and category public shelves', async () => {
|
||||
test('published big fish works stay hidden from platform home and category shelves', async () => {
|
||||
const user = userEvent.setup();
|
||||
const publishedBigFishWork: BigFishWorkSummary = {
|
||||
workId: 'big-fish-work-public-1',
|
||||
@@ -1623,20 +1636,17 @@ test('published big fish works appear on home and category public shelves', asyn
|
||||
render(<TestWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('机械深海 大鱼吃小鱼').length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(listBigFishGallery).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '分类' }));
|
||||
|
||||
const categoryPanel = getPlatformTabPanel('category');
|
||||
expect(within(categoryPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
|
||||
expect(
|
||||
within(categoryPanel).getAllByText('机械深海 大鱼吃小鱼').length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
within(categoryPanel).getAllByRole('button', { name: /大鱼/u }).length,
|
||||
).toBeGreaterThan(0);
|
||||
within(categoryPanel).queryAllByRole('button', { name: /大鱼/u }).length,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('published puzzle detail returns to the source platform tab', async () => {
|
||||
@@ -1853,31 +1863,15 @@ test('new creation entry maps raw bearer token errors to user-facing auth copy',
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
test('big fish creation timeout exits busy state and shows a readable error', async () => {
|
||||
test('hidden big fish creation entry does not render in platform create hub', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createBigFishCreationSession).mockRejectedValueOnce(
|
||||
Object.assign(new Error('请求超时:15000ms'), {
|
||||
name: 'TimeoutError',
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
|
||||
const button = screen.getByRole('button', { name: /大鱼吃小鱼/u });
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getAllByText(
|
||||
'开启大鱼吃小鱼创作工作台超时,请确认运行时后端已启动后重试。',
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect((button as HTMLButtonElement).disabled).toBe(false);
|
||||
expect(screen.queryByText(/正在加载大鱼吃小鱼共创工作区/u)).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||||
expect(createBigFishCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('puzzle creation timeout exits busy state and shows a readable error', async () => {
|
||||
@@ -2086,163 +2080,6 @@ test('public code search opens a published big fish work by BF code', async () =
|
||||
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('big fish draft card restores the bound agent session and opens the result view', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listBigFishWorks).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
workId: 'big-fish-work-big-fish-session-1',
|
||||
sourceSessionId: 'big-fish-session-1',
|
||||
ownerUserId: 'user-1',
|
||||
title: '机械深海 大鱼吃小鱼',
|
||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||
summary: '机械微生物吞并进化',
|
||||
coverImageSrc: null,
|
||||
status: 'draft',
|
||||
updatedAt: '2026-04-22T12:10:00.000Z',
|
||||
publishReady: false,
|
||||
levelCount: 8,
|
||||
levelMainImageReadyCount: 0,
|
||||
levelMotionReadyCount: 0,
|
||||
backgroundReady: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
|
||||
const title = await screen.findByText('机械深海 大鱼吃小鱼');
|
||||
const card = title.closest('.platform-surface');
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
throw new Error('Missing big fish draft card');
|
||||
}
|
||||
|
||||
await user.click(card);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getBigFishCreationSession).toHaveBeenCalledWith(
|
||||
'big-fish-session-1',
|
||||
);
|
||||
});
|
||||
|
||||
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
|
||||
expect(screen.getByText('机械深海 大鱼吃小鱼')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('大鱼吃小鱼工作区:big-fish-session-1'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText(/大鱼吃小鱼共创/u)).toBeNull();
|
||||
expect(screen.getByText('我想做机械深海里微生物互相吞并进化。')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('big fish result publish action refreshes creation works', async () => {
|
||||
const user = userEvent.setup();
|
||||
const baseBigFishSession = (
|
||||
await getBigFishCreationSession('big-fish-session-1')
|
||||
).session;
|
||||
vi.mocked(getBigFishCreationSession).mockClear();
|
||||
vi.mocked(listBigFishWorks).mockClear();
|
||||
vi.mocked(listBigFishGallery).mockClear();
|
||||
const publishedBigFishSession = {
|
||||
...baseBigFishSession,
|
||||
stage: 'published',
|
||||
publishReady: true,
|
||||
assetCoverage: {
|
||||
levelMainImageReadyCount: 8,
|
||||
levelMotionReadyCount: 16,
|
||||
backgroundReady: true,
|
||||
requiredLevelCount: 8,
|
||||
publishReady: true,
|
||||
blockers: [],
|
||||
},
|
||||
updatedAt: '2026-04-22T12:20:00.000Z',
|
||||
};
|
||||
vi.mocked(executeBigFishCreationAction).mockResolvedValue({
|
||||
session: publishedBigFishSession,
|
||||
});
|
||||
vi.mocked(listBigFishWorks)
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
workId: 'big-fish-work-big-fish-session-1',
|
||||
sourceSessionId: 'big-fish-session-1',
|
||||
ownerUserId: 'user-1',
|
||||
title: '机械深海 大鱼吃小鱼',
|
||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||
summary: '机械微生物吞并进化',
|
||||
coverImageSrc: null,
|
||||
status: 'draft',
|
||||
updatedAt: '2026-04-22T12:10:00.000Z',
|
||||
publishReady: true,
|
||||
levelCount: 8,
|
||||
levelMainImageReadyCount: 8,
|
||||
levelMotionReadyCount: 16,
|
||||
backgroundReady: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
workId: 'big-fish-work-big-fish-session-1',
|
||||
sourceSessionId: 'big-fish-session-1',
|
||||
ownerUserId: 'user-1',
|
||||
title: '机械深海 大鱼吃小鱼',
|
||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||
summary: '机械微生物吞并进化',
|
||||
coverImageSrc: null,
|
||||
status: 'published',
|
||||
updatedAt: '2026-04-22T12:20:00.000Z',
|
||||
publishReady: true,
|
||||
levelCount: 8,
|
||||
levelMainImageReadyCount: 8,
|
||||
levelMotionReadyCount: 16,
|
||||
backgroundReady: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
const title = await screen.findByText('机械深海 大鱼吃小鱼');
|
||||
const card = title.closest('.platform-surface');
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
throw new Error('Missing big fish draft card');
|
||||
}
|
||||
|
||||
await user.click(card);
|
||||
await waitFor(() => {
|
||||
expect(getBigFishCreationSession).toHaveBeenCalledWith(
|
||||
'big-fish-session-1',
|
||||
);
|
||||
});
|
||||
vi.mocked(listBigFishWorks).mockClear();
|
||||
|
||||
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: '发布' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(executeBigFishCreationAction).toHaveBeenCalledWith(
|
||||
'big-fish-session-1',
|
||||
{
|
||||
action: 'big_fish_publish_game',
|
||||
},
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(listBigFishWorks).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(listBigFishGallery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user