1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 15:45:14 +08:00
parent 8a7bd90458
commit 1c72066bab
73 changed files with 7814 additions and 1018 deletions

View File

@@ -33,6 +33,7 @@ import {
RoleCharacterSprite,
SCENE_TRANSITION_LOWER_COMPANION_DELAY_S,
SCENE_TRANSITION_UPPER_COMPANION_DELAY_S,
SceneEncounterNpcSprite,
SceneEntityButton,
} from './GameCanvasShared';
@@ -403,7 +404,9 @@ export function GameCanvasEntityLayer({
style={{imageRendering: 'pixelated'}}
/>
</div>
) : peacefulResolvedCharacter ? (
) : peacefulResolvedCharacter &&
!encounter.visual &&
!encounter.imageSrc?.trim() ? (
<RoleCharacterSprite
state={AnimationState.IDLE}
character={peacefulResolvedCharacter}
@@ -417,11 +420,11 @@ export function GameCanvasEntityLayer({
className="scale-[1.82] origin-bottom"
/>
) : (
<MedievalNpcAnimator
<SceneEncounterNpcSprite
encounter={encounter}
className="drop-shadow-[0_8px_14px_rgba(0,0,0,0.38)]"
state={AnimationState.IDLE}
facing={peacefulNpcSpriteFacing}
scale={GENERIC_NPC_SCENE_SCALE}
className="drop-shadow-[0_8px_14px_rgba(0,0,0,0.38)]"
/>
)}
</div>

View File

@@ -2,7 +2,10 @@ import React, {useEffect, useState} from 'react';
import {getCharacterById} from '../../data/characterPresets';
import {METERS_TO_PIXELS} from '../../data/hostileNpcs';
import {buildMedievalNpcVisualFromCustomWorldVisual} from '../../data/medievalNpcVisuals';
import {
buildMedievalNpcVisual,
buildMedievalNpcVisualFromCustomWorldVisual,
} from '../../data/medievalNpcVisuals';
import {
AnimationState,
Character,
@@ -246,6 +249,87 @@ export function RoleCharacterSprite({
);
}
export function SceneEncounterNpcSprite({
encounter,
state,
facing,
className,
}: {
encounter: Encounter;
state: AnimationState;
facing: 'left' | 'right';
className?: string;
}) {
if (encounter.visual) {
return (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(encounter.visual)}
className={`origin-bottom ${className ?? ''}`.trim()}
scale={1.36}
facing={facing}
/>
);
}
if (encounter.imageSrc?.trim()) {
return (
<img
src={encounter.imageSrc.trim()}
alt={encounter.npcName}
className={`h-full w-full object-contain ${className ?? ''}`.trim()}
style={{
...DEFAULT_IMAGE_STYLE,
transform: facing === 'left' ? 'scaleX(-1)' : undefined,
transformOrigin: 'bottom center',
}}
/>
);
}
const runtimeCustomWorldCharacter =
encounter.characterId ? getCharacterById(encounter.characterId) : null;
if (runtimeCustomWorldCharacter?.visual) {
return (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(runtimeCustomWorldCharacter.visual)}
className={`origin-bottom ${className ?? ''}`.trim()}
scale={1.36}
facing={facing}
/>
);
}
if (runtimeCustomWorldCharacter) {
return (
<div
className="h-full w-full"
style={{transform: facing === 'left' ? 'scaleX(-1)' : undefined}}
>
<CharacterAnimator
state={state}
character={runtimeCustomWorldCharacter}
className={ROLE_CHARACTER_SPRITE_CLASS}
/>
</div>
);
}
return (
<MedievalNpcAnimator
visualSpec={buildMedievalNpcVisual({
id: encounter.id ?? encounter.npcName,
npcName: encounter.npcName,
npcDescription: encounter.npcDescription,
npcAvatar: encounter.npcAvatar,
context: encounter.context,
} as Encounter)}
className={`origin-bottom ${className ?? ''}`.trim()}
scale={1.36}
facing={facing}
/>
);
}
export function DialogueBubbleIcon({
active = false,
flip = false,