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

This commit is contained in:
2026-04-20 15:45:14 +08:00
parent 8a7bd90458
commit 1c72066bab
73 changed files with 7814 additions and 1018 deletions

View File

@@ -110,6 +110,9 @@ interface AdventurePanelProps {
onSaveAndExit: () => void;
chapterState?: ChapterState | null;
journeyBeat?: JourneyBeat | null;
currentSceneActTitle?: string | null;
currentSceneActIndex?: number | null;
currentSceneActCount?: number | null;
}
const AdventurePanelOverlays = lazy(async () => {
@@ -280,19 +283,6 @@ function formatPlayTime(playTimeMs: number) {
return `${minutes}${String(seconds).padStart(2, '0')}`;
}
function getPlayerProgressionRatio(
statistics: AdventurePanelProps['statistics'],
) {
const currentLevelXp = Math.max(0, statistics.playerCurrentLevelXp ?? 0);
const xpToNextLevel = Math.max(0, statistics.playerXpToNextLevel ?? 0);
if (xpToNextLevel <= 0) {
return 1;
}
return Math.max(0, Math.min(1, currentLevelXp / xpToNextLevel));
}
function getOptionGoalAffordanceClass(option: StoryOption) {
switch (option.goalAffordance?.relation) {
case 'advance':
@@ -675,6 +665,9 @@ export function AdventurePanel({
onSaveAndExit,
chapterState = null,
journeyBeat = null,
currentSceneActTitle = null,
currentSceneActIndex = null,
currentSceneActCount = null,
}: AdventurePanelProps) {
const isDialogueStory = currentStory.displayMode === 'dialogue';
const dialogueTurns = currentStory.dialogue ?? [];
@@ -931,13 +924,10 @@ export function AdventurePanel({
],
[statistics],
);
const playerLevel = Math.max(1, statistics.playerLevel ?? 1);
const playerCurrentLevelXp = Math.max(
0,
statistics.playerCurrentLevelXp ?? 0,
);
const playerXpToNextLevel = Math.max(0, statistics.playerXpToNextLevel ?? 0);
const playerProgressionRatio = getPlayerProgressionRatio(statistics);
const limitedNpcChatRemainingTurns =
npcChatState?.turnLimit && npcChatState.limitReason === 'negative_affinity'
? Math.max(0, npcChatState.remainingTurns ?? 0)
: null;
const shouldMountAdventureOverlays =
isGoalPanelOpen ||
isSettingsPanelOpen ||
@@ -1059,9 +1049,32 @@ export function AdventurePanel({
<div
ref={storyScrollContainerRef}
className="pixel-nine-slice pixel-panel mb-3 min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide"
className="pixel-nine-slice pixel-panel mb-2 min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide"
style={getNineSliceStyle(UI_CHROME.storyPanel)}
>
{(currentSceneActTitle || limitedNpcChatRemainingTurns !== null) && (
<div className="mb-3 flex flex-wrap items-center gap-2 px-1">
{currentSceneActTitle ? (
<div className="inline-flex items-center gap-2 rounded-full border border-sky-300/18 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.16em] text-sky-100">
<span></span>
<span className="text-white/90">{currentSceneActTitle}</span>
{currentSceneActIndex && currentSceneActCount ? (
<span className="text-sky-100/65">
{currentSceneActIndex}/{currentSceneActCount}
</span>
) : null}
</div>
) : null}
{limitedNpcChatRemainingTurns !== null ? (
<div className="inline-flex items-center gap-2 rounded-full border border-rose-300/18 bg-rose-500/10 px-3 py-1 text-[10px] tracking-[0.16em] text-rose-100">
<span></span>
<span className="text-white/90">
{limitedNpcChatRemainingTurns}
</span>
</div>
) : null}
</div>
)}
{isDialogueStory ? (
<div className="space-y-3">
{dialogueTurns.length > 0 ? (
@@ -1083,6 +1096,8 @@ export function AdventurePanel({
</div>
</div>
))
) : isNpcChatMode && !isStoryStreaming ? (
<div className="h-1" aria-hidden="true" />
) : (
<div className="flex justify-start">
<div className="rounded-2xl border border-white/10 bg-black/20 px-3 py-2 text-sm text-zinc-400">
@@ -1098,29 +1113,8 @@ export function AdventurePanel({
)}
</div>
<div className="mt-auto shrink-0 pb-2">
<div className="mb-2 rounded-xl border border-amber-300/15 bg-[radial-gradient(circle_at_top,rgba(251,191,36,0.14),transparent_65%),rgba(0,0,0,0.24)] px-3 py-2.5">
<div className="flex items-center justify-between gap-3 text-[11px]">
<div className="font-semibold text-amber-50">Lv.{playerLevel}</div>
<div className="text-zinc-400">
{playerXpToNextLevel > 0
? `${playerCurrentLevelXp}/${playerXpToNextLevel}`
: 'MAX'}
</div>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full border border-white/10 bg-black/30">
<div
className="h-full rounded-full bg-[linear-gradient(90deg,rgba(251,191,36,0.78),rgba(253,224,71,0.96))]"
style={{
width:
playerProgressionRatio <= 0
? '0%'
: `${Math.max(6, playerProgressionRatio * 100)}%`,
}}
/>
</div>
</div>
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="mt-auto shrink-0 pb-[calc(env(safe-area-inset-bottom,0px)+0.35rem)]">
<div className="mb-1.5 flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<button
type="button"
@@ -1167,7 +1161,7 @@ export function AdventurePanel({
) : null}
</div>
<div className="space-y-2">
<div className="space-y-1.5">
{isLoading && !isStoryStreaming ? (
<div className="flex items-center justify-center space-x-2 p-4 text-zinc-600">
<Loader2 className="h-4 w-4 animate-spin" />
@@ -1275,7 +1269,7 @@ export function AdventurePanel({
);
})}
{isNpcChatMode && !isNpcQuestOfferMode ? (
<div className="pixel-nine-slice pixel-panel mt-0.5 border border-white/10 bg-black/25 p-1.5">
<div className="pixel-nine-slice pixel-panel border border-white/10 bg-black/25 p-1.5">
<div className="flex min-w-0 items-center gap-2">
<input
value={npcChatDraft}