1
This commit is contained in:
@@ -53,6 +53,10 @@ import {
|
||||
import { HostileNpcAnimator } from '../HostileNpcAnimator';
|
||||
import { PixelIcon } from '../PixelIcon';
|
||||
|
||||
const BATTLE_OPTION_ROW_MIN_HEIGHT = 58;
|
||||
const BATTLE_OPTION_ROW_GAP = 6;
|
||||
const DEFAULT_BATTLE_VISIBLE_OPTION_COUNT = 4;
|
||||
|
||||
export interface RpgAdventurePanelProps {
|
||||
aiError: string | null;
|
||||
currentStory: StoryMoment;
|
||||
@@ -140,6 +144,57 @@ function getOptionActionTextClass(option: StoryOption) {
|
||||
return 'text-zinc-300 group-hover:text-white';
|
||||
}
|
||||
|
||||
function getBattleVisibleOptionCount(availableHeight: number, total: number) {
|
||||
if (total <= 0) return 0;
|
||||
|
||||
if (!Number.isFinite(availableHeight) || availableHeight <= 0) {
|
||||
return Math.min(total, DEFAULT_BATTLE_VISIBLE_OPTION_COUNT);
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
total,
|
||||
Math.floor(
|
||||
(availableHeight + BATTLE_OPTION_ROW_GAP) /
|
||||
(BATTLE_OPTION_ROW_MIN_HEIGHT + BATTLE_OPTION_ROW_GAP),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function useMeasuredElementHeight<T extends HTMLElement>(enabled: boolean) {
|
||||
const elementRef = useRef<T | null>(null);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setHeight(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const updateHeight = () => {
|
||||
setHeight(element.getBoundingClientRect().height);
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const observer = new ResizeObserver(updateHeight);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateHeight);
|
||||
return () => window.removeEventListener('resize', updateHeight);
|
||||
}, [enabled]);
|
||||
|
||||
return [elementRef, height] as const;
|
||||
}
|
||||
|
||||
function getOptionFunctionTagText(option: StoryOption) {
|
||||
const tagByFunctionId: Record<string, string> = {
|
||||
battle_all_in_crush: '战斗',
|
||||
@@ -692,11 +747,14 @@ function RpgAdventureStorySection(props: {
|
||||
isStoryStreaming,
|
||||
currentStory,
|
||||
} = props;
|
||||
const storyPanelClassName = isNpcChatMode
|
||||
? 'flex-[1.18] sm:min-h-[15rem]'
|
||||
: 'flex-1 sm:min-h-[14rem]';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={storyScrollContainerRef}
|
||||
className="pixel-nine-slice pixel-panel mb-2 min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide"
|
||||
className={`pixel-nine-slice pixel-panel mb-3 min-h-0 overflow-y-auto pr-1 scrollbar-hide ${storyPanelClassName}`}
|
||||
style={getNineSliceStyle(UI_CHROME.storyPanel)}
|
||||
>
|
||||
{(currentSceneActTitle || limitedNpcChatRemainingTurns !== null) && (
|
||||
@@ -787,6 +845,7 @@ function RpgAdventureChoiceSection(props: {
|
||||
setNpcChatDraft: (value: string) => void;
|
||||
npcChatPlaceholder: string;
|
||||
submitNpcChatDraft: () => void;
|
||||
inBattle: boolean;
|
||||
}) {
|
||||
const {
|
||||
isNpcChatMode,
|
||||
@@ -813,11 +872,29 @@ function RpgAdventureChoiceSection(props: {
|
||||
setNpcChatDraft,
|
||||
npcChatPlaceholder,
|
||||
submitNpcChatDraft,
|
||||
inBattle,
|
||||
} = props;
|
||||
const [battleChoiceViewportRef, battleChoiceViewportHeight] =
|
||||
useMeasuredElementHeight<HTMLDivElement>(
|
||||
inBattle && !isNpcChatMode && !shouldHideChoiceUi,
|
||||
);
|
||||
const visibleDisplayedOptions =
|
||||
inBattle && !isNpcChatMode && !shouldHideChoiceUi
|
||||
? displayedOptions.slice(
|
||||
0,
|
||||
getBattleVisibleOptionCount(
|
||||
battleChoiceViewportHeight,
|
||||
displayedOptions.length,
|
||||
),
|
||||
)
|
||||
: displayedOptions;
|
||||
|
||||
return (
|
||||
<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={`mt-auto min-h-0 pb-[calc(env(safe-area-inset-bottom,0px)+0.1rem)] pt-1.5 ${inBattle ? 'flex flex-1 flex-col' : 'shrink-0'}`}
|
||||
>
|
||||
{/* 让底部操作区整体更贴近屏幕底部,同时把上方聊天区腾出更稳定的展示高度。 */}
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -880,12 +957,15 @@ function RpgAdventureChoiceSection(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div
|
||||
ref={battleChoiceViewportRef}
|
||||
className={`space-y-1.5 ${inBattle ? 'min-h-0 flex-1 overflow-hidden' : ''}`}
|
||||
>
|
||||
{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" />
|
||||
<span className="text-xs uppercase tracking-widest">
|
||||
剧情推演中...
|
||||
{inBattle ? '战斗结算中...' : '剧情推演中...'}
|
||||
</span>
|
||||
</div>
|
||||
) : isStoryStreaming ? (
|
||||
@@ -896,7 +976,7 @@ function RpgAdventureChoiceSection(props: {
|
||||
<div className="p-4" aria-hidden="true" />
|
||||
) : (
|
||||
<>
|
||||
{displayedOptions.map((option, index) => {
|
||||
{visibleDisplayedOptions.map((option, index) => {
|
||||
const optionImpactSummary = getOptionImpactSummary(
|
||||
option,
|
||||
playerCharacter,
|
||||
@@ -970,7 +1050,7 @@ function RpgAdventureChoiceSection(props: {
|
||||
);
|
||||
})}
|
||||
{isNpcChatMode && !isNpcQuestOfferMode ? (
|
||||
<div className="pixel-nine-slice pixel-panel border border-white/10 bg-black/25 p-1.5">
|
||||
<div className="pixel-nine-slice pixel-panel border border-white/10 bg-black/25 px-1.5 pb-1.5 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<input
|
||||
value={npcChatDraft}
|
||||
@@ -985,7 +1065,7 @@ function RpgAdventureChoiceSection(props: {
|
||||
}
|
||||
}}
|
||||
placeholder={npcChatPlaceholder}
|
||||
className="h-9 min-w-0 flex-1 rounded-md border border-white/10 bg-black/35 px-3 text-sm text-zinc-100 outline-none placeholder:text-zinc-500 focus:border-amber-200/40"
|
||||
className="h-10 min-w-0 flex-1 rounded-md border border-white/10 bg-black/35 px-3 text-sm text-zinc-100 outline-none placeholder:text-zinc-500 focus:border-amber-200/40"
|
||||
maxLength={80}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -993,7 +1073,7 @@ function RpgAdventureChoiceSection(props: {
|
||||
type="button"
|
||||
onClick={submitNpcChatDraft}
|
||||
disabled={isLoading || !npcChatDraft.trim()}
|
||||
className="inline-flex h-9 shrink-0 items-center rounded-md border border-amber-300/20 bg-amber-500/10 px-2.5 text-[11px] text-amber-100 transition-colors disabled:cursor-not-allowed disabled:opacity-40 sm:px-3 sm:text-xs"
|
||||
className="inline-flex h-10 shrink-0 items-center rounded-md border border-amber-300/20 bg-amber-500/10 px-2.5 text-[11px] text-amber-100 transition-colors disabled:cursor-not-allowed disabled:opacity-40 sm:px-3 sm:text-xs"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
@@ -1161,6 +1241,7 @@ export function RpgAdventurePanel({
|
||||
playerMana,
|
||||
playerMaxMana,
|
||||
playerSkillCooldowns,
|
||||
inBattle,
|
||||
currentNpcBattleMode,
|
||||
statistics,
|
||||
musicVolume,
|
||||
@@ -1550,18 +1631,20 @@ export function RpgAdventurePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RpgAdventureStorySection
|
||||
currentSceneActTitle={currentSceneActTitle}
|
||||
currentSceneActIndex={currentSceneActIndex}
|
||||
currentSceneActCount={currentSceneActCount}
|
||||
limitedNpcChatRemainingTurns={limitedNpcChatRemainingTurns}
|
||||
storyScrollContainerRef={storyScrollContainerRef}
|
||||
isDialogueStory={isDialogueStory}
|
||||
dialogueTurns={dialogueTurns}
|
||||
isNpcChatMode={isNpcChatMode}
|
||||
isStoryStreaming={isStoryStreaming}
|
||||
currentStory={currentStory}
|
||||
/>
|
||||
{!inBattle ? (
|
||||
<RpgAdventureStorySection
|
||||
currentSceneActTitle={currentSceneActTitle}
|
||||
currentSceneActIndex={currentSceneActIndex}
|
||||
currentSceneActCount={currentSceneActCount}
|
||||
limitedNpcChatRemainingTurns={limitedNpcChatRemainingTurns}
|
||||
storyScrollContainerRef={storyScrollContainerRef}
|
||||
isDialogueStory={isDialogueStory}
|
||||
dialogueTurns={dialogueTurns}
|
||||
isNpcChatMode={isNpcChatMode}
|
||||
isStoryStreaming={isStoryStreaming}
|
||||
currentStory={currentStory}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<RpgAdventureChoiceSection
|
||||
isNpcChatMode={isNpcChatMode}
|
||||
@@ -1590,6 +1673,7 @@ export function RpgAdventurePanel({
|
||||
npcChatState?.customInputPlaceholder ?? '输入你想说的话'
|
||||
}
|
||||
submitNpcChatDraft={submitNpcChatDraft}
|
||||
inBattle={inBattle}
|
||||
/>
|
||||
|
||||
<RpgAdventureOverlaySection
|
||||
|
||||
Reference in New Issue
Block a user