This commit is contained in:
2026-04-28 20:25:37 +08:00
parent f0471a4f8d
commit 0f013b6eee
45 changed files with 1117 additions and 1047 deletions

View File

@@ -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>

View File

@@ -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>
),
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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 (

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -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` 只控制平台入口是否展示,不影响既有玩法链路和路由能力。

View File

@@ -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>

View File

@@ -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: '在恶劣天气里保有余力。',
},
],
},

View File

@@ -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();

View File

@@ -156,7 +156,6 @@ export function getLeadingAttributeSlot(
export function buildSchemaSummary(schema: WorldAttributeSchema, limit = 6) {
return schema.slots.slice(0, limit).map(slot => ({
name: slot.name,
definition: slot.definition,
}));
}

View File

@@ -50,18 +50,6 @@ function toText(value: unknown, fallback: string) {
return normalized || fallback;
}
function toStringArray(value: unknown, fallback: string[]) {
if (!Array.isArray(value)) {
return [...fallback];
}
const normalized = value
.map(item => toOptionalText(item))
.filter(Boolean);
return normalized.length > 0 ? [...new Set(normalized)] : [...fallback];
}
export function coerceWorldAttributeSchema(
raw: unknown,
fallback: WorldAttributeSchema,
@@ -79,7 +67,6 @@ export function coerceWorldAttributeSchema(
schemaVersion: typeof raw.schemaVersion === 'number' && Number.isFinite(raw.schemaVersion) && raw.schemaVersion > 0
? Math.max(1, Math.round(raw.schemaVersion))
: fallback.schemaVersion,
schemaName: toOptionalText(raw.schemaName) || fallback.schemaName,
generatedFrom: {
...fallback.generatedFrom,
worldName: toText(rawGeneratedFrom.worldName, fallback.generatedFrom.worldName),
@@ -93,12 +80,6 @@ export function coerceWorldAttributeSchema(
...fallbackSlot,
slotId: fallbackSlot.slotId,
name: toText(rawSlot.name, fallbackSlot.name),
definition: toText(rawSlot.definition, fallbackSlot.definition),
positiveSignals: toStringArray(rawSlot.positiveSignals, fallbackSlot.positiveSignals),
negativeSignals: toStringArray(rawSlot.negativeSignals, fallbackSlot.negativeSignals),
combatUseText: toText(rawSlot.combatUseText, fallbackSlot.combatUseText),
socialUseText: toText(rawSlot.socialUseText, fallbackSlot.socialUseText),
explorationUseText: toText(rawSlot.explorationUseText, fallbackSlot.explorationUseText),
};
}),
};
@@ -151,17 +132,6 @@ export function validateWorldAttributeSchema(schema: WorldAttributeSchema) {
issues.push(`attribute name "${trimmedName}" contains banned legacy term`);
}
if (!slot.definition.trim()) {
issues.push(`slot ${slot.slotId} is missing a definition`);
}
if (/|||/u.test(slot.definition)) {
issues.push(`slot ${slot.slotId} definition is too derivative`);
}
if (!slot.combatUseText.trim() || !slot.socialUseText.trim() || !slot.explorationUseText.trim()) {
issues.push(`slot ${slot.slotId} must describe combat, social, and exploration usage`);
}
});
return issues;

View File

@@ -85,7 +85,6 @@ export type BuildDamageBreakdown = {
export type BuildContributionAttributeRow = {
slotId: string;
label: string;
definition: string;
similarity: number;
weight: number;
value: number;
@@ -104,7 +103,6 @@ export type OutgoingDamageResult = {
type BuildContributionTarget = {
slotId: string;
label: string;
definition: string;
};
type ResolvedTagAffinity = {
@@ -312,7 +310,6 @@ function resolveContributionTargets(
return schema.slots.map((slot) => ({
slotId: slot.slotId,
label: slot.name,
definition: slot.definition,
})) satisfies BuildContributionTarget[];
}
@@ -550,7 +547,6 @@ export function getBuildContributionAttributeRows(
return {
slotId: target.slotId,
label: target.label,
definition: target.definition,
similarity: roundNumber(
row.attributeSimilarities?.[target.slotId] ?? 0,
4,

View File

@@ -60,12 +60,6 @@ function buildSlotSemanticVector(slot: WorldAttributeSlot, index: number) {
const sourceText = [
slot.slotId,
slot.name,
slot.definition,
slot.combatUseText,
slot.socialUseText,
slot.explorationUseText,
...(slot.positiveSignals ?? []),
...(slot.negativeSignals ?? []),
].join(' ');
const semanticVector: AttributeVector = {};

View File

@@ -5,7 +5,8 @@ import type { FunctionDocumentationEntry } from '../types';
* camp_travel_home_scene
*
* 从营地与同伴对话结束后,正式前往角色主线场景的控制 function。
* 这里除了元信息,也直接收口了它的按钮构造判定 helper
* 中文注释:前端只保留按钮构造判定 helper 和视觉元信息;
* 正式场景迁移、遭遇预览与快照写入统一由后端 resolver 承接。
*/
export const CAMP_TRAVEL_HOME_OPTION_VISUALS: StoryOption['visuals'] = {
playerAnimation: AnimationState.RUN,
@@ -41,10 +42,10 @@ export const CAMP_TRAVEL_HOME_FUNCTION: FunctionDocumentationEntry = {
source: 'src/data/functionCatalog/flow/campTravelHomeScene.ts',
summary: '营地开场后的专用旅行控制项。',
detailedDescription:
'它负责把开局同伴营地流程平稳切到角色真正的起始场景,并清理当前营地 encounter、战斗态和镜头残留状态。',
'它负责把开局同伴营地流程平稳切到角色真正的起始场景;正式目标场景、encounter 清理、战斗态清理和镜头残留状态由后端 resolver 写入。',
trigger: '常见于开局同伴营地对话后的跟进选项。',
execution:
'点击后不会走普通 state function 结算,而是执行一次定制场景迁移和历史写入。',
'点击后作为服务端 runtime function id 提交到 /api/runtime/story/actions/resolve由后端执行定制场景迁移和历史写入。',
result: '玩家会离开营地进入角色主场景,正式开始该角色的冒险线。',
active: true,
runtime: {
@@ -52,11 +53,11 @@ export const CAMP_TRAVEL_HOME_FUNCTION: FunctionDocumentationEntry = {
uiMode: 'none',
visuals: CAMP_TRAVEL_HOME_OPTION_VISUALS,
executor:
'src/hooks/rpg-runtime-story/choiceActions.ts -> handleCampTravelHome',
'server-rs/crates/api-server/src/runtime_story/compat.rs -> resolve_camp_travel_home_scene_action',
animationNote:
'先播放营地离场的 run 演出,再切到正式场景并生成 encounter preview。',
'前端保留 run 视觉元信息;正式状态以服务端 hydrated snapshot 为准。',
storyNote:
'通过 commitGeneratedStateWithEncounterEntry 写入离营结果,并在新场景继续后续剧情。',
'后端写入离营结果、生成 encounter preview,并在新场景继续后续剧情。',
uiNote: '这是专用旅行流程,不会打开 modal。',
},
};

View File

@@ -1,8 +1,9 @@
import { existsSync } from 'node:fs';
import { SERVER_RUNTIME_FUNCTION_IDS } from '../../../packages/shared/src/contracts/rpgRuntimeStoryAction';
import { describe, expect, it } from 'vitest';
import { SERVER_RUNTIME_FUNCTION_IDS } from '../../../packages/shared/src/contracts/rpgRuntimeStoryAction';
import type { Encounter, GameState, InventoryItem } from '../../types';
import {
ALL_FUNCTION_DOCUMENTATION,
buildCampTravelHomeOption,
@@ -11,6 +12,7 @@ import {
buildNpcPreviewTalkOption,
buildNpcRecruitModalState,
buildNpcTradeModalState,
CAMP_TRAVEL_HOME_FUNCTION,
CONTINUE_ADVENTURE_FUNCTION,
getFunctionDocumentationById,
isNpcPreviewTalkOption,
@@ -18,7 +20,6 @@ import {
shouldNpcRecruitOpenModal,
} from './index';
import { RPG_FUNCTION_RUNTIME_OVERVIEW } from './runtimeIndex';
import type { Encounter, GameState, InventoryItem } from '../../types';
function createEncounter(overrides: Partial<Encounter> = {}): Encounter {
return {
@@ -103,6 +104,12 @@ describe('functionCatalog', () => {
expect(campTravelOption.functionId).toBe('camp_travel_home_scene');
expect(campTravelOption.actionText).toBe('前往 竹林古道');
expect(campTravelOption.detailText).toBe('离开营地,前往 竹林古道。');
expect(CAMP_TRAVEL_HOME_FUNCTION.runtime?.executor).toContain(
'server-rs/crates/api-server/src/runtime_story/compat.rs',
);
expect(CAMP_TRAVEL_HOME_FUNCTION.detailedDescription).toContain(
'后端 resolver',
);
});
it('builds npc preview talk options from the current encounter', () => {

View File

@@ -248,7 +248,7 @@ export function buildEncounterAttributeRumors(
return getSortedAttributeEntries(profile, schemaContext.schema)
.slice(0, options.limit ?? 2)
.map(entry => `${entry.slot.name}${entry.slot.definition}`);
.map(entry => entry.slot.name);
}
export function buildGiftAffinityInsight(

View File

@@ -10,7 +10,6 @@ export const WORLD_TEMPLATE_ATTRIBUTE_SCHEMAS: Record<
id: 'schema:wuxia:v1',
worldId: WorldType.WUXIA,
schemaVersion: 1,
schemaName: '江湖六脉',
generatedFrom: {
worldType: WorldType.WUXIA,
worldName: '武侠',
@@ -22,62 +21,26 @@ export const WORLD_TEMPLATE_ATTRIBUTE_SCHEMAS: Record<
{
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: '长线跋涉、在恶劣环境下维持专注与状态。',
},
],
},
@@ -85,7 +48,6 @@ export const WORLD_TEMPLATE_ATTRIBUTE_SCHEMAS: Record<
id: 'schema:xianxia:v1',
worldId: WorldType.XIANXIA,
schemaVersion: 1,
schemaName: '灵界六轴',
generatedFrom: {
worldType: WorldType.XIANXIA,
worldName: '仙侠',
@@ -97,62 +59,26 @@ export const WORLD_TEMPLATE_ATTRIBUTE_SCHEMAS: Record<
{
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: '在漫长探索与灵潮侵蚀中维持可行动状态。',
},
],
},

View File

@@ -21,7 +21,6 @@ import { createStoryChoiceActions } from './choiceActions';
vi.mock('./storyChoiceRuntime', async () => {
return {
runCampTravelHomeChoice: vi.fn(),
runServerRuntimeChoiceAction: runServerRuntimeChoiceActionMock,
shouldOpenLocalRuntimeNpcModal: (option: StoryOption) =>
(
@@ -811,4 +810,97 @@ describe('createStoryChoiceActions', () => {
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
expect(playResolvedChoice).not.toHaveBeenCalled();
});
it('routes camp_travel_home_scene to the backend resolver instead of the legacy local travel branch', async () => {
const state = {
...createBaseState(),
inBattle: false,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentEncounter: {
kind: 'npc' as const,
id: 'npc-camp',
npcName: '营地同伴',
npcDescription: '准备一起出发的同伴',
npcAvatar: '伴',
context: '营地',
hostile: false,
},
npcInteractionActive: false,
sceneHostileNpcs: [],
currentScenePreset: {
id: 'wuxia-border-camp',
name: '边关营地',
description: '营火未熄。',
imageSrc: '',
connectedSceneIds: ['wuxia-palace-court'],
connections: [],
forwardSceneId: 'wuxia-palace-court',
treasureHints: [],
npcs: [],
},
} satisfies GameState;
const option: StoryOption = {
...createBattleOption('camp_travel_home_scene'),
actionText: '前往宫苑内庭',
text: '前往宫苑内庭',
};
const buildResolvedChoiceState = vi.fn();
const playResolvedChoice = vi.fn();
const commitGeneratedStateWithEncounterEntry = vi.fn();
isRpgRuntimeServerFunctionIdMock.mockReturnValue(true);
const { handleChoice } = createStoryChoiceActions({
gameState: state,
currentStory: createFallbackStory('营地对话已经收束。'),
isLoading: false,
setGameState: vi.fn(),
setCurrentStory: vi.fn(),
setAiError: vi.fn(),
setIsLoading: vi.fn(),
setBattleReward: vi.fn(),
buildResolvedChoiceState,
playResolvedChoice,
buildStoryContextFromState: vi.fn(),
buildStoryFromResponse: vi.fn((_, __, response) => response),
buildFallbackStoryForState: vi.fn(() => createFallbackStory()),
generateStoryForState: vi.fn(),
getAvailableOptionsForState: vi.fn(() => [option]),
getStoryGenerationHostileNpcs: vi.fn(() => []),
getResolvedSceneHostileNpcs: vi.fn(() => []),
buildNpcStory: vi.fn(() => createFallbackStory()),
handleNpcBattleConversationContinuation: vi.fn(() => false),
updateQuestLog: vi.fn((inputState: GameState) => inputState),
incrementRuntimeStats: vi.fn((inputState: GameState) => inputState),
getCampCompanionTravelScene: vi.fn(() => {
throw new Error('legacy camp travel resolver should not run');
}),
enterNpcInteraction: vi.fn(() => false),
handleNpcInteraction: vi.fn(() => false),
handleTreasureInteraction: vi.fn(() => false),
commitGeneratedStateWithEncounterEntry,
finalizeNpcBattleResult: vi.fn(() => null),
isContinueAdventureOption: vi.fn(() => false),
isCampTravelHomeOption: vi.fn(() => true),
isRegularNpcEncounter: neverNpcEncounter,
isNpcEncounter: neverNpcEncounter,
npcPreviewTalkFunctionId: 'npc_preview_talk',
fallbackCompanionName: '同伴',
turnVisualMs: 820,
});
await handleChoice(option);
expect(runServerRuntimeChoiceActionMock).toHaveBeenCalledWith(
expect.objectContaining({
gameState: state,
option,
character: state.playerCharacter,
}),
);
expect(commitGeneratedStateWithEncounterEntry).not.toHaveBeenCalled();
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
expect(playResolvedChoice).not.toHaveBeenCalled();
});
});

View File

@@ -20,7 +20,6 @@ import type {
} from './progressionActions';
import { runLocalStoryChoiceContinuation } from './storyChoiceContinuation';
import {
runCampTravelHomeChoice,
runServerRuntimeChoiceAction,
shouldOpenLocalRuntimeNpcModal,
} from './storyChoiceRuntime';
@@ -99,18 +98,13 @@ export function createStoryChoiceActions({
handleNpcBattleConversationContinuation,
updateQuestLog,
incrementRuntimeStats,
getCampCompanionTravelScene,
enterNpcInteraction,
handleNpcInteraction,
handleTreasureInteraction,
commitGeneratedStateWithEncounterEntry,
finalizeNpcBattleResult,
isContinueAdventureOption,
isCampTravelHomeOption,
isRegularNpcEncounter,
isNpcEncounter,
npcPreviewTalkFunctionId,
fallbackCompanionName,
turnVisualMs,
}: {
gameState: GameState;
@@ -140,13 +134,13 @@ export function createStoryChoiceActions({
handleNpcBattleConversationContinuation: HandleNpcBattleConversationContinuation;
updateQuestLog: UpdateQuestLog;
incrementRuntimeStats: IncrementRuntimeStats;
getCampCompanionTravelScene: (state: GameState, character: Character) => GameState['currentScenePreset'] | null;
getCampCompanionTravelScene?: (state: GameState, character: Character) => GameState['currentScenePreset'] | null;
enterNpcInteraction: (encounter: Encounter, actionText: string) => boolean;
handleNpcInteraction: (option: StoryOption) => boolean | Promise<boolean>;
handleTreasureInteraction: (
option: StoryOption,
) => void | Promise<void> | boolean | Promise<boolean>;
commitGeneratedStateWithEncounterEntry: CommitGeneratedStateWithEncounterEntry;
commitGeneratedStateWithEncounterEntry?: CommitGeneratedStateWithEncounterEntry;
finalizeNpcBattleResult: (
state: GameState,
character: Character,
@@ -154,11 +148,11 @@ export function createStoryChoiceActions({
battleOutcome: GameState['currentNpcBattleOutcome'],
) => { nextState: GameState; resultText: string } | null;
isContinueAdventureOption: (option: StoryOption) => boolean;
isCampTravelHomeOption: (option: StoryOption) => boolean;
isCampTravelHomeOption?: (option: StoryOption) => boolean;
isRegularNpcEncounter: (encounter: GameState['currentEncounter']) => encounter is Encounter;
isNpcEncounter: (encounter: GameState['currentEncounter']) => encounter is Encounter;
isNpcEncounter?: (encounter: GameState['currentEncounter']) => encounter is Encounter;
npcPreviewTalkFunctionId: string;
fallbackCompanionName: string;
fallbackCompanionName?: string;
turnVisualMs: number;
}) {
const handleChoice = async (option: StoryOption) => {
@@ -191,25 +185,6 @@ export function createStoryChoiceActions({
return;
}
if (isCampTravelHomeOption(option)) {
await runCampTravelHomeChoice({
gameState,
option,
character,
setBattleReward,
setAiError,
setIsLoading,
setGameState,
incrementRuntimeStats,
getCampCompanionTravelScene,
commitGeneratedStateWithEncounterEntry,
isNpcEncounter,
fallbackCompanionName,
turnVisualMs,
});
return;
}
if (shouldOpenLocalRuntimeNpcModal(option)) {
setAiError(null);
await handleNpcInteraction(option);

View File

@@ -1,16 +1,6 @@
import {
buildEncounterEntryState,
hasEncounterEntity,
} from '../../data/encounterTransition';
import {
CALL_OUT_ENTRY_X_METERS,
createSceneEncounterPreview,
resolveSceneEncounterPreview,
} from '../../data/sceneEncounterPreviews';
import {
AnimationState,
Character,
Encounter,
GameState,
StoryMoment,
StoryOption,
@@ -18,24 +8,12 @@ import {
import { resolveRpgRuntimeChoice } from '.';
import type { BattleRewardSummary } from './uiTypes';
type RuntimeStatsIncrements = Partial<
Pick<
GameState['runtimeStats'],
'hostileNpcsDefeated' | 'questsAccepted' | 'itemsUsed' | 'scenesTraveled'
>
>;
type BuildFallbackStoryForState = (
state: GameState,
character: Character,
fallbackText?: string,
) => StoryMoment;
type IncrementRuntimeStats = (
state: GameState,
increments: RuntimeStatsIncrements,
) => GameState;
function sleep(ms: number) {
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
}
@@ -54,126 +32,6 @@ export function shouldOpenLocalRuntimeNpcModal(option: StoryOption) {
);
}
export async function runCampTravelHomeChoice(params: {
gameState: GameState;
option: StoryOption;
character: Character;
setBattleReward: (reward: BattleRewardSummary | null) => void;
setAiError: (message: string | null) => void;
setIsLoading: (loading: boolean) => void;
setGameState: (state: GameState) => void;
incrementRuntimeStats: IncrementRuntimeStats;
getCampCompanionTravelScene: (
state: GameState,
character: Character,
) => GameState['currentScenePreset'] | null;
commitGeneratedStateWithEncounterEntry: (
entryState: GameState,
resolvedState: GameState,
character: Character,
actionText: string,
resultText: string,
lastFunctionId?: string,
) => Promise<void> | void;
isNpcEncounter: (
encounter: GameState['currentEncounter'],
) => encounter is Encounter;
fallbackCompanionName: string;
turnVisualMs: number;
}) {
const targetScene = params.getCampCompanionTravelScene(
params.gameState,
params.character,
);
if (!targetScene) {
return false;
}
params.setBattleReward(null);
params.setAiError(null);
const companionName = params.isNpcEncounter(params.gameState.currentEncounter)
? params.gameState.currentEncounter.npcName
: params.fallbackCompanionName;
const travelRunState: GameState = {
...params.gameState,
ambientIdleMode: undefined,
currentEncounter: null,
npcInteractionActive: false,
sceneHostileNpcs: [],
playerX: 0,
playerFacing: 'right' as const,
animationState: AnimationState.RUN,
playerActionMode: 'idle' as const,
activeCombatEffects: [],
scrollWorld: true,
inBattle: false,
lastObserveSignsSceneId: null,
lastObserveSignsReport: null,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
};
const travelBaseState: GameState = params.incrementRuntimeStats(
{
...params.gameState,
ambientIdleMode: undefined,
currentScenePreset: targetScene,
currentEncounter: null,
npcInteractionActive: false,
sceneHostileNpcs: [],
playerX: 0,
playerFacing: 'right' as const,
animationState: AnimationState.IDLE,
playerActionMode: 'idle' as const,
activeCombatEffects: [],
scrollWorld: false,
inBattle: false,
lastObserveSignsSceneId: null,
lastObserveSignsReport: null,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
},
{
scenesTraveled: 1,
},
);
const travelPreviewState: GameState = {
...travelBaseState,
...createSceneEncounterPreview(travelBaseState),
};
const resolvedState = hasEncounterEntity(travelPreviewState)
? resolveSceneEncounterPreview(travelPreviewState)
: travelBaseState;
const entryState = buildEncounterEntryState(
resolvedState,
CALL_OUT_ENTRY_X_METERS,
);
params.setIsLoading(true);
params.setGameState(travelRunState);
await sleep(params.turnVisualMs);
await params.commitGeneratedStateWithEncounterEntry(
entryState,
resolvedState,
params.character,
params.option.actionText,
`You and ${companionName} leave camp and formally step into ${targetScene.name} to begin the adventure.`,
params.option.functionId,
);
return true;
}
export async function runServerRuntimeChoiceAction(params: {
gameState: GameState;
currentStory: StoryMoment | null;

View File

@@ -91,7 +91,6 @@ function buildSavedProfile(options: {
id: 'schema:test',
worldId: 'CUSTOM',
schemaVersion: 1,
schemaName: '测试',
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '回潮群岛',

View File

@@ -10,14 +10,12 @@ import {detectCustomWorldThemeMode} from './customWorldTheme';
function buildSchema(
input: AttributeSchemaGenerationInput,
schemaName: string,
slots: WorldAttributeSlot[],
): WorldAttributeSchema {
return {
id: `schema:${input.worldType.toLowerCase()}:${schemaName}`,
id: `schema:${input.worldType.toLowerCase()}:${input.worldName}`,
worldId: input.worldType === WorldType.CUSTOM ? `custom:${input.worldName}` : input.worldType,
schemaVersion: 1,
schemaName,
generatedFrom: {
worldType: input.worldType,
worldName: input.worldName,
@@ -40,62 +38,57 @@ function buildCustomThemeSlots(input: AttributeSchemaGenerationInput) {
if (themeMode === 'mythic') {
return {
schemaName: '叙境六维',
slots: [
{ 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: '在长线推进中持续保持判断和行动力。' },
{ slotId: 'axis_a', name: '体魄' },
{ slotId: 'axis_b', name: '身法' },
{ slotId: 'axis_c', name: '识见' },
{ slotId: 'axis_d', name: '胆魄' },
{ slotId: 'axis_e', name: '牵引' },
{ slotId: 'axis_f', name: '定力' },
] satisfies WorldAttributeSlot[],
};
}
if (themeMode === 'machina') {
return {
schemaName: '机潮六轴',
slots: [
{ 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: '在长时间高负荷环境里持续工作。' },
{ slotId: 'axis_a', name: '机锋' },
{ slotId: 'axis_b', name: '步准' },
{ slotId: 'axis_c', name: '算识' },
{ slotId: 'axis_d', name: '潮压' },
{ slotId: 'axis_e', name: '协频' },
{ slotId: 'axis_f', name: '续载' },
] satisfies WorldAttributeSlot[],
};
}
if (themeMode === 'tide') {
return {
schemaName: '潮境六脉',
slots: [
{ 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: '在漫长远行与恶劣天气里保有余力。' },
{ slotId: 'axis_a', name: '潮骨' },
{ slotId: 'axis_b', name: '浪步' },
{ slotId: 'axis_c', name: '舟识' },
{ slotId: 'axis_d', name: '潮魄' },
{ slotId: 'axis_e', name: '契汐' },
{ slotId: 'axis_f', name: '回澜' },
] satisfies WorldAttributeSlot[],
};
}
if (themeMode === 'rift') {
return {
schemaName: '裂界六轴',
slots: [
{ 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: '在裂界侵蚀与长线压力里保持在线。' },
{ slotId: 'axis_a', name: '界躯' },
{ slotId: 'axis_b', name: '裂步' },
{ slotId: 'axis_c', name: '界识' },
{ slotId: 'axis_d', name: '界压' },
{ slotId: 'axis_e', name: '缚契' },
{ slotId: 'axis_f', name: '回脉' },
] satisfies WorldAttributeSlot[],
};
}
return {
schemaName: '叙境六维',
slots: getTemplateWorldAttributeSchema(WorldType.WUXIA).slots,
};
}
@@ -110,7 +103,7 @@ export function generateWorldAttributeSchema(input: AttributeSchemaGenerationInp
}
const generated = buildCustomThemeSlots(input);
const schema = buildSchema(input, generated.schemaName, generated.slots);
const schema = buildSchema(input, generated.slots);
const issues = validateWorldAttributeSchema(schema);
if (issues.length > 0) {

View File

@@ -1478,7 +1478,7 @@ export function buildCustomWorldReferenceText(
`开局归处:${profile.camp?.name ?? '未设定'}${profile.camp?.description ? `${profile.camp.description}` : ''}`,
`题材适配层:${themePack.displayName};制度词汇 ${themePack.institutionLexicon.slice(0, 4).join('、')};禁忌词 ${themePack.tabooLexicon.slice(0, 4).join('、')};载体类型 ${themePack.artifactClasses.slice(0, 4).join('、')}`,
`当前激活线程:\n${activeThreads.map((thread) => `- ${thread.title}${thread.summary}`).join('\n') || '- 暂无'}`,
`世界属性轴:${profile.attributeSchema.slots.map((slot) => `${slot.name}${slot.definition}`).join('')}`,
`世界属性轴:${profile.attributeSchema.slots.map((slot) => slot.name).join('')}`,
`可扮演角色档案:\n${playableNpcText || '- 暂无'}`,
`世界场景角色档案:\n${storyNpcText || '- 暂无'}`,
`关键场景档案:\n${landmarkText || '- 暂无'}`,

View File

@@ -20,7 +20,6 @@ function createBaseProfile(): CustomWorldProfile {
id: 'schema:test',
worldId: 'CUSTOM',
schemaVersion: 1,
schemaName: '测试属性',
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '潮雾群岛',

View File

@@ -47,7 +47,6 @@ const sessionWithPreview: CustomWorldAgentSessionSnapshot = {
id: 'schema:draft:test',
worldId: 'custom:草稿',
schemaVersion: 1,
schemaName: '草稿六维',
generatedFrom: {
worldType: 'CUSTOM',
worldName: '只作为 fallback 的本地草稿名',
@@ -59,62 +58,26 @@ const sessionWithPreview: CustomWorldAgentSessionSnapshot = {
{
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: '长线保持行动力。',
},
],
},

View File

@@ -17,12 +17,6 @@ export type AttributeVector = Record<string, number>;
export interface WorldAttributeSlot {
slotId: WorldAttributeSlotId;
name: string;
definition: string;
positiveSignals: string[];
negativeSignals: string[];
combatUseText: string;
socialUseText: string;
explorationUseText: string;
}
export interface WorldAttributeSchema {
@@ -36,7 +30,6 @@ export interface WorldAttributeSchema {
tone: string;
conflictCore: string;
};
schemaName?: string;
slots: WorldAttributeSlot[];
}
@@ -155,6 +148,5 @@ export interface AttributeSchemaGenerationInput {
}
export interface AttributeSchemaGenerationOutput {
schemaName: string;
slots: WorldAttributeSlot[];
}