This commit is contained in:
188
src/components/game-canvas/GameCanvasEntityLayer.test.tsx
Normal file
188
src/components/game-canvas/GameCanvasEntityLayer.test.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
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> = {}): Encounter {
|
||||
return {
|
||||
id: 'npc-liu',
|
||||
kind: 'npc',
|
||||
npcName: '柳无声',
|
||||
npcDescription: '桥口旧识',
|
||||
npcAvatar: '/npc-liu.png',
|
||||
context: '断桥',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createHostileNpc(overrides: Partial<SceneHostileNpc> = {}): 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(
|
||||
<GameCanvasEntityLayer
|
||||
companions={[]}
|
||||
currentScenePreset={null}
|
||||
sceneTransitionToken={0}
|
||||
isSceneTransitionEntering={false}
|
||||
isSceneTransitionExiting={false}
|
||||
transitionSweepPx={320}
|
||||
sceneTransitionExitDurationS={0.2}
|
||||
sceneTransitionEntryDurationS={0.2}
|
||||
companionAnchorLeft="10%"
|
||||
companionAnchorBottom="20%"
|
||||
playerBottomOffsetPx={0}
|
||||
sceneTransitionPhase="idle"
|
||||
inBattle={false}
|
||||
onEntitySelect={null}
|
||||
playerLeft="20%"
|
||||
playerCharacter={createCharacter()}
|
||||
playerHp={100}
|
||||
playerMaxHp={100}
|
||||
effectivePlayerFacing="right"
|
||||
effectivePlayerAnimationState={AnimationState.IDLE}
|
||||
shouldShowPlayerDialogueIcon={false}
|
||||
dialogueIndicator={null}
|
||||
npcAffinityEffect={
|
||||
effectNpcId
|
||||
? {
|
||||
eventId: 'effect-1',
|
||||
npcId: effectNpcId,
|
||||
delta: 3,
|
||||
}
|
||||
: null
|
||||
}
|
||||
sceneCombatants={[createHostileNpc()]}
|
||||
monsters={[]}
|
||||
getHostileNpcOuterLeft={() => '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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user