import React, { useEffect, useState } from 'react'; import { AtlasTileSpec, buildMedievalNpcVisual, MedievalNpcVisualSpec } from '../data/medievalNpcVisuals'; import { Encounter } from '../types'; import { DEFAULT_NPC_LAYOUT_CONFIG, type NpcLayoutConfig, type NpcLayoutPart, } from './npcVisualShared'; const TILE_SIZE = 32; const HAND_TILE_SIZE = 16; const IDLE_FRAME_MS = 140; function mergeLayoutConfig(layoutConfig?: Partial): NpcLayoutConfig { if (!layoutConfig) return DEFAULT_NPC_LAYOUT_CONFIG; return { body: { ...DEFAULT_NPC_LAYOUT_CONFIG.body, ...layoutConfig.body }, head: { ...DEFAULT_NPC_LAYOUT_CONFIG.head, ...layoutConfig.head }, facialHair: { ...DEFAULT_NPC_LAYOUT_CONFIG.facialHair, ...layoutConfig.facialHair }, hair: { ...DEFAULT_NPC_LAYOUT_CONFIG.hair, ...layoutConfig.hair }, headgear: { ...DEFAULT_NPC_LAYOUT_CONFIG.headgear, ...layoutConfig.headgear }, hand: { ...DEFAULT_NPC_LAYOUT_CONFIG.hand, ...layoutConfig.hand }, mainHand: { ...DEFAULT_NPC_LAYOUT_CONFIG.mainHand, ...layoutConfig.mainHand }, offHand: { ...DEFAULT_NPC_LAYOUT_CONFIG.offHand, ...layoutConfig.offHand }, }; } function LayerSprite({ src, frameIndex, tileSize = TILE_SIZE, x = 0, y = 0, zIndex = 0, }: { src: string; frameIndex: number; tileSize?: number; x?: number; y?: number; zIndex?: number; }) { return (
); } function AtlasSprite({ spec, x = 0, y = 0, zIndex = 0, }: { spec: AtlasTileSpec; x?: number; y?: number; zIndex?: number; }) { const tileWidth = spec.tileWidth ?? TILE_SIZE; const tileHeight = spec.tileHeight ?? TILE_SIZE; const col = spec.frameIndex % spec.columns; const row = Math.floor(spec.frameIndex / spec.columns); return (
); } export function MedievalNpcAnimator({ encounter, visualSpec, layoutConfig, onPartPointerDown, selectedPart, className, scale = 2.4, facing = 'right', }: { encounter?: Encounter; visualSpec?: MedievalNpcVisualSpec; layoutConfig?: Partial; onPartPointerDown?: (part: NpcLayoutPart, event: React.PointerEvent) => void; selectedPart?: NpcLayoutPart | null; className?: string; scale?: number; facing?: 'left' | 'right'; }) { const [frameCursor, setFrameCursor] = useState(0); const visual = visualSpec ?? buildMedievalNpcVisual(encounter ?? { npcName: '预览角色', npcDescription: '用于预览的角色外形。', npcAvatar: '预', context: '预览', }); const bodyFrame = visual.bodyFrames[frameCursor % visual.bodyFrames.length] ?? 0; const headFrame = visual.headFrame; const hairFrame = visual.hairFrame; const handFrame = visual.handFrame; const facialFrame = visual.facialHairFrame ?? 0; const bobOffsets = [0, 1, 1, -1]; const bobY = bobOffsets[frameCursor % bobOffsets.length] ?? 0; const layout = mergeLayoutConfig(layoutConfig); const getPartClassName = (part: NpcLayoutPart) => onPartPointerDown ? `cursor-grab ${selectedPart === part ? 'drop-shadow-[0_0_10px_rgba(16,185,129,0.7)]' : ''}` : ''; const getPartHandlers = (part: NpcLayoutPart) => onPartPointerDown ? { onPointerDown: (event: React.PointerEvent) => onPartPointerDown(part, event), } : {}; useEffect(() => { const interval = window.setInterval(() => { setFrameCursor(prev => (prev + 1) % 4); }, IDLE_FRAME_MS); return () => window.clearInterval(interval); }, []); return (
{visual.mainHand && (
)}
{visual.facialHairSrc && (
)}
{visual.headgear && (
)} {visual.offHand && (
)}
); }