-
+
@@ -1467,11 +1490,17 @@ export function PlatformHomeView({
onClick={openUserSurface}
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
>
-
+
{avatarLabel}
-
+
{authUi?.user?.displayName || '进入账户'}
diff --git a/src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx b/src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx
index 770c9c19..5b75cfeb 100644
--- a/src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx
+++ b/src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx
@@ -211,7 +211,6 @@ type TestAuthValue = {
openSettingsModal: (section?: PlatformSettingsSection) => void;
openAccountModal: () => void;
logout: () => Promise;
- setGlobalAccountActionsVisible: (visible: boolean) => void;
musicVolume: number;
setMusicVolume: (value: number) => void;
platformTheme: 'light' | 'dark';
@@ -229,7 +228,6 @@ function createAuthValue(overrides: Partial = {}): TestAuthValue
openSettingsModal: () => {},
openAccountModal: () => {},
logout: async () => {},
- setGlobalAccountActionsVisible: () => {},
musicVolume: 0.42,
setMusicVolume: () => {},
platformTheme: 'light',
diff --git a/src/data/npcInteractions.ts b/src/data/npcInteractions.ts
index 2b41b0ba..4eafafcd 100644
--- a/src/data/npcInteractions.ts
+++ b/src/data/npcInteractions.ts
@@ -26,6 +26,7 @@ import {
scoreAttributeFit,
} from './attributeResolver';
import {
+ getCharacterAdventureOpening,
getCharacterById,
getCharacterCombatStats,
getCharacterEquipment,
@@ -985,6 +986,76 @@ function getFirstContactRelationStance(npcState: NpcPersistentState) {
return npcState.relationState?.stance ?? buildRelationState(npcState.affinity).stance;
}
+function ensureDialogueSentence(text: string | null | undefined) {
+ const normalized = text?.trim() ?? '';
+ if (!normalized) {
+ return '';
+ }
+
+ return /[。!?!?]$/u.test(normalized) ? normalized : `${normalized}。`;
+}
+
+export function buildNpcChatOpeningText(
+ encounter: Encounter,
+ npcState: NpcPersistentState,
+ worldType: WorldType | null,
+ recruitCharacterOverride?: Character | null,
+) {
+ const recruitCharacter =
+ recruitCharacterOverride ?? resolveEncounterRecruitCharacter(encounter);
+ const opening = recruitCharacter
+ ? getCharacterAdventureOpening(recruitCharacter, worldType)
+ : null;
+ const stance = getFirstContactRelationStance(npcState);
+
+ if (isNpcFirstMeaningfulContact(encounter, npcState)) {
+ const greeting =
+ stance === 'guarded' ? '先打个招呼。' : '先和你打个招呼。';
+ const surfaceHook = ensureDialogueSentence(opening?.surfaceHook);
+ const immediateConcern = ensureDialogueSentence(opening?.immediateConcern);
+ const guardedMotive = ensureDialogueSentence(opening?.guardedMotive);
+ const fallbackLine =
+ stance === 'bonded'
+ ? '这一步我既然亲自来了,就说明眼前这件事得先和你对齐。'
+ : stance === 'cooperative'
+ ? '我先来和你碰个头,眼下这局势最好别各说各话。'
+ : stance === 'neutral'
+ ? '我会出现在这里不是没有缘由,不过咱们最好先把眼前情况看清。'
+ : '前面的动静不太对,我想先看看你会怎么开口。';
+
+ if (
+ encounter.specialBehavior === 'camp_companion'
+ || encounter.specialBehavior === 'initial_companion'
+ ) {
+ return [
+ greeting,
+ surfaceHook || immediateConcern || fallbackLine,
+ surfaceHook && immediateConcern && surfaceHook !== immediateConcern
+ ? immediateConcern
+ : null,
+ guardedMotive,
+ ]
+ .filter(Boolean)
+ .join('');
+ }
+
+ return [greeting, immediateConcern || surfaceHook || fallbackLine]
+ .filter(Boolean)
+ .join('');
+ }
+
+ switch (stance) {
+ case 'bonded':
+ return '又见面了。你想先从哪件事接着说?';
+ case 'cooperative':
+ return '你开口吧,我先听听你想聊哪一件。';
+ case 'neutral':
+ return '先说吧,你想从哪里问起?';
+ default:
+ return '说吧,你想先问什么?';
+ }
+}
+
export function getNpcFirstContactTopics(
encounter: Encounter,
npcState: NpcPersistentState,
diff --git a/src/data/worldAttributeSchemas.ts b/src/data/worldAttributeSchemas.ts
index 88d56fb4..7ebeb22c 100644
--- a/src/data/worldAttributeSchemas.ts
+++ b/src/data/worldAttributeSchemas.ts
@@ -169,10 +169,14 @@ export function getWorldAttributeSchema(
customWorldProfile?: CustomWorldProfile | null,
) {
if (worldType === WorldType.CUSTOM && customWorldProfile) {
- return (
- resolveCustomWorldRuleProfile(customWorldProfile)?.attributeSchema
- ?? customWorldProfile.attributeSchema
- );
+ try {
+ return (
+ resolveCustomWorldRuleProfile(customWorldProfile)?.attributeSchema
+ ?? customWorldProfile.attributeSchema
+ );
+ } catch {
+ return customWorldProfile.attributeSchema;
+ }
}
if (worldType === WorldType.XIANXIA) {
diff --git a/src/hooks/story/npcEncounterActions.test.ts b/src/hooks/story/npcEncounterActions.test.ts
index 7180752d..e3abbbe8 100644
--- a/src/hooks/story/npcEncounterActions.test.ts
+++ b/src/hooks/story/npcEncounterActions.test.ts
@@ -164,6 +164,44 @@ function createState(overrides: Partial = {}): GameState {
} as GameState;
}
+function createSceneActProfile(
+ primaryNpcId = 'npc-rival',
+): NonNullable {
+ return {
+ id: 'custom-world-scene-act-test',
+ name: '断桥旧案',
+ summary: '用于测试场景幕主角色聊天规则。',
+ playableNpcs: [],
+ storyNpcs: [],
+ sceneChapterBlueprints: [
+ {
+ id: 'scene-bridge-chapter',
+ sceneId: 'scene-bridge',
+ title: '断桥口',
+ summary: '桥口旧账还没了结。',
+ linkedThreadIds: [],
+ linkedLandmarkIds: [],
+ acts: [
+ {
+ id: 'scene-bridge-act-1',
+ sceneId: 'scene-bridge',
+ title: '对峙幕',
+ summary: '玩家与断桥客正面碰头。',
+ stageCoverage: ['opening'],
+ backgroundImageSrc: '/bridge-act-1.png',
+ encounterNpcIds: [primaryNpcId, 'npc-bystander'],
+ primaryNpcId,
+ linkedThreadIds: [],
+ advanceRule: 'after_primary_contact',
+ actGoal: '逼近断桥旧案的核心线索。',
+ transitionHook: '桥下藏着还没灭的灯。',
+ },
+ ],
+ },
+ ],
+ } as unknown as NonNullable;
+}
+
function createCurrentChatStory(): StoryMoment {
return {
text: '断桥客:你居然还敢来。\n你:我只是想把话说清楚。',
@@ -195,6 +233,42 @@ function createCurrentChatStory(): StoryMoment {
};
}
+function createLimitedPrimaryNpcChatStory(turnCount: number): StoryMoment {
+ return {
+ text: '断桥客还在压着不肯说完的话。',
+ options: [
+ createOption('npc_chat', '那你至少告诉我接下来该去哪', {
+ kind: 'npc',
+ npcId: 'npc-rival',
+ action: 'chat',
+ }),
+ ],
+ displayMode: 'dialogue',
+ dialogue: [
+ {
+ speaker: 'npc',
+ speakerName: '断桥客',
+ text: '该听见的人还没到。',
+ },
+ {
+ speaker: 'player',
+ text: '你总得让我知道下一步该往哪边走。',
+ },
+ ],
+ npcChatState: {
+ npcId: 'npc-rival',
+ npcName: '断桥客',
+ turnCount,
+ customInputPlaceholder: '输入你想对 TA 说的话',
+ sceneActId: 'scene-bridge-act-1',
+ turnLimit: 5,
+ remainingTurns: Math.max(0, 5 - turnCount),
+ limitReason: 'negative_affinity',
+ forceExitAfterTurn: false,
+ },
+ };
+}
+
function createQuest(id: string, title: string): QuestLogEntry {
return {
id,
@@ -541,6 +615,170 @@ describe('npcEncounterActions', () => {
},
);
+ it('opens npc chat without injecting a local preset opening line', () => {
+ const encounter = createEncounter();
+ streamNpcChatTurnMock.mockResolvedValueOnce({
+ affinityDelta: 0,
+ affinityText: '这轮对话暂时没有带来明显关系变化。',
+ npcReply: '先站住。你想从哪一句开始问,我先听听。',
+ suggestions: ['我先问桥上出了什么事'],
+ });
+ const actions = createNpcEncounterActions({
+ gameState: createState({
+ currentEncounter: encounter,
+ npcInteractionActive: false,
+ }),
+ currentStory: {
+ text: '断桥客站在风口,等你先挑明来意。',
+ options: [],
+ },
+ });
+
+ expect(actions.enterNpcInteraction(encounter, '与断桥客搭话')).toBe(true);
+
+ const nextStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment;
+ expect(nextStory.displayMode).toBe('dialogue');
+ expect(nextStory.dialogue ?? []).toEqual([]);
+ expect(nextStory.text).toBe('');
+ expect(nextStory.npcChatState).toMatchObject({
+ npcId: 'npc-rival',
+ npcName: '断桥客',
+ turnCount: 0,
+ });
+ });
+
+ it('streams a model-driven npc-initiated opening on first meaningful contact', async () => {
+ const encounter = createEncounter();
+ streamNpcChatTurnMock.mockResolvedValueOnce({
+ affinityDelta: 0,
+ affinityText: '这轮对话暂时没有带来明显关系变化。',
+ npcReply: '先别急着拔话头。桥上的风向刚变,我得先确认你是来问旧账,还是来救人。',
+ suggestions: ['我先听你说桥上出了什么事', '你先说你在防谁', '我不是来翻旧账的'],
+ });
+
+ const actions = createNpcEncounterActions({
+ gameState: createState({
+ currentEncounter: encounter,
+ npcInteractionActive: false,
+ npcStates: {
+ 'npc-rival': {
+ affinity: 8,
+ helpUsed: false,
+ chattedCount: 0,
+ giftsGiven: 0,
+ inventory: [],
+ recruited: false,
+ firstMeaningfulContactResolved: false,
+ },
+ },
+ }),
+ currentStory: {
+ text: '断桥客站在风口,等你先挑明来意。',
+ options: [],
+ },
+ });
+
+ expect(actions.enterNpcInteraction(encounter, '与断桥客搭话')).toBe(true);
+ await flushAsyncWork();
+
+ expect(streamNpcChatTurnMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ id: 'npc-rival',
+ }),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ [],
+ '【NPC 主动开场】',
+ expect.anything(),
+ expect.objectContaining({
+ npcInitiatesConversation: true,
+ }),
+ );
+
+ const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment;
+ expect(lastStory.streaming).toBe(false);
+ expect(lastStory.dialogue).toEqual([
+ {
+ speaker: 'npc',
+ speakerName: '断桥客',
+ text: '先别急着拔话头。桥上的风向刚变,我得先确认你是来问旧账,还是来救人。',
+ },
+ ]);
+ expect(lastStory.npcChatState).toMatchObject({
+ npcId: 'npc-rival',
+ openingSource: 'npc_initiated',
+ turnCount: 0,
+ });
+ expect(lastStory.options.map((option) => option.actionText)).toEqual([
+ '我先听你说桥上出了什么事',
+ '你先说你在防谁',
+ '我不是来翻旧账的',
+ ]);
+ });
+
+ it('removes any prefilled local opening line before the first model-driven npc reply', async () => {
+ streamNpcChatTurnMock.mockResolvedValueOnce({
+ affinityDelta: 1,
+ affinityText: '断桥客的语气稍微松了一点。',
+ npcReply: '先打个招呼。你盯着我看了这么久,总得先告诉我你想问哪一层。',
+ suggestions: ['我先问你刚才在防谁'],
+ });
+ const actions = createNpcEncounterActions({
+ currentStory: {
+ text: '先和你打个招呼。前面的风不太对。',
+ options: [
+ createOption('npc_chat', '先问问你刚才在留意什么', {
+ kind: 'npc',
+ npcId: 'npc-rival',
+ action: 'chat',
+ }),
+ ],
+ displayMode: 'dialogue',
+ dialogue: [
+ {
+ speaker: 'npc',
+ speakerName: '断桥客',
+ text: '先和你打个招呼。前面的风不太对。',
+ },
+ ],
+ npcChatState: {
+ npcId: 'npc-rival',
+ npcName: '断桥客',
+ turnCount: 0,
+ customInputPlaceholder: '输入你想对 TA 说的话',
+ openingSource: 'player_reply',
+ },
+ },
+ });
+
+ await expect(
+ actions.handleNpcChatTurn(createEncounter(), '你刚才到底在看什么?'),
+ ).resolves.toBe(true);
+
+ expect(streamNpcChatTurnMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ [],
+ '你刚才到底在看什么?',
+ expect.anything(),
+ expect.anything(),
+ );
+
+ const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment;
+ expect(
+ lastStory.dialogue?.some((turn) =>
+ turn.text.includes('先和你打个招呼。前面的风不太对。'),
+ ),
+ ).toBe(false);
+ });
+
it('passes the quest id through to the server runtime resolver for quest turn-in', async () => {
resolveServerRuntimeChoiceMock.mockResolvedValueOnce({
hydratedSnapshot: {
@@ -729,6 +967,134 @@ describe('npcEncounterActions', () => {
);
});
+ it('lets the current act primary npc enter limited chat even with negative affinity', () => {
+ const encounter = createEncounter();
+ streamNpcChatTurnMock.mockResolvedValueOnce({
+ affinityDelta: 0,
+ affinityText: '这轮对话暂时没有带来明显关系变化。',
+ npcReply: '先把来意说清楚,我再决定要不要把后半句给你。',
+ suggestions: ['你先说你到底在防谁'],
+ });
+ const actions = createNpcEncounterActions({
+ gameState: createState({
+ currentEncounter: encounter,
+ customWorldProfile: createSceneActProfile(),
+ npcInteractionActive: false,
+ npcStates: {
+ 'npc-rival': {
+ affinity: -8,
+ helpUsed: false,
+ chattedCount: 0,
+ giftsGiven: 0,
+ inventory: [],
+ recruited: false,
+ },
+ },
+ }),
+ currentStory: {
+ text: '断桥客停在桥口,像是在等你自己把话说出来。',
+ options: [],
+ },
+ });
+
+ expect(actions.enterNpcInteraction(encounter, '与断桥客搭话')).toBe(true);
+
+ const nextStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment;
+ expect(nextStory.npcChatState).toMatchObject({
+ npcId: 'npc-rival',
+ sceneActId: 'scene-bridge-act-1',
+ turnLimit: 5,
+ remainingTurns: 5,
+ limitReason: 'negative_affinity',
+ });
+ expect(
+ nextStory.options.some((option) => option.functionId === 'npc_fight'),
+ ).toBe(false);
+ expect(
+ nextStory.options.some(
+ (option) => option.functionId === 'battle_escape_breakout',
+ ),
+ ).toBe(false);
+ });
+
+ it('force exits limited hostile chat on the fifth turn and offers a continue option', async () => {
+ streamNpcChatTurnMock.mockResolvedValueOnce({
+ affinityDelta: 0,
+ affinityText: '这轮对话暂时没有带来明显关系变化。',
+ npcReply: '去城西废桥下找那盏没灭的灯。等你看见它,再来问我剩下那半句。',
+ suggestions: [],
+ chatDirective: {
+ turnLimit: 5,
+ remainingTurns: 0,
+ forceExit: true,
+ closingMode: 'foreshadow_close',
+ },
+ });
+
+ const actions = createNpcEncounterActions({
+ gameState: createState({
+ customWorldProfile: createSceneActProfile(),
+ npcStates: {
+ 'npc-rival': {
+ affinity: -12,
+ helpUsed: false,
+ chattedCount: 4,
+ giftsGiven: 0,
+ inventory: [],
+ recruited: false,
+ },
+ },
+ }),
+ currentStory: createLimitedPrimaryNpcChatStory(4),
+ });
+
+ await expect(
+ actions.handleNpcChatTurn(
+ createEncounter(),
+ '那你至少告诉我,接下来该去哪里找答案。',
+ ),
+ ).resolves.toBe(true);
+
+ expect(streamNpcChatTurnMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ id: 'npc-rival',
+ }),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ expect.anything(),
+ '那你至少告诉我,接下来该去哪里找答案。',
+ expect.anything(),
+ expect.objectContaining({
+ questOfferContext: null,
+ chatDirective: expect.objectContaining({
+ sceneActId: 'scene-bridge-act-1',
+ turnLimit: 5,
+ remainingTurns: 0,
+ limitReason: 'negative_affinity',
+ forceExitAfterTurn: true,
+ }),
+ }),
+ );
+
+ const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment;
+ expect(lastStory.npcChatState).toBeUndefined();
+ expect(lastStory.options).toEqual([
+ expect.objectContaining({
+ functionId: 'story_continue_adventure',
+ actionText: '继续',
+ }),
+ ]);
+ expect(lastStory.dialogue?.at(-1)).toEqual(
+ expect.objectContaining({
+ speaker: 'system',
+ text: '这轮交谈先在这里收束,对方留下的线索把你推向了下一步。',
+ }),
+ );
+ });
+
it('offers a pending quest after enough warmup chat turns with a positive-affinity npc', async () => {
const pendingQuest = createQuest('quest-bridge-offer', '断桥口的密信');
streamNpcChatTurnMock.mockResolvedValueOnce({
diff --git a/src/hooks/story/npcEncounterActions.ts b/src/hooks/story/npcEncounterActions.ts
index 306702b8..07773b35 100644
--- a/src/hooks/story/npcEncounterActions.ts
+++ b/src/hooks/story/npcEncounterActions.ts
@@ -21,6 +21,9 @@ import { resolveFunctionOption } from '../../data/stateFunctions';
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
import { streamNpcChatTurn } from '../../services/aiService';
import type { StoryGenerationContext } from '../../services/aiTypes';
+import {
+ resolveLimitedPrimaryNpcChatState,
+} from '../../services/customWorldSceneActRuntime';
import { generateQuestForNpcEncounter } from '../../services/questDirector';
import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory';
import { createHistoryMoment } from '../../services/storyHistory';
@@ -74,6 +77,14 @@ type BuildStoryContextExtras = {
encounterNpcStateOverride?: GameState['npcStates'][string] | null;
};
+type NpcChatDirective = {
+ sceneActId?: string | null;
+ turnLimit?: number | null;
+ remainingTurns?: number | null;
+ limitReason?: 'negative_affinity' | null;
+ forceExitAfterTurn?: boolean;
+} | null;
+
function isNpcEncounter(
encounter: GameState['currentEncounter'],
): encounter is Encounter {
@@ -103,6 +114,7 @@ export function createStoryNpcEncounterActions({
generateStoryForState,
getStoryGenerationHostileNpcs,
getAvailableOptionsForState,
+ buildContinueAdventureOption,
getNpcEncounterKey,
getResolvedNpcState,
updateNpcState,
@@ -587,9 +599,11 @@ export function createStoryNpcEncounterActions({
options: StoryOption[];
streaming: boolean;
turnCount: number;
+ chatDirective?: NpcChatDirective;
pendingQuestOffer?: {
quest: QuestLogEntry;
} | null;
+ openingSource?: 'npc_initiated' | 'player_reply';
}): StoryMoment => ({
text: params.dialogue.map((turn) => turn.text).join('\n'),
options: params.options,
@@ -601,6 +615,12 @@ export function createStoryNpcEncounterActions({
npcName: params.encounter.npcName,
turnCount: params.turnCount,
customInputPlaceholder: '输入你想对 TA 说的话',
+ openingSource: params.openingSource ?? 'player_reply',
+ sceneActId: params.chatDirective?.sceneActId ?? null,
+ turnLimit: params.chatDirective?.turnLimit ?? null,
+ remainingTurns: params.chatDirective?.remainingTurns ?? null,
+ limitReason: params.chatDirective?.limitReason ?? null,
+ forceExitAfterTurn: params.chatDirective?.forceExitAfterTurn ?? false,
pendingQuestOffer: params.pendingQuestOffer ?? null,
},
});
@@ -622,17 +642,51 @@ export function createStoryNpcEncounterActions({
});
};
- const buildNpcChatOpeningDialogue = (encounter: Encounter) =>
+ const buildLegacyNpcChatOpeningPlaceholder = (encounter: Encounter) =>
+ `${encounter.npcName}看着你,像是在等你把话接下去。`;
+
+ const sanitizeNpcChatDialogueHistory = (
+ encounter: Encounter,
+ dialogue: NonNullable,
+ turnCount: number,
+ openingSource?: StoryMoment['npcChatState'] extends infer T
+ ? T extends { openingSource?: infer U }
+ ? U
+ : never
+ : never,
+ ) => {
+ const legacyOpeningText = buildLegacyNpcChatOpeningPlaceholder(encounter);
+
+ return dialogue.filter((turn, index) => {
+ if (index !== 0 || turn.speaker !== 'npc') {
+ return true;
+ }
+
+ if (turn.text.trim() === legacyOpeningText) {
+ return false;
+ }
+
+ if (turnCount === 0 && dialogue.length === 1) {
+ return openingSource === 'npc_initiated';
+ }
+
+ return true;
+ });
+ };
+
+ const buildNpcChatDialogueHistory = (
+ encounter: Encounter,
+ turnCount: number,
+ ) =>
currentStory?.npcChatState?.npcId === (encounter.id ?? encounter.npcName) &&
currentStory.dialogue
- ? [...currentStory.dialogue]
- : [
- {
- speaker: 'npc' as const,
- speakerName: encounter.npcName,
- text: `${encounter.npcName}看着你,像是在等你把话接下去。`,
- },
- ];
+ ? sanitizeNpcChatDialogueHistory(
+ encounter,
+ currentStory.dialogue,
+ turnCount,
+ currentStory.npcChatState?.openingSource,
+ )
+ : [];
const buildHostileNpcDeclarationText = (
encounter: Encounter,
@@ -744,8 +798,10 @@ export function createStoryNpcEncounterActions({
encounter: Encounter,
selectedOption: StoryOption,
extraOptions: StoryOption[] = [],
+ chatDirective?: NpcChatDirective,
+ openingSource: 'npc_initiated' | 'player_reply' = 'player_reply',
) => {
- const openingDialogue = buildNpcChatOpeningDialogue(encounter);
+ const openingDialogue = buildNpcChatDialogueHistory(encounter, 0);
setAiError(null);
setCurrentStory(
@@ -759,11 +815,144 @@ export function createStoryNpcEncounterActions({
),
streaming: false,
turnCount: 0,
+ chatDirective,
+ openingSource,
}),
);
return true;
};
+ const startNpcInitiatedOpening = async (
+ encounter: Encounter,
+ selectedOption: StoryOption,
+ extraOptions: StoryOption[] = [],
+ chatDirective?: NpcChatDirective,
+ ) => {
+ const playerCharacter = gameState.playerCharacter;
+ if (!playerCharacter || !gameState.worldType) {
+ return enterNpcChat(
+ encounter,
+ selectedOption,
+ extraOptions,
+ chatDirective,
+ 'npc_initiated',
+ );
+ }
+
+ const npcState = getResolvedNpcState(gameState, encounter);
+ const openingCampContext = buildOpeningCampChatContext(
+ gameState,
+ playerCharacter,
+ encounter,
+ );
+ const existingDialogue = buildNpcChatDialogueHistory(encounter, 0);
+ const openingOptions = buildNpcChatEntryOptions(
+ encounter,
+ selectedOption,
+ extraOptions,
+ );
+
+ setAiError(null);
+ setIsLoading(true);
+ setCurrentStory(
+ buildNpcChatStoryMoment({
+ encounter,
+ dialogue: existingDialogue,
+ options: [],
+ streaming: true,
+ turnCount: 0,
+ chatDirective,
+ openingSource: 'npc_initiated',
+ }),
+ );
+
+ try {
+ const chatTurn = await streamNpcChatTurn(
+ gameState.worldType,
+ playerCharacter,
+ encounter,
+ getStoryGenerationHostileNpcs(gameState),
+ gameState.storyHistory,
+ buildStoryContextFromState(gameState, {
+ lastFunctionId: 'npc_chat',
+ ...openingCampContext,
+ encounterNpcStateOverride: npcState,
+ }),
+ existingDialogue,
+ '【NPC 主动开场】',
+ {
+ affinity: npcState.affinity,
+ chattedCount: npcState.chattedCount,
+ recruited: npcState.recruited,
+ },
+ {
+ onReplyUpdate: (text) => {
+ setCurrentStory(
+ buildNpcChatStoryMoment({
+ encounter,
+ dialogue: [
+ ...existingDialogue,
+ {
+ speaker: 'npc',
+ speakerName: encounter.npcName,
+ text,
+ },
+ ],
+ options: [],
+ streaming: true,
+ turnCount: 0,
+ chatDirective,
+ openingSource: 'npc_initiated',
+ }),
+ );
+ },
+ chatDirective,
+ npcInitiatesConversation: true,
+ },
+ );
+ if (!chatTurn?.npcReply?.trim()) {
+ throw new Error('NPC 主动开场结果为空');
+ }
+
+ setCurrentStory(
+ buildNpcChatStoryMoment({
+ encounter,
+ dialogue: [
+ ...existingDialogue,
+ {
+ speaker: 'npc',
+ speakerName: encounter.npcName,
+ text: chatTurn.npcReply,
+ },
+ ],
+ options: buildNpcChatTurnOptions(
+ encounter,
+ chatTurn.suggestions.length > 0
+ ? chatTurn.suggestions
+ : openingOptions.map((option) => option.actionText),
+ ),
+ streaming: false,
+ turnCount: 0,
+ chatDirective,
+ openingSource: 'npc_initiated',
+ }),
+ );
+ return true;
+ } catch (error) {
+ console.error('Failed to start npc initiated opening:', error);
+ setAiError(error instanceof Error ? error.message : 'NPC 主动开场失败');
+ return enterNpcChat(
+ encounter,
+ selectedOption,
+ extraOptions,
+ chatDirective,
+ 'npc_initiated',
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
const handleNpcChatTurn = async (
encounter: Encounter,
playerMessage: string,
@@ -780,7 +969,12 @@ export function createStoryNpcEncounterActions({
: null;
const existingDialogue =
currentStory?.dialogue && currentNpcChatState
- ? [...currentStory.dialogue]
+ ? sanitizeNpcChatDialogueHistory(
+ encounter,
+ currentStory.dialogue,
+ currentNpcChatState.turnCount ?? 0,
+ currentNpcChatState.openingSource,
+ )
: [];
const dialogueWithPlayer = [
...existingDialogue,
@@ -790,6 +984,12 @@ export function createStoryNpcEncounterActions({
},
];
const nextTurnCount = (currentNpcChatState?.turnCount ?? 0) + 1;
+ const limitedChatDirective = resolveLimitedPrimaryNpcChatState({
+ state: gameState,
+ npcId: encounter.id ?? encounter.npcName,
+ affinity: npcState.affinity,
+ nextTurnCount,
+ });
const openingCampContext = buildOpeningCampChatContext(
gameState,
playerCharacter,
@@ -805,6 +1005,7 @@ export function createStoryNpcEncounterActions({
options: [],
streaming: true,
turnCount: nextTurnCount,
+ chatDirective: limitedChatDirective,
}),
);
@@ -843,13 +1044,17 @@ export function createStoryNpcEncounterActions({
options: [],
streaming: true,
turnCount: nextTurnCount,
+ chatDirective: limitedChatDirective,
}),
);
},
- questOfferContext: {
- state: gameState,
- turnCount: nextTurnCount,
- },
+ questOfferContext: limitedChatDirective
+ ? null
+ : {
+ state: gameState,
+ turnCount: nextTurnCount,
+ },
+ chatDirective: limitedChatDirective,
},
);
@@ -912,8 +1117,45 @@ export function createStoryNpcEncounterActions({
const pendingQuest =
(chatTurn.pendingQuestOffer?.quest as QuestLogEntry | undefined) ??
null;
+ const resolvedChatDirective = limitedChatDirective
+ ? {
+ sceneActId: limitedChatDirective.sceneActId ?? null,
+ turnLimit:
+ chatTurn.chatDirective?.turnLimit ??
+ limitedChatDirective.turnLimit ??
+ null,
+ remainingTurns:
+ chatTurn.chatDirective?.remainingTurns ??
+ limitedChatDirective.remainingTurns ??
+ null,
+ limitReason: limitedChatDirective.limitReason ?? null,
+ forceExitAfterTurn:
+ chatTurn.chatDirective?.forceExit ??
+ limitedChatDirective.forceExitAfterTurn ??
+ false,
+ }
+ : null;
+ const shouldForceExitAfterTurn =
+ resolvedChatDirective?.forceExitAfterTurn === true;
const pendingQuestIntroText =
chatTurn.pendingQuestOffer?.introText?.trim() || '';
+ if (shouldForceExitAfterTurn) {
+ const closingDialogue = [
+ ...nextDialogue,
+ {
+ speaker: 'system' as const,
+ text: '这轮交谈先在这里收束,对方留下的线索把你推向了下一步。',
+ },
+ ];
+ setCurrentStory({
+ text: closingDialogue.map((turn) => turn.text).join('\n'),
+ options: [buildContinueAdventureOption()],
+ displayMode: 'dialogue',
+ dialogue: closingDialogue,
+ streaming: false,
+ });
+ return true;
+ }
if (pendingQuest) {
setCurrentStory(
buildNpcChatStoryMoment({
@@ -931,6 +1173,7 @@ export function createStoryNpcEncounterActions({
options: buildPendingQuestOfferOptions(encounter),
streaming: false,
turnCount: nextTurnCount,
+ chatDirective: resolvedChatDirective,
pendingQuestOffer: {
quest: pendingQuest,
},
@@ -951,6 +1194,7 @@ export function createStoryNpcEncounterActions({
),
streaming: false,
turnCount: nextTurnCount,
+ chatDirective: resolvedChatDirective,
}),
);
return true;
@@ -967,6 +1211,7 @@ export function createStoryNpcEncounterActions({
),
streaming: false,
turnCount: nextTurnCount,
+ chatDirective: limitedChatDirective,
}),
);
return false;
@@ -1041,7 +1286,14 @@ export function createStoryNpcEncounterActions({
setGameState(nextState);
setAiError(null);
- if (npcState.affinity < 0 || encounter.hostile) {
+ const limitedChatDirective = resolveLimitedPrimaryNpcChatState({
+ state: nextState,
+ npcId: encounter.id ?? encounter.npcName,
+ affinity: npcState.affinity,
+ nextTurnCount: 0,
+ });
+
+ if ((npcState.affinity < 0 || encounter.hostile) && !limitedChatDirective) {
setCurrentStory(
buildHostileNpcStoryMoment(
encounter,
@@ -1079,7 +1331,22 @@ export function createStoryNpcEncounterActions({
},
} satisfies StoryOption);
- return enterNpcChat(encounter, seedChatOption, chatOptions.slice(1));
+ if (!currentStory?.npcChatState && !npcState.firstMeaningfulContactResolved) {
+ void startNpcInitiatedOpening(
+ encounter,
+ seedChatOption,
+ chatOptions.slice(1),
+ limitedChatDirective,
+ );
+ return true;
+ }
+
+ return enterNpcChat(
+ encounter,
+ seedChatOption,
+ chatOptions.slice(1),
+ limitedChatDirective,
+ );
};
const resolveServerNpcStoryAction = async (params: {
diff --git a/src/hooks/story/progressionActions.ts b/src/hooks/story/progressionActions.ts
index ee7bc756..f5fac00b 100644
--- a/src/hooks/story/progressionActions.ts
+++ b/src/hooks/story/progressionActions.ts
@@ -66,6 +66,7 @@ import {
import { appendChronicleEntries } from '../../services/storyEngine/storyChronicle';
import { buildThemePackFromWorldProfile } from '../../services/storyEngine/themePack';
import { buildThreadContractsFromProfile } from '../../services/storyEngine/threadContract';
+import { buildInitialSceneActRuntimeState } from '../../services/customWorldSceneActRuntime';
import {
collectStorySignals,
resolveSignalsToThreadUpdates,
@@ -216,6 +217,12 @@ function ensureSceneChapterQuestState(params: {
storyEngineMemory: {
...storyEngineMemory,
openedSceneChapterIds,
+ currentSceneActState:
+ buildInitialSceneActRuntimeState({
+ profile: params.nextState.customWorldProfile,
+ sceneId: scene.id,
+ storyEngineMemory,
+ }) ?? storyEngineMemory.currentSceneActState ?? null,
},
};
}
@@ -223,6 +230,12 @@ function ensureSceneChapterQuestState(params: {
const nextMemory = {
...storyEngineMemory,
openedSceneChapterIds: [...openedSceneChapterIds, scene.id],
+ currentSceneActState:
+ buildInitialSceneActRuntimeState({
+ profile: params.nextState.customWorldProfile,
+ sceneId: scene.id,
+ storyEngineMemory,
+ }) ?? storyEngineMemory.currentSceneActState ?? null,
};
const existingChapterQuest = getChapterQuestForScene(
params.nextState.quests,
diff --git a/src/hooks/story/storyCampCompanion.test.ts b/src/hooks/story/storyCampCompanion.test.ts
index 27f33991..eec9f07f 100644
--- a/src/hooks/story/storyCampCompanion.test.ts
+++ b/src/hooks/story/storyCampCompanion.test.ts
@@ -73,6 +73,7 @@ function createEncounter(overrides: Partial = {}): Encounter {
return {
id: 'camp-companion',
kind: 'npc',
+ characterId: 'sword-princess',
npcName: '沈砺',
npcDescription: '正靠在营地灯火旁观察风向。',
npcAvatar: '/npc.png',
@@ -152,9 +153,11 @@ describe('storyCampCompanion', () => {
WorldType.WUXIA,
);
+ expect(text).toContain('先和你打个招呼。');
expect(text).toContain('我来这里,是为了确认旧路尽头到底出了什么事。');
- expect(text).toContain('沈砺:那就不要说得太快太多。');
+ expect(text).toContain('眼下的风向不对,我们不能直接把底牌亮出来。');
expect(text).toContain('我真正要找的东西,还不能让更多人知道。');
+ expect(text).not.toContain('像是在等你把话接下去');
});
it('summarizes the camp opening result with the current concern', () => {
@@ -168,7 +171,7 @@ describe('storyCampCompanion', () => {
expect(text).toContain('眼下的风向不对');
});
- it('keeps chat and recruit options while appending the travel action for camp openings', () => {
+ it('keeps the opening camp options focused on继续交谈', () => {
const buildNpcStory = vi.fn(() =>
createStory('营地开场', [
createOption('npc_chat', '继续交谈'),
@@ -190,11 +193,7 @@ describe('storyCampCompanion', () => {
createEncounter(),
);
- expect(options.map((option) => option.functionId)).toEqual([
- 'npc_chat',
- 'npc_recruit',
- 'camp_travel_home_scene',
- ]);
+ expect(options.map((option) => option.functionId)).toEqual(['npc_chat']);
});
it('uses AI follow-up options when the camp follow-up request succeeds and falls back on errors', async () => {
diff --git a/src/hooks/story/storyCampCompanion.ts b/src/hooks/story/storyCampCompanion.ts
index 22f8341a..9cb6c9f6 100644
--- a/src/hooks/story/storyCampCompanion.ts
+++ b/src/hooks/story/storyCampCompanion.ts
@@ -7,9 +7,11 @@ import {
NPC_CHAT_FUNCTION,
NPC_FIGHT_FUNCTION,
NPC_LEAVE_FUNCTION,
- NPC_RECRUIT_FUNCTION,
} from '../../data/functionCatalog';
-import { buildInitialNpcState } from '../../data/npcInteractions';
+import {
+ buildInitialNpcState,
+ buildNpcChatOpeningText,
+} from '../../data/npcInteractions';
import {
getForwardScenePreset,
getScenePresetById,
@@ -57,15 +59,20 @@ export function buildInitialCompanionDialogueText(
encounter: Encounter,
worldType: WorldType | null,
) {
- const opening = getCharacterAdventureOpening(character, worldType);
- const surfaceHook =
- opening?.surfaceHook ?? '这个地方与我来此的目的息息相关。';
- const immediateConcern =
- opening?.immediateConcern ?? '前方似乎有些不对劲,我们不能贸然前进。';
- const guardedMotive =
- opening?.guardedMotive ?? '我并非偶然来到这里,但我还不准备全盘托出。';
-
- return `${encounter.npcName}看着你,先压低声音开口:“${immediateConcern}。${guardedMotive}”`;
+ const resolvedEncounter =
+ encounter.characterId === character.id
+ ? encounter
+ : {
+ ...encounter,
+ characterId: encounter.characterId ?? character.id,
+ };
+ const initialNpcState = buildInitialNpcState(resolvedEncounter, worldType);
+ return buildNpcChatOpeningText(
+ resolvedEncounter,
+ initialNpcState,
+ worldType,
+ character,
+ );
}
export function buildCampCompanionOpeningResultText(
diff --git a/src/index.css b/src/index.css
index 91989c58..d676b1b4 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,21 +1,21 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Noto+Serif+SC:wght@400;700&display=swap');
-@import "tailwindcss";
+@import 'tailwindcss';
@source not "../dist";
@source not "../dist_check";
@source not "../dist_check_final";
@source not "../dist_check_monster_position";
@font-face {
- font-family: "Fusion Pixel";
- src: url("/fusion-pixel.ttf") format("truetype");
+ font-family: 'Fusion Pixel';
+ src: url('/fusion-pixel.ttf') format('truetype');
font-style: normal;
font-weight: 400;
font-display: swap;
}
@theme {
- --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
- --font-serif: "Noto Serif SC", "Georgia", serif;
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
+ --font-serif: 'Noto Serif SC', 'Georgia', serif;
}
:root {
@@ -33,22 +33,30 @@ body {
transform: translateY(0) rotate(0deg) scaleX(1) scale(1);
}
- 24% {
- transform: translateY(3%) rotate(-8deg) scaleX(1) scale(0.99);
+ 16% {
+ transform: translateY(1%) rotate(-6deg) scaleX(1) scale(1);
}
- 58% {
- transform: translateY(12%) rotate(54deg) scaleX(-1) scale(0.9);
+ 34% {
+ transform: translateY(4%) rotate(-18deg) scaleX(1) scale(0.98);
+ }
+
+ 62% {
+ transform: translateY(12%) rotate(-64deg) scaleX(-1) scale(0.9);
+ }
+
+ 82% {
+ transform: translateY(17%) rotate(-96deg) scaleX(-1) scale(0.81);
}
100% {
- transform: translateY(16%) rotate(90deg) scaleX(-1) scale(0.82);
+ transform: translateY(16%) rotate(-90deg) scaleX(-1) scale(0.82);
}
}
.fusion-pixel-app,
.fusion-pixel-app * {
- font-family: "Fusion Pixel", "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Fusion Pixel', 'Inter', ui-sans-serif, system-ui, sans-serif !important;
}
.selection-hero-brand {
@@ -64,7 +72,7 @@ body {
}
.selection-hero-brand__title {
- font-family: "Noto Serif SC", "Georgia", serif !important;
+ font-family: 'Noto Serif SC', 'Georgia', serif !important;
font-size: clamp(3rem, 10vw, 4.6rem);
font-weight: 700;
line-height: 0.95;
@@ -81,7 +89,7 @@ body {
align-items: center;
justify-content: center;
gap: clamp(0.55rem, 2vw, 0.95rem);
- font-family: "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Inter', ui-sans-serif, system-ui, sans-serif !important;
font-size: clamp(0.72rem, 2vw, 0.92rem);
font-weight: 600;
letter-spacing: 0.42em;
@@ -99,7 +107,12 @@ body {
content: '';
width: clamp(1.75rem, 8vw, 3.2rem);
height: 1px;
- background: linear-gradient(90deg, transparent, rgba(96, 165, 250, 0.72), transparent);
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(96, 165, 250, 0.72),
+ transparent
+ );
opacity: 0.82;
}
@@ -109,12 +122,12 @@ body {
.platform-ui-shell,
.platform-ui-shell * {
- font-family: "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Inter', ui-sans-serif, system-ui, sans-serif !important;
}
.platform-theme,
.platform-theme * {
- font-family: "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Inter', ui-sans-serif, system-ui, sans-serif !important;
}
.platform-theme {
@@ -123,23 +136,42 @@ body {
.platform-theme--light {
color-scheme: light;
- --platform-body-fill:
- radial-gradient(circle at top, rgba(255, 255, 255, 0.24), transparent 22%),
- radial-gradient(circle at 82% 18%, rgba(255, 209, 223, 0.26), transparent 18%),
- radial-gradient(circle at bottom right, rgba(255, 191, 173, 0.18), transparent 24%),
- linear-gradient(180deg, #f54d76 0%, #ed4168 100%);
- --platform-panel-shadow:
- 0 28px 88px rgba(201, 46, 100, 0.18),
- 0 12px 32px rgba(255, 255, 255, 0.2);
- --platform-panel-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 247, 249, 0.94));
- --platform-panel-fill-soft:
- linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 245, 248, 0.78));
- --platform-hero-fill:
- linear-gradient(135deg, rgba(255, 31, 111, 0.96), rgba(255, 135, 103, 0.92));
+ --platform-body-fill: radial-gradient(
+ circle at top left,
+ rgba(255, 108, 155, 0.16),
+ transparent 20%
+ ),
+ radial-gradient(
+ circle at 88% 4%,
+ rgba(255, 195, 150, 0.14),
+ transparent 18%
+ ),
+ radial-gradient(
+ circle at bottom,
+ rgba(255, 118, 162, 0.08),
+ transparent 22%
+ ),
+ linear-gradient(180deg, #fffafc 0%, #fffefe 48%, #fff5f8 100%);
+ --platform-panel-shadow: 0 22px 60px rgba(215, 87, 134, 0.12),
+ 0 8px 20px rgba(255, 255, 255, 0.82);
+ --platform-panel-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.985),
+ rgba(255, 250, 251, 0.96)
+ );
+ --platform-panel-fill-soft: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.95),
+ rgba(255, 248, 250, 0.88)
+ );
+ --platform-hero-fill: linear-gradient(
+ 135deg,
+ rgba(255, 31, 111, 0.96),
+ rgba(255, 135, 103, 0.92)
+ );
--platform-hero-glow-a: rgba(255, 255, 255, 0.18);
--platform-hero-glow-b: rgba(255, 197, 219, 0.18);
- --platform-surface-border: rgba(255, 255, 255, 0.5);
+ --platform-surface-border: rgba(239, 221, 228, 0.9);
--platform-surface-hover-border: rgba(255, 154, 188, 0.58);
--platform-shell-glow-1: rgba(255, 255, 255, 0.22);
--platform-shell-glow-2: rgba(255, 186, 205, 0.22);
@@ -152,10 +184,13 @@ body {
--platform-brand-logo-title: #3b1a24;
--platform-brand-logo-subtitle: #d93570;
--platform-brand-logo-shadow: #8f5870;
- --platform-line-soft: rgba(233, 183, 202, 0.42);
- --platform-subpanel-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(255, 244, 248, 0.72));
- --platform-subpanel-border: rgba(234, 193, 208, 0.5);
+ --platform-line-soft: rgba(236, 214, 221, 0.72);
+ --platform-subpanel-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.94),
+ rgba(255, 250, 251, 0.9)
+ );
+ --platform-subpanel-border: rgba(233, 217, 223, 0.82);
--platform-warm-border: rgba(255, 140, 116, 0.28);
--platform-warm-bg: rgba(255, 140, 116, 0.14);
--platform-warm-text: #cf4f4e;
@@ -181,73 +216,140 @@ body {
--platform-icon-fill: rgba(255, 255, 255, 0.62);
--platform-icon-border: rgba(232, 191, 205, 0.46);
--platform-icon-text: #7a5d67;
- --platform-nav-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.66), rgba(255, 245, 248, 0.56));
- --platform-nav-active-fill:
- linear-gradient(180deg, rgba(255, 91, 132, 0.18), rgba(255, 151, 116, 0.18));
- --platform-nav-active-border: rgba(255, 126, 154, 0.3);
- --platform-nav-active-shadow: 0 12px 28px rgba(255, 91, 132, 0.12);
- --platform-modal-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 245, 248, 0.95));
+ --platform-nav-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.96),
+ rgba(255, 249, 250, 0.92)
+ );
+ --platform-nav-active-fill: linear-gradient(
+ 180deg,
+ rgba(255, 91, 132, 0.16),
+ rgba(255, 151, 116, 0.16)
+ );
+ --platform-nav-active-border: rgba(255, 126, 154, 0.32);
+ --platform-nav-active-shadow: 0 10px 22px rgba(255, 91, 132, 0.12);
+ --platform-modal-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.96),
+ rgba(255, 245, 248, 0.95)
+ );
--platform-modal-border: rgba(255, 255, 255, 0.52);
- --platform-desktop-shell-fill:
- linear-gradient(180deg, rgba(255, 252, 253, 0.98), rgba(255, 242, 246, 0.98));
- --platform-desktop-shell-border: rgba(255, 255, 255, 0.48);
- --platform-desktop-shell-inner-border: rgba(236, 204, 215, 0.64);
- --platform-desktop-topbar-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 246, 249, 0.74));
- --platform-desktop-panel-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 246, 249, 0.72));
- --platform-desktop-panel-border: rgba(235, 195, 209, 0.46);
+ --platform-desktop-shell-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.99),
+ rgba(255, 251, 252, 0.985)
+ );
+ --platform-desktop-shell-border: rgba(240, 228, 232, 0.94);
+ --platform-desktop-shell-inner-border: rgba(241, 230, 234, 0.92);
+ --platform-desktop-topbar-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.95),
+ rgba(255, 251, 252, 0.92)
+ );
+ --platform-desktop-panel-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.95),
+ rgba(255, 250, 251, 0.91)
+ );
+ --platform-desktop-panel-border: rgba(238, 223, 228, 0.88);
--platform-desktop-hover-shadow: 0 16px 28px rgba(222, 82, 124, 0.12);
- --platform-overlay-fill:
- linear-gradient(180deg, rgba(239, 78, 122, 0.22), rgba(255, 255, 255, 0.42));
- --platform-track-border: rgba(234, 193, 208, 0.52);
- --platform-track-fill: rgba(255, 255, 255, 0.7);
- --platform-page-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(255, 245, 248, 0.24));
- --platform-page-border: rgba(255, 255, 255, 0.24);
- --platform-input-fill: rgba(255, 255, 255, 0.82);
+ --platform-overlay-fill: linear-gradient(
+ 180deg,
+ rgba(255, 184, 204, 0.14),
+ rgba(255, 255, 255, 0.56)
+ );
+ --platform-track-border: rgba(234, 193, 208, 0.46);
+ --platform-track-fill: rgba(255, 255, 255, 0.88);
+ --platform-page-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.9),
+ rgba(255, 250, 251, 0.8)
+ );
+ --platform-page-border: rgba(241, 230, 234, 0.88);
+ --platform-input-fill: rgba(255, 255, 255, 0.94);
--platform-input-fill-focus: rgba(255, 255, 255, 0.96);
- --platform-input-highlight: rgba(255, 255, 255, 0.72);
+ --platform-input-highlight: rgba(255, 255, 255, 0.9);
--platform-input-focus-ring: rgba(255, 91, 132, 0.14);
- --platform-nav-item-text: #7b606c;
+ --platform-nav-item-text: #7c6770;
--platform-nav-item-text-active: #2d1820;
- --platform-nav-item-hover-fill: rgba(255, 255, 255, 0.52);
- --platform-nav-item-icon-fill: rgba(255, 255, 255, 0.66);
+ --platform-nav-item-hover-fill: rgba(255, 244, 247, 0.92);
+ --platform-nav-item-icon-fill: rgba(248, 244, 246, 1);
--platform-nav-item-icon-text: #7a5d67;
- --platform-nav-item-icon-active-fill: rgba(255, 255, 255, 0.92);
+ --platform-nav-item-icon-active-fill: rgba(255, 255, 255, 0.98);
--platform-nav-item-icon-active-text: #d93570;
--platform-nav-icon-active-shadow: 0 12px 24px rgba(255, 91, 132, 0.16);
- --platform-profile-hero-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 245, 248, 0.9));
+ --platform-profile-hero-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.96),
+ rgba(255, 245, 248, 0.9)
+ );
--platform-profile-hero-border: rgba(255, 255, 255, 0.52);
--platform-profile-hero-shadow: 0 20px 56px rgba(216, 74, 124, 0.18);
- --platform-profile-avatar-fill:
- linear-gradient(135deg, rgba(255, 79, 139, 0.96), rgba(255, 140, 110, 0.9));
+ --platform-profile-avatar-fill: linear-gradient(
+ 135deg,
+ rgba(255, 79, 139, 0.96),
+ rgba(255, 140, 110, 0.9)
+ );
--platform-profile-avatar-shadow: 0 14px 30px rgba(255, 79, 139, 0.24);
- --platform-profile-chip-fill: rgba(255, 255, 255, 0.74);
- --platform-profile-chip-hover-fill: rgba(255, 255, 255, 0.92);
+ --platform-profile-chip-fill: rgba(255, 255, 255, 0.88);
+ --platform-profile-chip-hover-fill: rgba(255, 255, 255, 0.96);
--platform-profile-chip-text: #6a505b;
--platform-profile-action-fill: linear-gradient(135deg, #ff4f8b, #ff8a73);
--platform-profile-action-text: #fff7fb;
--platform-profile-action-shadow: 0 14px 30px rgba(255, 79, 139, 0.24);
+ --platform-card-overlay-soft: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.08),
+ rgba(255, 247, 249, 0.82)
+ );
+ --platform-card-overlay-strong: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.16),
+ rgba(255, 243, 247, 0.92)
+ );
+ --platform-card-overlay-deep: radial-gradient(
+ circle at top left,
+ rgba(255, 255, 255, 0.2),
+ transparent 30%
+ ),
+ radial-gradient(circle at right, rgba(255, 205, 178, 0.14), transparent 28%),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 241, 246, 0.9));
}
.platform-theme--dark {
color-scheme: dark;
- --platform-body-fill:
- radial-gradient(circle at top, rgba(129, 140, 248, 0.2), transparent 30%),
- radial-gradient(circle at top right, rgba(59, 130, 246, 0.12), transparent 24%),
- radial-gradient(circle at bottom left, rgba(34, 211, 238, 0.08), transparent 26%),
+ --platform-body-fill: radial-gradient(
+ circle at top,
+ rgba(129, 140, 248, 0.2),
+ transparent 30%
+ ),
+ radial-gradient(
+ circle at top right,
+ rgba(59, 130, 246, 0.12),
+ transparent 24%
+ ),
+ radial-gradient(
+ circle at bottom left,
+ rgba(34, 211, 238, 0.08),
+ transparent 26%
+ ),
linear-gradient(180deg, #13151c 0%, #090b11 100%);
--platform-panel-shadow: 0 28px 88px rgba(5, 8, 28, 0.5);
- --platform-panel-fill:
- linear-gradient(180deg, rgba(22, 20, 52, 0.95), rgba(7, 9, 21, 0.98));
- --platform-panel-fill-soft:
- linear-gradient(180deg, rgba(122, 92, 255, 0.12), rgba(255, 255, 255, 0.03));
- --platform-hero-fill:
- radial-gradient(circle at top left, rgba(129, 140, 248, 0.24), transparent 34%),
+ --platform-panel-fill: linear-gradient(
+ 180deg,
+ rgba(22, 20, 52, 0.95),
+ rgba(7, 9, 21, 0.98)
+ );
+ --platform-panel-fill-soft: linear-gradient(
+ 180deg,
+ rgba(122, 92, 255, 0.12),
+ rgba(255, 255, 255, 0.03)
+ );
+ --platform-hero-fill: radial-gradient(
+ circle at top left,
+ rgba(129, 140, 248, 0.24),
+ transparent 34%
+ ),
radial-gradient(circle at right, rgba(34, 211, 238, 0.12), transparent 32%),
linear-gradient(180deg, rgba(17, 22, 66, 0.95), rgba(8, 10, 24, 0.98));
--platform-hero-glow-a: rgba(129, 140, 248, 0.18);
@@ -293,30 +395,51 @@ body {
--platform-icon-fill: rgba(255, 255, 255, 0.05);
--platform-icon-border: rgba(255, 255, 255, 0.1);
--platform-icon-text: rgb(212 212 216);
- --platform-nav-fill:
- linear-gradient(180deg, rgba(109, 40, 217, 0.12), rgba(255, 255, 255, 0.03));
- --platform-nav-active-fill:
- linear-gradient(180deg, rgba(91, 108, 255, 0.2), rgba(61, 217, 255, 0.08));
+ --platform-nav-fill: linear-gradient(
+ 180deg,
+ rgba(109, 40, 217, 0.12),
+ rgba(255, 255, 255, 0.03)
+ );
+ --platform-nav-active-fill: linear-gradient(
+ 180deg,
+ rgba(91, 108, 255, 0.2),
+ rgba(61, 217, 255, 0.08)
+ );
--platform-nav-active-border: rgba(160, 169, 255, 0.24);
--platform-nav-active-shadow: 0 12px 28px rgba(8, 14, 42, 0.4);
- --platform-modal-fill:
- linear-gradient(180deg, rgba(16, 18, 46, 0.98), rgba(7, 8, 19, 0.98));
+ --platform-modal-fill: linear-gradient(
+ 180deg,
+ rgba(16, 18, 46, 0.98),
+ rgba(7, 8, 19, 0.98)
+ );
--platform-modal-border: rgba(160, 169, 255, 0.12);
- --platform-desktop-shell-fill:
- linear-gradient(180deg, rgba(8, 8, 30, 0.98), rgba(5, 6, 18, 0.99));
+ --platform-desktop-shell-fill: linear-gradient(
+ 180deg,
+ rgba(8, 8, 30, 0.98),
+ rgba(5, 6, 18, 0.99)
+ );
--platform-desktop-shell-border: rgba(160, 169, 255, 0.14);
--platform-desktop-shell-inner-border: rgba(255, 255, 255, 0.03);
- --platform-desktop-topbar-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.03));
- --platform-desktop-panel-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
+ --platform-desktop-topbar-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.05),
+ rgba(255, 255, 255, 0.03)
+ );
+ --platform-desktop-panel-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.05),
+ rgba(255, 255, 255, 0.02)
+ );
--platform-desktop-panel-border: rgba(160, 169, 255, 0.12);
--platform-desktop-hover-shadow: 0 16px 28px rgba(8, 11, 38, 0.2);
--platform-overlay-fill: rgba(5, 8, 28, 0.72);
--platform-track-border: rgba(255, 255, 255, 0.12);
--platform-track-fill: rgba(255, 255, 255, 0.08);
- --platform-page-fill:
- linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
+ --platform-page-fill: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.04),
+ rgba(255, 255, 255, 0.02)
+ );
--platform-page-border: rgba(255, 255, 255, 0.06);
--platform-input-fill: rgba(255, 255, 255, 0.05);
--platform-input-fill-focus: rgba(255, 255, 255, 0.08);
@@ -327,16 +450,25 @@ body {
--platform-nav-item-hover-fill: rgba(91, 108, 255, 0.08);
--platform-nav-item-icon-fill: rgba(255, 255, 255, 0.06);
--platform-nav-item-icon-text: rgb(161 161 170);
- --platform-nav-item-icon-active-fill:
- linear-gradient(180deg, rgba(91, 108, 255, 0.24), rgba(61, 217, 255, 0.12));
+ --platform-nav-item-icon-active-fill: linear-gradient(
+ 180deg,
+ rgba(91, 108, 255, 0.24),
+ rgba(61, 217, 255, 0.12)
+ );
--platform-nav-item-icon-active-text: rgb(238 248 255);
--platform-nav-icon-active-shadow: 0 12px 24px rgba(8, 14, 42, 0.42);
- --platform-profile-hero-fill:
- linear-gradient(180deg, rgba(20, 24, 58, 0.96), rgba(8, 10, 24, 0.98));
+ --platform-profile-hero-fill: linear-gradient(
+ 180deg,
+ rgba(20, 24, 58, 0.96),
+ rgba(8, 10, 24, 0.98)
+ );
--platform-profile-hero-border: rgba(160, 169, 255, 0.14);
--platform-profile-hero-shadow: 0 24px 70px rgba(5, 8, 28, 0.42);
- --platform-profile-avatar-fill:
- linear-gradient(135deg, rgba(91, 108, 255, 0.94), rgba(61, 217, 255, 0.78));
+ --platform-profile-avatar-fill: linear-gradient(
+ 135deg,
+ rgba(91, 108, 255, 0.94),
+ rgba(61, 217, 255, 0.78)
+ );
--platform-profile-avatar-shadow: 0 14px 32px rgba(61, 217, 255, 0.16);
--platform-profile-chip-fill: rgba(255, 255, 255, 0.08);
--platform-profile-chip-hover-fill: rgba(255, 255, 255, 0.14);
@@ -344,6 +476,23 @@ body {
--platform-profile-action-fill: linear-gradient(135deg, #5b6cff, #3dd9ff);
--platform-profile-action-text: rgb(238 248 255);
--platform-profile-action-shadow: 0 14px 32px rgba(91, 108, 255, 0.22);
+ --platform-card-overlay-soft: linear-gradient(
+ 180deg,
+ rgba(8, 10, 14, 0.06),
+ rgba(8, 10, 14, 0.74)
+ );
+ --platform-card-overlay-strong: linear-gradient(
+ 180deg,
+ rgba(8, 10, 14, 0.14),
+ rgba(8, 10, 14, 0.9)
+ );
+ --platform-card-overlay-deep: radial-gradient(
+ circle at top left,
+ rgba(255, 255, 255, 0.14),
+ transparent 30%
+ ),
+ radial-gradient(circle at right, rgba(255, 205, 178, 0.14), transparent 28%),
+ linear-gradient(180deg, rgba(8, 10, 14, 0.22), rgba(8, 10, 14, 0.9));
}
.platform-brand-logo {
@@ -363,7 +512,7 @@ body {
}
.platform-brand-logo__title {
- font-family: "Fusion Pixel", "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Fusion Pixel', 'Inter', ui-sans-serif, system-ui, sans-serif !important;
font-size: clamp(1.9rem, 5.2vw, 2.65rem);
font-weight: 400;
line-height: 0.92;
@@ -373,7 +522,7 @@ body {
.platform-brand-logo__subtitle {
padding-left: 0.08rem;
- font-family: "Fusion Pixel", "Inter", ui-sans-serif, system-ui, sans-serif !important;
+ font-family: 'Fusion Pixel', 'Inter', ui-sans-serif, system-ui, sans-serif !important;
font-size: clamp(0.56rem, 1.7vw, 0.7rem);
font-weight: 400;
line-height: 1;
@@ -390,10 +539,21 @@ body {
position: absolute;
inset: 0;
pointer-events: none;
- background:
- radial-gradient(circle at top, var(--platform-shell-glow-1), transparent 30%),
- radial-gradient(circle at top right, var(--platform-shell-glow-2), transparent 24%),
- radial-gradient(circle at bottom left, var(--platform-shell-glow-3), transparent 26%);
+ background: radial-gradient(
+ circle at top,
+ var(--platform-shell-glow-1),
+ transparent 30%
+ ),
+ radial-gradient(
+ circle at top right,
+ var(--platform-shell-glow-2),
+ transparent 24%
+ ),
+ radial-gradient(
+ circle at bottom left,
+ var(--platform-shell-glow-3),
+ transparent 26%
+ );
opacity: 0.9;
}
@@ -421,9 +581,16 @@ body {
position: absolute;
inset: 0;
pointer-events: none;
- background:
- radial-gradient(circle at top left, var(--platform-surface-glow-a), transparent 34%),
- radial-gradient(circle at bottom right, var(--platform-surface-glow-b), transparent 26%);
+ background: radial-gradient(
+ circle at top left,
+ var(--platform-surface-glow-a),
+ transparent 34%
+ ),
+ radial-gradient(
+ circle at bottom right,
+ var(--platform-surface-glow-b),
+ transparent 26%
+ );
}
.platform-surface > * {
@@ -449,9 +616,16 @@ body {
}
.platform-surface--light::before {
- background:
- radial-gradient(circle at top right, var(--platform-hero-glow-a), transparent 32%),
- radial-gradient(circle at bottom left, var(--platform-hero-glow-b), transparent 26%);
+ background: radial-gradient(
+ circle at top right,
+ var(--platform-hero-glow-a),
+ transparent 32%
+ ),
+ radial-gradient(
+ circle at bottom left,
+ var(--platform-hero-glow-b),
+ transparent 26%
+ );
}
.platform-interactive-card {
@@ -636,11 +810,14 @@ body {
.platform-bottom-nav__button {
display: flex;
+ box-sizing: border-box;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
+ border: 1px solid transparent;
border-radius: 1rem;
+ background: transparent;
padding: 0.35rem 0.5rem;
color: var(--platform-nav-item-text);
transition:
@@ -755,10 +932,21 @@ body {
position: absolute;
inset: 0;
pointer-events: none;
- background:
- radial-gradient(circle at top left, var(--platform-shell-glow-1), transparent 24%),
- radial-gradient(circle at top right, var(--platform-shell-glow-2), transparent 18%),
- radial-gradient(circle at 50% 0%, var(--platform-shell-glow-3), transparent 30%);
+ background: radial-gradient(
+ circle at top left,
+ var(--platform-shell-glow-1),
+ transparent 24%
+ ),
+ radial-gradient(
+ circle at top right,
+ var(--platform-shell-glow-2),
+ transparent 18%
+ ),
+ radial-gradient(
+ circle at 50% 0%,
+ var(--platform-shell-glow-3),
+ transparent 30%
+ );
}
.platform-desktop-shell::after {
@@ -787,9 +975,16 @@ body {
position: absolute;
inset: 0;
pointer-events: none;
- background:
- radial-gradient(circle at left, var(--platform-shell-glow-1), transparent 22%),
- radial-gradient(circle at right, var(--platform-shell-glow-2), transparent 24%);
+ background: radial-gradient(
+ circle at left,
+ var(--platform-shell-glow-1),
+ transparent 22%
+ ),
+ radial-gradient(
+ circle at right,
+ var(--platform-shell-glow-2),
+ transparent 24%
+ );
}
.platform-desktop-search {
@@ -862,9 +1057,16 @@ body {
position: absolute;
inset: 0;
pointer-events: none;
- background:
- radial-gradient(circle at top right, var(--platform-surface-glow-a), transparent 24%),
- radial-gradient(circle at bottom left, var(--platform-surface-glow-b), transparent 26%);
+ background: radial-gradient(
+ circle at top right,
+ var(--platform-surface-glow-a),
+ transparent 24%
+ ),
+ radial-gradient(
+ circle at bottom left,
+ var(--platform-surface-glow-b),
+ transparent 26%
+ );
}
.platform-desktop-panel > * {
@@ -988,8 +1190,11 @@ body {
.platform-role-studio__preview {
border: 1px solid var(--platform-subpanel-border);
- background:
- radial-gradient(circle at top, var(--platform-surface-glow-a), transparent 48%),
+ background: radial-gradient(
+ circle at top,
+ var(--platform-surface-glow-a),
+ transparent 48%
+ ),
var(--platform-subpanel-fill);
}
@@ -1000,54 +1205,69 @@ body {
.platform-role-studio__footer {
border-top: 1px solid var(--platform-subpanel-border);
- background:
- linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 16%),
+ background: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.08) 0%,
+ transparent 16%
+ ),
var(--platform-desktop-panel-fill);
backdrop-filter: blur(18px);
}
-.platform-theme :where(
- .platform-modal-shell,
- .platform-auth-card,
- .platform-subpanel,
- .platform-remap-surface,
- .platform-role-studio
-) :where(
- input:not([type='file']):not([type='range']):not([type='checkbox']):not([type='radio']),
- textarea,
- select
-) {
+.platform-theme
+ :where(
+ .platform-modal-shell,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface,
+ .platform-role-studio
+ )
+ :where(
+ input:not([type='file']):not([type='range']):not([type='checkbox']):not(
+ [type='radio']
+ ),
+ textarea,
+ select
+ ) {
border-color: var(--platform-subpanel-border) !important;
background: var(--platform-input-fill) !important;
color: var(--platform-text-strong) !important;
box-shadow: inset 0 1px 0 var(--platform-input-highlight);
}
-.platform-theme :where(
- .platform-modal-shell,
- .platform-auth-card,
- .platform-subpanel,
- .platform-remap-surface,
- .platform-role-studio
-) :where(
- input:not([type='file']):not([type='range']):not([type='checkbox']):not([type='radio']),
- textarea,
- select
-)::placeholder {
+.platform-theme
+ :where(
+ .platform-modal-shell,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface,
+ .platform-role-studio
+ )
+ :where(
+ input:not([type='file']):not([type='range']):not([type='checkbox']):not(
+ [type='radio']
+ ),
+ textarea,
+ select
+ )::placeholder {
color: var(--platform-text-soft) !important;
}
-.platform-theme :where(
- .platform-modal-shell,
- .platform-auth-card,
- .platform-subpanel,
- .platform-remap-surface,
- .platform-role-studio
-) :where(
- input:not([type='file']):not([type='range']):not([type='checkbox']):not([type='radio']),
- textarea,
- select
-):focus {
+.platform-theme
+ :where(
+ .platform-modal-shell,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface,
+ .platform-role-studio
+ )
+ :where(
+ input:not([type='file']):not([type='range']):not([type='checkbox']):not(
+ [type='radio']
+ ),
+ textarea,
+ select
+ ):focus {
border-color: var(--platform-nav-active-border) !important;
background: var(--platform-input-fill-focus) !important;
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
@@ -1088,395 +1308,548 @@ body {
background: var(--platform-track-fill);
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-desktop-topbar,
- .platform-desktop-search,
- .platform-desktop-rail,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-white'],
- [class*='text-zinc-50'],
- [class*='text-zinc-100']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-white'],
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-50'],
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-100'] {
color: var(--platform-text-strong) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-desktop-topbar,
- .platform-desktop-search,
- .platform-desktop-rail,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-zinc-200'],
- [class*='text-zinc-300']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-200'],
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-300'] {
color: var(--platform-text-base) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-desktop-topbar,
- .platform-desktop-search,
- .platform-desktop-rail,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-zinc-400'],
- [class*='text-zinc-500']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-400'],
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface
+ )[class*='text-zinc-500'] {
color: var(--platform-text-soft) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='bg-black/10'],
- [class*='bg-black/16'],
- [class*='bg-black/18'],
- [class*='bg-black/20'],
- [class*='bg-black/22'],
- [class*='bg-black/25'],
- [class*='bg-black/26'],
- [class*='bg-black/30'],
- [class*='bg-black/35'],
- [class*='bg-black/46'],
- [class*='bg-black/55'],
- [class*='bg-white/5'],
- [class*='bg-white/6'],
- [class*='bg-white/8']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='text-white'],
+ [class*='text-zinc-50'],
+ [class*='text-zinc-100']
+ ) {
+ color: var(--platform-text-strong) !important;
+}
+
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where([class*='text-zinc-200'], [class*='text-zinc-300']) {
+ color: var(--platform-text-base) !important;
+}
+
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-desktop-topbar,
+ .platform-desktop-search,
+ .platform-desktop-rail,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where([class*='text-zinc-400'], [class*='text-zinc-500']) {
+ color: var(--platform-text-soft) !important;
+}
+
+.platform-theme--light
+ .platform-surface:not(.platform-surface--hero)
+ :where([class*='bg-black/18'], [class*='bg-black/24']),
+.platform-theme--light
+ .platform-subpanel:where([class*='bg-black/18'], [class*='bg-black/24']),
+.platform-theme--light
+ .platform-remap-surface:where([class*='bg-black/18'], [class*='bg-black/24']) {
background: var(--platform-subpanel-fill) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='border-white/8'],
- [class*='border-white/10'],
- [class*='border-white/12'],
- [class*='border-white/16'],
- [class*='border-white/18'],
- [class*='border-white/20'],
- [class*='border-white/22'],
- [class*='border-white/25']
-) {
+.platform-theme--light
+ .platform-surface:not(.platform-surface--hero)
+ :where([class*='border-white/10'], [class*='border-white/12'], [class*='border-white/15']),
+.platform-theme--light
+ .platform-subpanel:where([class*='border-white/10'], [class*='border-white/12'], [class*='border-white/15']),
+.platform-theme--light
+ .platform-remap-surface:where([class*='border-white/10'], [class*='border-white/12'], [class*='border-white/15']) {
border-color: var(--platform-subpanel-border) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-sky-50'],
- [class*='text-sky-100'],
- [class*='text-sky-200'],
- [class*='text-sky-300']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='bg-black/10'],
+ [class*='bg-black/16'],
+ [class*='bg-black/18'],
+ [class*='bg-black/20'],
+ [class*='bg-black/22'],
+ [class*='bg-black/25'],
+ [class*='bg-black/26'],
+ [class*='bg-black/30'],
+ [class*='bg-black/35'],
+ [class*='bg-black/46'],
+ [class*='bg-black/55'],
+ [class*='bg-white/5'],
+ [class*='bg-white/6'],
+ [class*='bg-white/8']
+ ) {
+ background: var(--platform-subpanel-fill) !important;
+}
+
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='border-white/8'],
+ [class*='border-white/10'],
+ [class*='border-white/12'],
+ [class*='border-white/16'],
+ [class*='border-white/18'],
+ [class*='border-white/20'],
+ [class*='border-white/22'],
+ [class*='border-white/25']
+ ) {
+ border-color: var(--platform-subpanel-border) !important;
+}
+
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='text-sky-50'],
+ [class*='text-sky-100'],
+ [class*='text-sky-200'],
+ [class*='text-sky-300']
+ ) {
color: var(--platform-cool-text) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='bg-sky-500/8'],
- [class*='bg-sky-500/10'],
- [class*='bg-sky-500/12'],
- [class*='bg-sky-500/15']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='bg-sky-500/8'],
+ [class*='bg-sky-500/10'],
+ [class*='bg-sky-500/12'],
+ [class*='bg-sky-500/15']
+ ) {
background-color: var(--platform-cool-bg) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='border-sky-200/'],
- [class*='border-sky-300/'],
- [class*='border-sky-400/']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='border-sky-200/'],
+ [class*='border-sky-300/'],
+ [class*='border-sky-400/']
+ ) {
border-color: var(--platform-cool-border) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-amber-50'],
- [class*='text-amber-100'],
- [class*='text-amber-200']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='text-amber-50'],
+ [class*='text-amber-100'],
+ [class*='text-amber-200']
+ ) {
color: var(--platform-warm-text) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='bg-amber-500/8'],
- [class*='bg-amber-500/10'],
- [class*='bg-amber-500/12'],
- [class*='bg-amber-500/15']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='bg-amber-500/8'],
+ [class*='bg-amber-500/10'],
+ [class*='bg-amber-500/12'],
+ [class*='bg-amber-500/15']
+ ) {
background-color: var(--platform-warm-bg) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='border-amber-300/'],
- [class*='border-amber-400/']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where([class*='border-amber-300/'], [class*='border-amber-400/']) {
border-color: var(--platform-warm-border) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='text-emerald-50'],
- [class*='text-emerald-100'],
- [class*='text-emerald-600']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='text-emerald-50'],
+ [class*='text-emerald-100'],
+ [class*='text-emerald-600']
+ ) {
color: var(--platform-success-text) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='bg-emerald-500/8'],
- [class*='bg-emerald-500/10'],
- [class*='bg-emerald-500/12'],
- [class*='bg-emerald-500/15']
-) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where(
+ [class*='bg-emerald-500/8'],
+ [class*='bg-emerald-500/10'],
+ [class*='bg-emerald-500/12'],
+ [class*='bg-emerald-500/15']
+ ) {
background-color: var(--platform-success-bg) !important;
}
-.platform-theme--light :where(
- .platform-surface:not(.platform-surface--hero),
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel
-) :where(
- [class*='border-emerald-300/'],
- [class*='border-emerald-400/']
- ) {
+.platform-theme--light
+ :where(
+ .platform-surface:not(.platform-surface--hero),
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel
+ )
+ :where([class*='border-emerald-300/'], [class*='border-emerald-400/']) {
border-color: var(--platform-success-border) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-white'],
- [class*='text-zinc-50'],
- [class*='text-zinc-100']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='text-white'],
+ [class*='text-zinc-50'],
+ [class*='text-zinc-100']
+ ) {
color: var(--platform-text-strong) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-zinc-200'],
- [class*='text-zinc-300']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where([class*='text-zinc-200'], [class*='text-zinc-300']) {
color: var(--platform-text-base) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-zinc-400'],
- [class*='text-zinc-500']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where([class*='text-zinc-400'], [class*='text-zinc-500']) {
color: var(--platform-text-soft) !important;
}
-.platform-theme--light :where(
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel,
- .platform-remap-surface,
- .platform-role-studio
-) :where(
- [class~='bg-black/24'],
- [class~='bg-black/26'],
- [class~='bg-black/30'],
- [class~='bg-[#111318]/92'],
- [class~='bg-[#111318]/95'],
- [class~='bg-[#11161f]']
-) {
+.platform-theme--light
+ :where(
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-remap-surface,
+ .platform-role-studio
+ )
+ :where(
+ [class~='bg-black/24'],
+ [class~='bg-black/26'],
+ [class~='bg-black/30'],
+ [class~='bg-[#111318]/92'],
+ [class~='bg-[#111318]/95'],
+ [class~='bg-[#11161f]']
+ ) {
background: var(--platform-subpanel-fill) !important;
}
-.platform-theme--light :where(
- .platform-modal-shell,
- .platform-desktop-panel,
- .platform-desktop-trending-item,
- .platform-auth-card,
- .platform-subpanel,
- .platform-role-studio
-) :where(
- [class~='bg-white/10'],
- [class~='bg-white/12']
-) {
+.platform-theme--light
+ :where(
+ .platform-modal-shell,
+ .platform-desktop-panel,
+ .platform-desktop-trending-item,
+ .platform-auth-card,
+ .platform-subpanel,
+ .platform-role-studio
+ )
+ :where([class~='bg-white/10'], [class~='bg-white/12']) {
background-color: var(--platform-track-fill) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class~='bg-black/18'],
- [class~='bg-black/20'],
- [class~='bg-black/22'],
- [class~='bg-black/24'],
- [class~='bg-black/26'],
- [class~='bg-black/30'],
- [class~='bg-[#111318]/92'],
- [class~='bg-[#111318]/95'],
- [class~='bg-white/5'],
- [class~='bg-white/6'],
- [class~='bg-white/8']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class~='bg-black/18'],
+ [class~='bg-black/20'],
+ [class~='bg-black/22'],
+ [class~='bg-black/24'],
+ [class~='bg-black/26'],
+ [class~='bg-black/30'],
+ [class~='bg-[#111318]/92'],
+ [class~='bg-[#111318]/95'],
+ [class~='bg-white/5'],
+ [class~='bg-white/6'],
+ [class~='bg-white/8']
+ ) {
background: var(--platform-subpanel-fill) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='border-white/8'],
- [class*='border-white/10'],
- [class*='border-white/12'],
- [class*='border-white/14'],
- [class*='border-white/16'],
- [class*='border-white/18'],
- [class*='border-white/20'],
- [class*='border-white/22'],
- [class*='border-white/25']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='border-white/8'],
+ [class*='border-white/10'],
+ [class*='border-white/12'],
+ [class*='border-white/14'],
+ [class*='border-white/16'],
+ [class*='border-white/18'],
+ [class*='border-white/20'],
+ [class*='border-white/22'],
+ [class*='border-white/25']
+ ) {
border-color: var(--platform-subpanel-border) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-sky-50'],
- [class*='text-sky-100'],
- [class*='text-sky-200'],
- [class*='text-sky-300']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='text-sky-50'],
+ [class*='text-sky-100'],
+ [class*='text-sky-200'],
+ [class*='text-sky-300']
+ ) {
color: var(--platform-cool-text) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='bg-sky-500/8'],
- [class*='bg-sky-500/10'],
- [class*='bg-sky-500/12'],
- [class*='bg-sky-500/15']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='bg-sky-500/8'],
+ [class*='bg-sky-500/10'],
+ [class*='bg-sky-500/12'],
+ [class*='bg-sky-500/15']
+ ) {
background-color: var(--platform-cool-bg) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='border-sky-200/'],
- [class*='border-sky-300/'],
- [class*='border-sky-400/']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='border-sky-200/'],
+ [class*='border-sky-300/'],
+ [class*='border-sky-400/']
+ ) {
border-color: var(--platform-cool-border) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-amber-50'],
- [class*='text-amber-100'],
- [class*='text-amber-200']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='text-amber-50'],
+ [class*='text-amber-100'],
+ [class*='text-amber-200']
+ ) {
color: var(--platform-warm-text) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='bg-amber-500/8'],
- [class*='bg-amber-500/10'],
- [class*='bg-amber-500/12'],
- [class*='bg-amber-500/15']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='bg-amber-500/8'],
+ [class*='bg-amber-500/10'],
+ [class*='bg-amber-500/12'],
+ [class*='bg-amber-500/15']
+ ) {
background-color: var(--platform-warm-bg) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='border-amber-300/'],
- [class*='border-amber-400/']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where([class*='border-amber-300/'], [class*='border-amber-400/']) {
border-color: var(--platform-warm-border) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='text-emerald-50'],
- [class*='text-emerald-100'],
- [class*='text-emerald-600']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='text-emerald-50'],
+ [class*='text-emerald-100'],
+ [class*='text-emerald-600']
+ ) {
color: var(--platform-success-text) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='bg-emerald-500/8'],
- [class*='bg-emerald-500/10'],
- [class*='bg-emerald-500/12'],
- [class*='bg-emerald-500/15']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where(
+ [class*='bg-emerald-500/8'],
+ [class*='bg-emerald-500/10'],
+ [class*='bg-emerald-500/12'],
+ [class*='bg-emerald-500/15']
+ ) {
background-color: var(--platform-success-bg) !important;
}
-.platform-theme--light .platform-remap-surface :where(
- [class*='border-emerald-300/'],
- [class*='border-emerald-400/']
-) {
+.platform-theme--light
+ .platform-remap-surface
+ :where([class*='border-emerald-300/'], [class*='border-emerald-400/']) {
border-color: var(--platform-success-border) !important;
}
@@ -1498,20 +1871,33 @@ button {
box-sizing: border-box;
border-style: solid;
border-color: transparent;
- border-top-width: calc(var(--slice-top, 16) * var(--frame-scale, var(--ui-scale)) * 1px);
- border-right-width: calc(var(--slice-right, 16) * var(--frame-scale, var(--ui-scale)) * 1px);
- border-bottom-width: calc(var(--slice-bottom, 16) * var(--frame-scale, var(--ui-scale)) * 1px);
- border-left-width: calc(var(--slice-left, 16) * var(--frame-scale, var(--ui-scale)) * 1px);
+ border-top-width: calc(
+ var(--slice-top, 16) * var(--frame-scale, var(--ui-scale)) * 1px
+ );
+ border-right-width: calc(
+ var(--slice-right, 16) * var(--frame-scale, var(--ui-scale)) * 1px
+ );
+ border-bottom-width: calc(
+ var(--slice-bottom, 16) * var(--frame-scale, var(--ui-scale)) * 1px
+ );
+ border-left-width: calc(
+ var(--slice-left, 16) * var(--frame-scale, var(--ui-scale)) * 1px
+ );
border-image-source: var(--frame-src);
- border-image-slice: var(--slice-top, 16) var(--slice-right, 16) var(--slice-bottom, 16) var(--slice-left, 16) fill;
+ border-image-slice: var(--slice-top, 16) var(--slice-right, 16)
+ var(--slice-bottom, 16) var(--slice-left, 16) fill;
border-image-repeat: var(--frame-repeat, round);
- padding: calc(var(--frame-pad-y, 12) * var(--frame-scale, var(--ui-scale)) * 1px)
+ padding: calc(
+ var(--frame-pad-y, 12) * var(--frame-scale, var(--ui-scale)) * 1px
+ )
calc(var(--frame-pad-x, 14) * var(--frame-scale, var(--ui-scale)) * 1px);
background-color: var(--frame-base, transparent);
}
.pixel-pressable {
- transition: transform 160ms ease, filter 160ms ease;
+ transition:
+ transform 160ms ease,
+ filter 160ms ease;
}
.pixel-pressable:hover {
@@ -1617,7 +2003,10 @@ button {
gap: 1rem;
min-height: var(--world-card-height);
transform-origin: center center;
- transition: transform 220ms ease, opacity 220ms ease, filter 220ms ease;
+ transition:
+ transform 220ms ease,
+ opacity 220ms ease,
+ filter 220ms ease;
scroll-snap-align: center;
will-change: transform, opacity, filter;
}
@@ -1681,7 +2070,10 @@ button {
flex: 0 0 var(--character-card-width);
height: clamp(16.25rem, 38vh, 18.5rem);
scroll-snap-align: center;
- transition: transform 220ms ease, opacity 220ms ease, filter 220ms ease;
+ transition:
+ transform 220ms ease,
+ opacity 220ms ease,
+ filter 220ms ease;
transform-origin: center bottom;
will-change: transform, opacity, filter;
}
@@ -1693,7 +2085,11 @@ button {
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
- background: linear-gradient(180deg, rgba(18, 22, 30, 0.92), rgba(6, 8, 12, 0.98));
+ background: linear-gradient(
+ 180deg,
+ rgba(18, 22, 30, 0.92),
+ rgba(6, 8, 12, 0.98)
+ );
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
}
@@ -1709,9 +2105,16 @@ button {
align-items: flex-end;
justify-content: center;
overflow: hidden;
- background:
- radial-gradient(circle at 50% 22%, rgba(255, 255, 255, 0.14), transparent 42%),
- linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 38%);
+ background: radial-gradient(
+ circle at 50% 22%,
+ rgba(255, 255, 255, 0.14),
+ transparent 42%
+ ),
+ linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.06),
+ rgba(255, 255, 255, 0) 38%
+ );
}
.character-carousel__portrait {
diff --git a/src/prompts/customWorldRolePromptDefaults.ts b/src/prompts/customWorldRolePromptDefaults.ts
index 4cb76cd8..554dfe8f 100644
--- a/src/prompts/customWorldRolePromptDefaults.ts
+++ b/src/prompts/customWorldRolePromptDefaults.ts
@@ -1,3 +1,21 @@
+/**
+ * 自定义世界角色资产工坊的“默认描述文本种子”主源。
+ *
+ * 这份脚本只负责一件事:
+ * - 从当前角色对象已有字段里挑出最合适的文本,
+ * 作为资产工坊输入框的初始默认值
+ *
+ * 它不负责:
+ * - 直接调用 LLM 重新编译默认描述
+ * - 直接生成图像模型 prompt
+ * - 直接生成动作模型 prompt
+ *
+ * 当前真实调用状态:
+ * - CustomWorldRoleAssetStudioModal 的初始默认值主链,来自本文件
+ * - 也就是说,资产工坊页面打开时看到的“形象描述 / 动作描述”
+ * 当前优先取这里的本地字段映射,而不是后端
+ * /api/assets/character-prompts/generate 接口
+ */
export type PromptDefaultRole = {
name: string;
title: string;
@@ -19,10 +37,18 @@ export type CustomWorldRolePromptBundle = {
scenePromptText: string;
};
+/**
+ * 对角色字段做轻量清洗,确保作为输入框默认值时不会带多余空白。
+ */
function cleanSeedText(value: string | undefined, maxLength: number) {
return (value ?? '').replace(/\s+/gu, ' ').trim().slice(0, maxLength);
}
+/**
+ * 按优先级选择第一条可用文本。
+ *
+ * 这里是非常轻量的本地回退逻辑,不做任何“重新创作”或 prompt 扩写。
+ */
function pickFirstDescription(
values: Array,
maxLength: number,
@@ -37,6 +63,18 @@ function pickFirstDescription(
return '';
}
+/**
+ * 资产工坊默认文本映射规则。
+ *
+ * 规则分层:
+ * - visualPromptText: 优先使用角色 visualDescription,其次 description
+ * - animationPromptText: 优先使用 actionDescription,其次 combatStyle
+ * - scenePromptText: 优先使用 sceneVisualDescription,其次 backstory
+ *
+ * 注意:
+ * - 返回值只是“输入框默认文案”
+ * - 正式图像 / 动作模型 prompt 还会在后端继续编译
+ */
export function buildDefaultRolePromptBundle(
role: PromptDefaultRole,
): CustomWorldRolePromptBundle {
diff --git a/src/prompts/storyPromptBuilders.ts b/src/prompts/storyPromptBuilders.ts
index d6552216..58663d60 100644
--- a/src/prompts/storyPromptBuilders.ts
+++ b/src/prompts/storyPromptBuilders.ts
@@ -30,6 +30,11 @@ import {
getFunctionById,
getFunctionPromptDescription,
} from '../data/stateFunctions';
+import type { StoryGenerationContext } from '../services/aiTypes';
+import { buildCustomWorldReferenceText } from '../services/customWorld';
+import { sanitizePromptNarrativeText } from '../services/narrativeLanguage';
+import { describeGoalStackForPrompt } from '../services/storyEngine/goalDirector';
+import { buildStoryPromptHistory } from '../services/storyHistory';
import {
Character,
CharacterGender,
@@ -40,11 +45,6 @@ import {
StoryOption,
WorldType,
} from '../types';
-import type { StoryGenerationContext } from '../services/aiTypes';
-import { buildCustomWorldReferenceText } from '../services/customWorld';
-import { sanitizePromptNarrativeText } from '../services/narrativeLanguage';
-import { describeGoalStackForPrompt } from '../services/storyEngine/goalDirector';
-import { buildStoryPromptHistory } from '../services/storyHistory';
export const SYSTEM_PROMPT = `你是角色扮演 RPG 的剧情推进者,只能返回 JSON 对象,不能输出解释、markdown 或代码块。
输出格式必须严格符合:
@@ -1817,8 +1817,11 @@ export function buildStrictNpcChatDialoguePrompt(
'不要让对方在聊天里推进交易、招募、切磋、战斗、送礼、求助、离开、继续前进、切换场景等其他 function。',
'不要替玩家做选择,不要用建议句、命令句或诱导句把聊天写成别的行为入口。',
'低揭示阶段时,宁可留钩子、先谈眼前局势,也不要把完整来历和目标一次说完。',
+ context.isFirstMeaningfulContact
+ ? '如果这是第一次真正接触,对方第一次开口必须先用一句自然招呼或开场判断起手,不能写成“某人看着你,像是在等你把话接下去”这类第三人称占位旁白。'
+ : null,
'如果当前情景是初见或刚打完一轮冲突,优先写短句、观察句和试探句,不要写成正式自我介绍。',
- ].join('\n\n');
+ ].filter(Boolean).join('\n\n');
}
export function buildNpcRecruitDialoguePrompt(
diff --git a/src/services/aiService.ts b/src/services/aiService.ts
index 99085642..8a539ddd 100644
--- a/src/services/aiService.ts
+++ b/src/services/aiService.ts
@@ -979,6 +979,7 @@ export async function streamNpcChatTurn(
turnCount: number;
} | null;
chatDirective?: NpcChatTurnDirective | null;
+ npcInitiatesConversation?: boolean;
} = {},
) {
const payload = {
@@ -993,6 +994,7 @@ export async function streamNpcChatTurn(
dialogue: conversationHistory ?? [],
playerMessage,
npcState,
+ npcInitiatesConversation: options.npcInitiatesConversation ?? false,
questOfferContext: options.questOfferContext
? {
state: options.questOfferContext.state,
diff --git a/src/types/story.ts b/src/types/story.ts
index 69d9e959..7a615f86 100644
--- a/src/types/story.ts
+++ b/src/types/story.ts
@@ -116,6 +116,7 @@ export interface StoryNpcChatState {
npcName: string;
turnCount: number;
customInputPlaceholder?: string;
+ openingSource?: 'npc_initiated' | 'player_reply';
sceneActId?: string | null;
turnLimit?: number | null;
remainingTurns?: number | null;