@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user