Simplify custom world result editing controls
This commit is contained in:
@@ -10,10 +10,10 @@ import type {
|
||||
} from '../../hooks/useStoryGeneration';
|
||||
import type {
|
||||
CompanionRenderState,
|
||||
CustomWorldProfile,
|
||||
GameState,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import { UI_CHROME } from '../../uiAssets';
|
||||
import type { GameCanvasEntitySelection } from '../GameCanvas';
|
||||
@@ -51,7 +51,7 @@ export function GameShellMainContent({
|
||||
hasSavedGame,
|
||||
handleContinueGame,
|
||||
handleStartNewGame,
|
||||
handleWorldSelect,
|
||||
handleCustomWorldSelect,
|
||||
handleBackToWorldSelect,
|
||||
handleCharacterSelect,
|
||||
displayedOptions,
|
||||
@@ -88,7 +88,7 @@ export function GameShellMainContent({
|
||||
hasSavedGame: boolean;
|
||||
handleContinueGame: () => void;
|
||||
handleStartNewGame: () => void;
|
||||
handleWorldSelect: (type: WorldType, customWorldProfile?: GameState['customWorldProfile']) => void;
|
||||
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
|
||||
handleBackToWorldSelect: () => void;
|
||||
handleCharacterSelect: (character: NonNullable<GameState['playerCharacter']>) => void;
|
||||
displayedOptions: StoryOption[];
|
||||
@@ -132,7 +132,7 @@ export function GameShellMainContent({
|
||||
hasSavedGame={hasSavedGame}
|
||||
handleContinueGame={handleContinueGame}
|
||||
handleStartNewGame={handleStartNewGame}
|
||||
handleWorldSelect={handleWorldSelect}
|
||||
handleCustomWorldSelect={handleCustomWorldSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
|
||||
handleContinueGame,
|
||||
handleStartNewGame,
|
||||
handleSaveAndExit,
|
||||
handleWorldSelect,
|
||||
handleCustomWorldSelect,
|
||||
handleBackToWorldSelect,
|
||||
handleCharacterSelect,
|
||||
} = entry;
|
||||
@@ -228,7 +228,7 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
|
||||
hasSavedGame={hasSavedGame}
|
||||
handleContinueGame={handleContinueGame}
|
||||
handleStartNewGame={handleStartNewGame}
|
||||
handleWorldSelect={handleWorldSelect}
|
||||
handleCustomWorldSelect={handleCustomWorldSelect}
|
||||
handleBackToWorldSelect={handleBackToWorldSelect}
|
||||
handleCharacterSelect={handleCharacterSelect}
|
||||
displayedOptions={displayedOptions}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
upsertSavedCustomWorldProfile,
|
||||
} from '../../data/customWorldLibrary';
|
||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||
import { getScenePreset } from '../../data/scenePresets';
|
||||
import {
|
||||
type CustomWorldGenerationProgress,
|
||||
generateCustomWorldProfile,
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
type CustomWorldGenerationMode,
|
||||
type CustomWorldProfile,
|
||||
type GameState,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import {
|
||||
CHROME_ICONS,
|
||||
@@ -45,8 +43,6 @@ export type SelectionStage =
|
||||
| 'custom-world-generating'
|
||||
| 'custom-world-result';
|
||||
|
||||
type WorldOnlineCounts = Partial<Record<WorldType, number>>;
|
||||
|
||||
type PreGameSelectionFlowProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
@@ -54,10 +50,7 @@ type PreGameSelectionFlowProps = {
|
||||
hasSavedGame: boolean;
|
||||
handleContinueGame: () => void;
|
||||
handleStartNewGame: () => void;
|
||||
handleWorldSelect: (
|
||||
type: WorldType,
|
||||
customWorldProfile?: GameState['customWorldProfile'],
|
||||
) => void;
|
||||
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
|
||||
};
|
||||
|
||||
const DEVELOPER_TEAM_MESSAGE =
|
||||
@@ -68,32 +61,6 @@ const START_SCREEN_CONTACTS = [
|
||||
{ label: '微信', value: 'bzh253518756' },
|
||||
] as const;
|
||||
|
||||
const WORLD_OPTIONS = [
|
||||
{
|
||||
id: WorldType.WUXIA,
|
||||
name: '武侠',
|
||||
subtitle: '刀剑江湖',
|
||||
icon: WORLD_SELECT_ICONS.wuxia,
|
||||
texture: UI_CHROME.worldButtonWuxia,
|
||||
},
|
||||
{
|
||||
id: WorldType.XIANXIA,
|
||||
name: '仙侠',
|
||||
subtitle: '云灵仙境',
|
||||
icon: WORLD_SELECT_ICONS.xianxia,
|
||||
texture: UI_CHROME.worldButtonXianxia,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function generateWorldOnlineCounts(): WorldOnlineCounts {
|
||||
const roll = (base: number) =>
|
||||
Math.max(100, Math.min(200, base + Math.floor(Math.random() * 19) - 9));
|
||||
return {
|
||||
[WorldType.WUXIA]: roll(146),
|
||||
[WorldType.XIANXIA]: roll(173),
|
||||
};
|
||||
}
|
||||
|
||||
function buildLockedSeedNameSets(profile: CustomWorldProfile) {
|
||||
const lockedCharacterNames = new Set(
|
||||
profile.creatorIntent?.keyCharacters
|
||||
@@ -168,7 +135,7 @@ export function PreGameSelectionFlow({
|
||||
hasSavedGame,
|
||||
handleContinueGame,
|
||||
handleStartNewGame,
|
||||
handleWorldSelect,
|
||||
handleCustomWorldSelect,
|
||||
}: PreGameSelectionFlowProps) {
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
useState<GameState['customWorldProfile']>(null);
|
||||
@@ -176,9 +143,6 @@ export function PreGameSelectionFlow({
|
||||
CustomWorldProfile[]
|
||||
>(() => readSavedCustomWorldProfiles());
|
||||
const [showDeveloperTeamModal, setShowDeveloperTeamModal] = useState(false);
|
||||
const [worldOnlineCounts, setWorldOnlineCounts] = useState<WorldOnlineCounts>(
|
||||
() => generateWorldOnlineCounts(),
|
||||
);
|
||||
const [showCustomWorldModal, setShowCustomWorldModal] = useState(false);
|
||||
const [customWorldCreatorIntent, setCustomWorldCreatorIntent] =
|
||||
useState<CustomWorldCreatorIntent>(() =>
|
||||
@@ -200,23 +164,6 @@ export function PreGameSelectionFlow({
|
||||
[generatedCustomWorldProfile],
|
||||
);
|
||||
|
||||
const worldCards = useMemo(
|
||||
() =>
|
||||
WORLD_OPTIONS.map((world, index) => ({
|
||||
...world,
|
||||
sceneImage:
|
||||
getScenePreset(world.id, index + 1)?.imageSrc ??
|
||||
getScenePreset(world.id, 0)?.imageSrc ??
|
||||
'',
|
||||
featureIcon:
|
||||
world.id === WorldType.WUXIA
|
||||
? '/Icons/03_Torch.png'
|
||||
: '/Icons/19_Mana_potion.png',
|
||||
onlineCount: worldOnlineCounts[world.id] ?? 0,
|
||||
})),
|
||||
[worldOnlineCounts],
|
||||
);
|
||||
|
||||
const savedCustomWorldCards = useMemo(
|
||||
() =>
|
||||
savedCustomWorldProfiles.map((profile) => {
|
||||
@@ -255,12 +202,6 @@ export function PreGameSelectionFlow({
|
||||
return customWorldCreatorIntent.rawSettingText.trim();
|
||||
}, [customWorldCreatorIntent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gameState.worldType && selectionStage === 'world') {
|
||||
setWorldOnlineCounts(generateWorldOnlineCounts());
|
||||
}
|
||||
}, [gameState.worldType, selectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectionStage === 'custom-world-result' &&
|
||||
@@ -343,7 +284,7 @@ export function PreGameSelectionFlow({
|
||||
return;
|
||||
}
|
||||
|
||||
handleWorldSelect(WorldType.CUSTOM, generatedCustomWorldProfile);
|
||||
handleCustomWorldSelect(generatedCustomWorldProfile);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
@@ -454,147 +395,6 @@ export function PreGameSelectionFlow({
|
||||
);
|
||||
};
|
||||
|
||||
const regeneratePlayableNpc = async (id: string) => {
|
||||
await regenerateFromCurrentProfile(
|
||||
(currentProfile, regeneratedProfile) => {
|
||||
const targetIndex = currentProfile.playableNpcs.findIndex(
|
||||
(entry) => entry.id === id,
|
||||
);
|
||||
if (targetIndex < 0) {
|
||||
return currentProfile;
|
||||
}
|
||||
const nextNpc =
|
||||
regeneratedProfile.playableNpcs[targetIndex] ??
|
||||
regeneratedProfile.playableNpcs.find(
|
||||
(entry) =>
|
||||
entry.name === currentProfile.playableNpcs[targetIndex]?.name,
|
||||
);
|
||||
if (!nextNpc) {
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentProfile,
|
||||
playableNpcs: currentProfile.playableNpcs.map((entry, index) =>
|
||||
index === targetIndex ? nextNpc : entry,
|
||||
),
|
||||
};
|
||||
},
|
||||
{
|
||||
confirmMessage: '确认重新生成这个可扮演角色吗?当前角色的 AI 生成内容会被替换。',
|
||||
generationMode: 'full',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const regenerateStoryNpc = async (id: string) => {
|
||||
await regenerateFromCurrentProfile(
|
||||
(currentProfile, regeneratedProfile) => {
|
||||
const targetIndex = currentProfile.storyNpcs.findIndex(
|
||||
(entry) => entry.id === id,
|
||||
);
|
||||
if (targetIndex < 0) {
|
||||
return currentProfile;
|
||||
}
|
||||
const nextNpc =
|
||||
regeneratedProfile.storyNpcs[targetIndex] ??
|
||||
regeneratedProfile.storyNpcs.find(
|
||||
(entry) => entry.name === currentProfile.storyNpcs[targetIndex]?.name,
|
||||
);
|
||||
if (!nextNpc) {
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
const nextStoryNpcs = currentProfile.storyNpcs.map((entry, index) =>
|
||||
index === targetIndex ? nextNpc : entry,
|
||||
);
|
||||
|
||||
return {
|
||||
...currentProfile,
|
||||
storyNpcs: nextStoryNpcs,
|
||||
landmarks: currentProfile.landmarks.map((landmark) => ({
|
||||
...landmark,
|
||||
sceneNpcIds: landmark.sceneNpcIds.map((npcId) =>
|
||||
npcId === id ? nextNpc.id : npcId,
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
{
|
||||
confirmMessage: '确认重新生成这个场景角色吗?当前角色的 AI 生成内容会被替换。',
|
||||
generationMode: 'full',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const regenerateLandmark = async (id: string) => {
|
||||
await regenerateFromCurrentProfile(
|
||||
(currentProfile, regeneratedProfile) => {
|
||||
const targetIndex = currentProfile.landmarks.findIndex(
|
||||
(entry) => entry.id === id,
|
||||
);
|
||||
if (targetIndex < 0) {
|
||||
return currentProfile;
|
||||
}
|
||||
const nextLandmark =
|
||||
regeneratedProfile.landmarks[targetIndex] ??
|
||||
regeneratedProfile.landmarks.find(
|
||||
(entry) => entry.name === currentProfile.landmarks[targetIndex]?.name,
|
||||
);
|
||||
if (!nextLandmark) {
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentProfile,
|
||||
landmarks: currentProfile.landmarks.map((entry, index) =>
|
||||
index === targetIndex ? nextLandmark : entry,
|
||||
),
|
||||
};
|
||||
},
|
||||
{
|
||||
confirmMessage: '确认重新生成这个关键地点吗?当前场景的 AI 生成内容会被替换。',
|
||||
generationMode: 'full',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const regenerateStoryExpansion = async () => {
|
||||
await regenerateFromCurrentProfile(
|
||||
(currentProfile, regeneratedProfile) => ({
|
||||
...currentProfile,
|
||||
storyNpcs: regeneratedProfile.storyNpcs,
|
||||
}),
|
||||
{
|
||||
confirmMessage:
|
||||
'确认重新生成长尾场景角色吗?已锁定锚点会保留,其余场景角色会被新的生成结果替换。',
|
||||
generationMode: 'full',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const regenerateLandmarkNetwork = async () => {
|
||||
await regenerateFromCurrentProfile(
|
||||
(currentProfile, regeneratedProfile) => ({
|
||||
...currentProfile,
|
||||
landmarks: currentProfile.landmarks.map((landmark, index) => ({
|
||||
...landmark,
|
||||
sceneNpcIds:
|
||||
regeneratedProfile.landmarks[index]?.sceneNpcIds ??
|
||||
landmark.sceneNpcIds,
|
||||
connections:
|
||||
regeneratedProfile.landmarks[index]?.connections ??
|
||||
landmark.connections,
|
||||
})),
|
||||
}),
|
||||
{
|
||||
confirmMessage:
|
||||
'确认重新生成场景网络吗?已锁定场景名称与描述会保留,但 NPC 分布和连接关系会按最新结果刷新。',
|
||||
generationMode: 'full',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const createCustomWorld = async () => {
|
||||
if (isGeneratingCustomWorld) {
|
||||
return;
|
||||
@@ -800,7 +600,7 @@ export function PreGameSelectionFlow({
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-bold tracking-[0.2em] text-zinc-400">
|
||||
选择世界
|
||||
自定义世界
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -814,67 +614,14 @@ export function PreGameSelectionFlow({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div className="grid gap-3 pb-1 md:grid-cols-2 xl:grid-cols-3">
|
||||
{worldCards.map((world) => (
|
||||
<button
|
||||
key={world.id}
|
||||
type="button"
|
||||
onClick={() => handleWorldSelect(world.id)}
|
||||
className="pixel-nine-slice pixel-pressable order-2 relative flex min-h-[12.5rem] flex-col items-start justify-between overflow-hidden text-left"
|
||||
style={getNineSliceStyle(world.texture, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
{world.sceneImage && (
|
||||
<img
|
||||
src={world.sceneImage}
|
||||
alt={world.subtitle}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-25"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.12),rgba(8,10,14,0.82))]" />
|
||||
<div className="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/25">
|
||||
<PixelIcon
|
||||
src={world.featureIcon}
|
||||
className="h-5 w-5 opacity-95"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex h-full w-full flex-col">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] tracking-[0.2em] text-zinc-100">
|
||||
{world.name}
|
||||
</div>
|
||||
<PixelIcon
|
||||
src={world.icon}
|
||||
className="h-10 w-10 opacity-95"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<div className="text-3xl font-black text-white">
|
||||
{world.subtitle}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="rounded-full border border-emerald-400/20 bg-emerald-500/10 px-3 py-1 text-[10px] text-emerald-100">
|
||||
在线 {world.onlineCount}
|
||||
</span>
|
||||
<span className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
|
||||
推荐
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{savedCustomWorldCards.map((world) => (
|
||||
<div key={world.id} className="order-1 relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleWorldSelect(WorldType.CUSTOM, world.profile)
|
||||
handleCustomWorldSelect(world.profile)
|
||||
}
|
||||
className="pixel-nine-slice pixel-pressable relative flex min-h-[12.5rem] w-full flex-col items-start justify-between overflow-hidden text-left"
|
||||
style={getNineSliceStyle(world.texture, {
|
||||
@@ -1013,21 +760,6 @@ export function PreGameSelectionFlow({
|
||||
onContinueExpand={() => {
|
||||
void continueExpandCustomWorld();
|
||||
}}
|
||||
onRegeneratePlayableNpc={(id) => {
|
||||
void regeneratePlayableNpc(id);
|
||||
}}
|
||||
onRegenerateStoryNpc={(id) => {
|
||||
void regenerateStoryNpc(id);
|
||||
}}
|
||||
onRegenerateLandmark={(id) => {
|
||||
void regenerateLandmark(id);
|
||||
}}
|
||||
onRegenerateStoryExpansion={() => {
|
||||
void regenerateStoryExpansion();
|
||||
}}
|
||||
onRegenerateLandmarkNetwork={() => {
|
||||
void regenerateLandmarkNetwork();
|
||||
}}
|
||||
onSave={saveGeneratedCustomWorld}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import type {
|
||||
} from '../../hooks/useStoryGeneration';
|
||||
import type {
|
||||
Character,
|
||||
CustomWorldProfile,
|
||||
CompanionRenderState,
|
||||
GameState,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
|
||||
export interface GameShellSessionProps {
|
||||
@@ -46,7 +46,7 @@ export interface GameShellEntryProps {
|
||||
handleContinueGame: () => void;
|
||||
handleStartNewGame: () => void;
|
||||
handleSaveAndExit: () => void;
|
||||
handleWorldSelect: (type: WorldType, customWorldProfile?: GameState['customWorldProfile']) => void;
|
||||
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
|
||||
handleBackToWorldSelect: () => void;
|
||||
handleCharacterSelect: (character: Character) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user