import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { AnimationState, type Character, type Encounter, type SceneHostileNpc, } from '../../types'; import { GameCanvasEntityLayer } from './GameCanvasEntityLayer'; import { CHARACTER_COMBAT_HP_TOP_PX, ENTITY_CONTAINER_REM, getEncounterCharacterBottomOffsetPx, getEncounterCharacterOpponentBottom, getHostileNpcSceneBottomOffsetPx, getMirroredStageEntityLeft, getNpcCombatHpTop, getSceneNpcVisualBottomOffsetPx, MONSTER_COMBAT_HP_TOP_PX, } from './GameCanvasShared'; function createCharacter(): Character { return { id: 'hero', name: '沈行', title: '试剑客', description: '测试主角', backstory: '测试背景', avatar: '/hero.png', portrait: '/hero.png', assetFolder: 'hero', assetVariant: 'default', attributes: { strength: 10, agility: 10, intelligence: 8, spirit: 9, }, personality: 'calm', skills: [], adventureOpenings: {}, } as Character; } function createEncounter(overrides: Partial = {}): Encounter { return { id: 'npc-liu', kind: 'npc', npcName: '柳无声', npcDescription: '桥口旧识', npcAvatar: '/npc-liu.png', context: '断桥', ...overrides, }; } function createHostileNpc(overrides: Partial = {}): SceneHostileNpc { return { id: 'npc-liu', name: '柳无声', action: '对峙', description: '桥口旧识', animation: 'idle', xMeters: 3, yOffset: 0, facing: 'left', attackRange: 1, speed: 1, hp: 10, maxHp: 10, encounter: createEncounter(), ...overrides, }; } function renderEntityLayer(effectNpcId: string | null) { return renderToStaticMarkup( '70%'} groundBottom="18%" stageLiftPx={68} encounter={null} sideAnchor="15%" cameraAnchorX={0} monsterAnchorMeters={3.2} playerX={0} />, ); } describe('GameCanvasEntityLayer', () => { it('uses mirrored stage anchors for player and opponent containers', () => { expect(getMirroredStageEntityLeft('15%', 'player')).toBe('15%'); expect(getMirroredStageEntityLeft('15%', 'opponent')).toBe(`calc(100% - 15% - ${ENTITY_CONTAINER_REM}rem)`); }); it('lowers large monster sprites to the shared scene ground line', () => { expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 62})).toBe(-78); expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 46})).toBe(-68); expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 37})).toBe(-52); expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 23})).toBe(-28); }); it('uses scene npc visual anchors instead of template character foot offsets', () => { const sceneNpcEncounter = createEncounter({ characterId: 'hero', monsterPresetId: 'monster-20', imageSrc: '/generated-custom-world-npc/shark.png', }); const character = createCharacter(); expect(getEncounterCharacterOpponentBottom('18%', 68, sceneNpcEncounter, character)) .toBe('calc(18% + 68px - 78px)'); expect(getEncounterCharacterBottomOffsetPx(68, sceneNpcEncounter, character)) .toBe(-10); }); it('lowers scene npc custom visuals even without character ids', () => { const sceneNpcEncounter = createEncounter({ visual: { race: 'elf', bodyColor: 'blue', headIndex: 0, hairColorIndex: 1, hairStyleFrame: 2, facialHairEnabled: false, facialHairColorIndex: 0, facialHairStyleFrame: 0, }, }); expect(getSceneNpcVisualBottomOffsetPx(sceneNpcEncounter)).toBe(-78); }); it('keeps combat hp bars above character and monster silhouettes', () => { expect(getNpcCombatHpTop('hero', null)).toBe(CHARACTER_COMBAT_HP_TOP_PX); expect(getNpcCombatHpTop(null, 'monster-20')).toBe(MONSTER_COMBAT_HP_TOP_PX); expect(getNpcCombatHpTop(null, null)).toBe(CHARACTER_COMBAT_HP_TOP_PX); }); it('renders affinity effect on the matching hostile npc', () => { const html = renderEntityLayer('npc-liu'); expect(html).toContain('data-testid="npc-affinity-effect-npc-liu"'); expect(html).toContain('aria-label="好感度变化 +3"'); }); it('does not render affinity effect on a different npc', () => { const html = renderEntityLayer('npc-other'); expect(html).not.toContain('npc-affinity-effect-npc-liu'); expect(html).not.toContain('好感度变化 +3'); }); it('renders scene act back-row encounters alongside the primary encounter', () => { const html = renderToStaticMarkup( '70%'} groundBottom="18%" stageLiftPx={68} encounter={createEncounter({ id: 'npc-primary', npcName: '主角色' })} sideAnchor="15%" cameraAnchorX={0} monsterAnchorMeters={3.2} playerX={0} />, ); expect(html).toContain('查看主角色详情'); expect(html).toContain('查看后排甲详情'); expect(html).toContain('查看后排乙详情'); }); });