This commit is contained in:
2026-04-28 02:05:12 +08:00
parent 271db02e4a
commit 1eb090e4a5
39 changed files with 2671 additions and 165 deletions

View File

@@ -14,6 +14,7 @@ import {
getEncounterCharacterBottomOffsetPx,
getEncounterCharacterOpponentBottom,
getHostileNpcSceneBottomOffsetPx,
getMonsterWorldLeft,
getMirroredStageEntityLeft,
getNpcCombatHpTop,
getSceneNpcVisualBottomOffsetPx,
@@ -173,6 +174,59 @@ describe('GameCanvasEntityLayer', () => {
expect(getNpcCombatHpTop(null, null)).toBe(CHARACTER_COMBAT_HP_TOP_PX);
});
it('does not apply scene visual ground offset twice for custom character enemies in battle', () => {
const hostileNpc = createHostileNpc({
encounter: createEncounter({
id: 'npc-shark-elder',
npcName: '珊瑚长老',
characterId: 'hero',
imageSrc: '/generated-custom-world-npc/shark-elder.png',
}),
});
const html = renderToStaticMarkup(
<GameCanvasEntityLayer
companions={[]}
sceneActAmbientEncounters={[]}
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={true}
onEntitySelect={null}
playerLeft="20%"
playerCharacter={createCharacter()}
playerHp={100}
playerMaxHp={100}
effectivePlayerFacing="right"
effectivePlayerAnimationState={AnimationState.IDLE}
shouldShowPlayerDialogueIcon={false}
dialogueIndicator={null}
npcAffinityEffect={null}
sceneCombatants={[hostileNpc]}
monsters={[]}
getHostileNpcOuterLeft={() => '70%'}
groundBottom="18%"
stageLiftPx={68}
encounter={null}
sideAnchor="15%"
cameraAnchorX={0}
monsterAnchorMeters={3.2}
playerX={0}
/>,
);
expect(html).toContain('bottom:calc(calc(18% + 68px) + -78px)');
expect(html).not.toContain('bottom:calc(calc(18% + 68px - 78px) + -78px)');
});
it('renders affinity effect on the matching hostile npc', () => {
const html = renderEntityLayer('npc-liu');
@@ -283,4 +337,160 @@ describe('GameCanvasEntityLayer', () => {
expect(html).toContain('查看后排甲详情');
expect(html).toContain('查看后排乙详情');
});
it('keeps hostile combatant identity stable while attack position changes', () => {
const sideAnchor = '15%';
const cameraAnchorX = 0;
const monsterAnchorMeters = 3.2;
const attackingNpc = createHostileNpc({
id: 'npc-attacker',
xMeters: 0.1,
animation: 'attack',
combatMode: 'melee',
encounter: createEncounter({
id: 'npc-attacker',
npcName: '突进敌人',
}),
});
const renderedLeft = getMonsterWorldLeft(
sideAnchor,
attackingNpc.xMeters,
cameraAnchorX,
monsterAnchorMeters,
);
const html = renderToStaticMarkup(
<GameCanvasEntityLayer
companions={[]}
sceneActAmbientEncounters={[]}
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={true}
onEntitySelect={null}
playerLeft="20%"
playerCharacter={createCharacter()}
playerHp={100}
playerMaxHp={100}
effectivePlayerFacing="right"
effectivePlayerAnimationState={AnimationState.IDLE}
shouldShowPlayerDialogueIcon={false}
dialogueIndicator={null}
npcAffinityEffect={null}
sceneCombatants={[attackingNpc]}
monsters={[]}
getHostileNpcOuterLeft={(hostileNpc) =>
getMonsterWorldLeft(
sideAnchor,
hostileNpc.xMeters,
cameraAnchorX,
monsterAnchorMeters,
)
}
groundBottom="18%"
stageLiftPx={68}
encounter={null}
sideAnchor={sideAnchor}
cameraAnchorX={cameraAnchorX}
monsterAnchorMeters={monsterAnchorMeters}
playerX={0}
/>,
);
expect(html).toContain(`left:${renderedLeft}`);
});
it('keeps enemy formation positions when battle starts before any attack dash', () => {
const sideAnchor = '15%';
const cameraAnchorX = 0;
const monsterAnchorMeters = 3.2;
const frontNpc = createHostileNpc({
id: 'npc-front',
xMeters: 3.2,
encounter: createEncounter({
id: 'npc-front',
npcName: '前排敌人',
}),
});
const backNpc = createHostileNpc({
id: 'npc-back',
xMeters: 4.28,
yOffset: 62,
encounter: createEncounter({
id: 'npc-back',
npcName: '后排敌人',
}),
});
const html = renderToStaticMarkup(
<GameCanvasEntityLayer
companions={[]}
sceneActAmbientEncounters={[]}
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={true}
onEntitySelect={null}
playerLeft="20%"
playerCharacter={createCharacter()}
playerHp={100}
playerMaxHp={100}
effectivePlayerFacing="right"
effectivePlayerAnimationState={AnimationState.IDLE}
shouldShowPlayerDialogueIcon={false}
dialogueIndicator={null}
npcAffinityEffect={null}
sceneCombatants={[frontNpc, backNpc]}
monsters={[]}
getHostileNpcOuterLeft={(hostileNpc) =>
getMonsterWorldLeft(
sideAnchor,
hostileNpc.xMeters,
cameraAnchorX,
monsterAnchorMeters,
)
}
groundBottom="18%"
stageLiftPx={68}
encounter={null}
sideAnchor={sideAnchor}
cameraAnchorX={cameraAnchorX}
monsterAnchorMeters={monsterAnchorMeters}
playerX={0}
/>,
);
const frontLeft = `left:${getMonsterWorldLeft(
sideAnchor,
frontNpc.xMeters,
cameraAnchorX,
monsterAnchorMeters,
)}`;
const backLeft = `left:${getMonsterWorldLeft(
sideAnchor,
backNpc.xMeters,
cameraAnchorX,
monsterAnchorMeters,
)}`;
expect(html).toContain(frontLeft);
expect(html).toContain(backLeft);
});
});