1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 11:30:19 +08:00
parent 50759f3c1e
commit 8a7bd90458
85 changed files with 7290 additions and 1903 deletions

View File

@@ -39,6 +39,8 @@ export function GameShellCanvasStage({
encounter={visibleGameState.currentEncounter}
currentScenePreset={visibleGameState.currentScenePreset}
worldType={visibleGameState.worldType}
customWorldProfile={visibleGameState.customWorldProfile}
storyEngineMemory={visibleGameState.storyEngineMemory}
sceneHostileNpcs={visibleGameState.sceneHostileNpcs}
playerX={visibleGameState.playerX}
playerOffsetY={visibleGameState.playerOffsetY}

View File

@@ -703,7 +703,6 @@ export function PlatformHomeView({
saves: Archive,
profile: UserRound,
} as const;
const latestSaveEntry = saveEntries[0] ?? null;
const openUserSurface = () => {
if (authUi?.user) {
authUi.openAccountModal();
@@ -876,41 +875,6 @@ export function PlatformHomeView({
<div className={MOBILE_PAGE_STAGE_CLASS}>
{authUi?.user ? (
<>
<section
className={`${HERO_SURFACE_CLASS} relative overflow-hidden px-[18px] py-4 text-left`}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_30%),radial-gradient(circle_at_right,rgba(255,205,178,0.18),transparent_28%),linear-gradient(135deg,rgba(255,92,120,0.92),rgba(255,139,98,0.9))]" />
<div className="relative z-10 flex min-h-[10.5rem] flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="platform-pill platform-pill--cool">
SAVE ARCHIVE
</span>
<div className="platform-pill platform-pill--neutral px-3 text-[11px] tracking-[0.08em]">
{saveEntries.length > 0 ? `${saveEntries.length} 个存档` : '暂无存档'}
</div>
</div>
<div className="flex min-w-0 items-stretch gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<div className="line-clamp-2 break-words text-[1.95rem] font-black leading-[1.02] text-white sm:text-3xl">
{latestSaveEntry ? latestSaveEntry.worldName : '存档'}
</div>
<div className="mt-2 text-sm leading-6 text-zinc-200/88">
{latestSaveEntry
? `最近更新于 ${formatSnapshotTime(latestSaveEntry.lastPlayedAt)},点开后可直接继续游玩。`
: '你在平台里留下的最近可恢复存档会显示在这里。'}
</div>
</div>
{latestSaveEntry ? (
<SaveArchivePreview
entry={latestSaveEntry}
label="最近更新"
className="h-[8.8rem] w-[6.1rem] sm:h-[9.4rem] sm:w-[7rem]"
/>
) : null}
</div>
</div>
</section>
{saveError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{saveError}

View File

@@ -641,6 +641,7 @@ test('authenticated users with save archives default into the saves tab', async
expect((await screen.findAllByText('全部存档')).length).toBeGreaterThan(0);
expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
expect(screen.getAllByText('最近更新时间排序').length).toBeGreaterThan(0);
expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
});
test('save tab can resume a selected archive directly into the game', async () => {

View File

@@ -85,6 +85,10 @@ export interface GameShellAdventureStatistics {
scenesTraveled: number;
currentSceneName: string;
playerCurrency: number;
playerLevel?: number;
playerCurrentLevelXp?: number;
playerXpToNextLevel?: number;
playerTotalXp?: number;
inventoryItemCount: number;
inventoryStackCount: number;
activeCompanionCount: number;

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { normalizePlayerProgressionState } from '../../data/playerProgression';
import { getLiveGamePlayTimeMs } from '../../data/runtimeStats';
import { getWorldCampScenePreset } from '../../data/scenePresets';
import type {
@@ -41,7 +42,8 @@ export function buildGameShellDialogueIndicator(params: {
return {
showPlayer: true,
showEncounter: true,
activeSpeaker: lastSpeaker === 'player' ? 'player' : lastSpeaker ? 'npc' : null,
activeSpeaker:
lastSpeaker === 'player' ? 'player' : lastSpeaker ? 'npc' : null,
};
}
@@ -62,7 +64,7 @@ export function buildCanvasCompanionRenderStates(params: {
}) {
const activeEncounterNpcId =
params.visibleGameState.currentEncounter?.kind === 'npc'
? params.visibleGameState.currentEncounter.id ?? null
? (params.visibleGameState.currentEncounter.id ?? null)
: null;
if (!activeEncounterNpcId) {
return params.visibleCompanionRenderStates;
@@ -79,6 +81,9 @@ export function buildAdventureStatistics(params: {
livePlayTimeMs: number;
}): GameShellAdventureStatistics {
const { gameState, visibleGameState, livePlayTimeMs } = params;
const playerProgression = normalizePlayerProgressionState(
visibleGameState.playerProgression ?? null,
);
return {
playTimeMs: livePlayTimeMs,
@@ -94,6 +99,10 @@ export function buildAdventureStatistics(params: {
scenesTraveled: gameState.runtimeStats.scenesTraveled,
currentSceneName: visibleGameState.currentScenePreset?.name ?? '当前区域',
playerCurrency: visibleGameState.playerCurrency,
playerLevel: playerProgression.level,
playerCurrentLevelXp: playerProgression.currentLevelXp,
playerXpToNextLevel: playerProgression.xpToNextLevel,
playerTotalXp: playerProgression.totalXp,
inventoryItemCount: visibleGameState.playerInventory.reduce(
(sum, item) => sum + item.quantity,
0,
@@ -104,17 +113,11 @@ export function buildAdventureStatistics(params: {
};
}
export function useGameShellRuntimeViewModel(params: Pick<
GameShellProps,
'session' | 'story' | 'companions'
>) {
export function useGameShellRuntimeViewModel(
params: Pick<GameShellProps, 'session' | 'story' | 'companions'>,
) {
const { session, story, companions } = params;
const {
gameState,
currentStory,
isLoading,
isMapOpen,
} = session;
const { gameState, currentStory, isLoading, isMapOpen } = session;
const { npcUi, characterChatUi, handleChoice } = story;
const { buildCompanionRenderStates } = companions;
@@ -122,7 +125,7 @@ export function useGameShellRuntimeViewModel(params: Pick<
const openingCampSceneId = useMemo(
() =>
gameState.worldType
? getWorldCampScenePreset(gameState.worldType)?.id ?? null
? (getWorldCampScenePreset(gameState.worldType)?.id ?? null)
: null,
[gameState.worldType],
);