import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useMemo, useState } from 'react';
import { getCharacterById } from '../data/characterPresets';
import { MAX_COMPANIONS } from '../data/npcInteractions';
import { Character, CompanionState } from '../types';
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { PixelCloseButton } from './PixelCloseButton';
import { ResolvedAssetImage } from './ResolvedAssetImage';
interface CompanionCampModalProps {
isOpen: boolean;
playerCharacter: Character | null;
companions: CompanionState[];
roster: CompanionState[];
inBattle: boolean;
onClose: () => void;
onBenchCompanion: (npcId: string) => void;
onActivateCompanion: (npcId: string, swapNpcId?: string | null) => void;
}
type CompanionCardData = {
companion: CompanionState;
character: Character;
};
function StatusPill({ label, value }: { label: string; value: string }) {
return (
{label} {value}
);
}
function buildCampMoments(
playerCharacter: Character | null,
activeCompanions: CompanionCardData[],
reserveCompanions: CompanionCardData[],
) {
if (!playerCharacter) {
return ['营地尚未准备完毕。'];
}
const moments: string[] = [];
if (activeCompanions.length === 0 && reserveCompanions.length === 0) {
moments.push(`${playerCharacter.name}独自坐在营火旁,暂时还没有固定同行者。`);
}
if (activeCompanions.length >= 2) {
const firstCompanion = activeCompanions[0];
const secondCompanion = activeCompanions[1];
if (firstCompanion && secondCompanion) {
moments.push(`${firstCompanion.character.name}和${secondCompanion.character.name}正低声商量下一段路怎么走。`);
}
}
const trustedCompanion = activeCompanions.find(item => item.companion.joinedAtAffinity >= 70);
if (trustedCompanion) {
moments.push(`${trustedCompanion.character.name}熟练地清点补给,看起来已经像能交托后背的同伴了。`);
}
if (reserveCompanions.length > 0) {
const reserveCompanion = reserveCompanions[0];
if (reserveCompanion) {
moments.push(`${reserveCompanion.character.name}正在营地里待命,随时都能重新归队。`);
}
}
if (moments.length === 0) {
moments.push(`${playerCharacter.name}环视营地,确认众人都已经各就各位。`);
}
return moments.slice(0, 3);
}
export function CompanionCampModal({
isOpen,
playerCharacter,
companions,
roster,
inBattle,
onClose,
onBenchCompanion,
onActivateCompanion,
}: CompanionCampModalProps) {
const [selectedSwapNpcId, setSelectedSwapNpcId] = useState(null);
const activeCompanionCards = useMemo(
() => companions
.map(companion => {
const character = getCharacterById(companion.characterId);
return character ? { companion, character } : null;
})
.filter(Boolean) as CompanionCardData[],
[companions],
);
const reserveCompanionCards = useMemo(
() => roster
.map(companion => {
const character = getCharacterById(companion.characterId);
return character ? { companion, character } : null;
})
.filter(Boolean) as CompanionCardData[],
[roster],
);
const campMoments = useMemo(
() => buildCampMoments(playerCharacter, activeCompanionCards, reserveCompanionCards),
[activeCompanionCards, playerCharacter, reserveCompanionCards],
);
useEffect(() => {
if (!isOpen) return;
if (companions.length >= MAX_COMPANIONS) {
setSelectedSwapNpcId(companions[0]?.npcId ?? null);
return;
}
setSelectedSwapNpcId(null);
}, [companions, isOpen]);
return (
{isOpen && (
event.stopPropagation()}
>
营地编组
{playerCharacter ? `${playerCharacter.name} / 出战 ${companions.length}/${MAX_COMPANIONS}` : '队伍调度'}
当前队伍
可直接把同行者转入后备,或先选定替换位,再让后备成员归队。
{inBattle && (
战斗中无法调整编组。
)}
{activeCompanionCards.length > 0 ? activeCompanionCards.map(({ companion, character }) => {
const selectedForSwap = selectedSwapNpcId === companion.npcId;
return (
{character.name}
{character.title}
);
}) : (
当前没有已出战的同行者。
)}
后备队伍
后备同行者会在营地待命,随时可以重新召回。
{reserveCompanionCards.length > 0 ? reserveCompanionCards.map(({ companion, character }) => {
const needsSwap = companions.length >= MAX_COMPANIONS;
return (
{character.name}
{character.title}
);
}) : (
当前还没有后备同行者。
)}
营地气氛
{campMoments.map((moment, index) => (
{moment}
))}
)}
);
}