This commit is contained in:
2026-04-27 14:23:19 +08:00
parent 09d3fe59b3
commit fa2dbb310b
75 changed files with 7363 additions and 1487 deletions

View File

@@ -51,6 +51,7 @@ type MonsterSpriteConfig = (typeof MONSTERS_BY_WORLD)[WorldType.WUXIA][number];
interface GameCanvasEntityLayerProps {
companions: CompanionRenderState[];
sceneActAmbientEncounters: Encounter[];
currentScenePreset: ScenePresetInfo | null;
sceneTransitionToken: number;
isSceneTransitionEntering: boolean;
@@ -93,6 +94,13 @@ interface GameCanvasEntityLayerProps {
playerX: number;
}
const SCENE_ACT_BACK_ROW_ANCHOR_X_METERS = RESOLVED_ENTITY_X_METERS + 1.08;
const SCENE_ACT_BACK_ROW_OFFSET_PX = [62, -46] as const;
function addCssPxOffset(value: string, offsetPx: number) {
return offsetPx === 0 ? value : `calc(${value} + ${offsetPx}px)`;
}
function CombatFloatingNumber({
event,
onDone,
@@ -177,6 +185,7 @@ function CombatReactiveSpriteFrame({
export function GameCanvasEntityLayer({
companions,
sceneActAmbientEncounters,
currentScenePreset,
sceneTransitionToken,
isSceneTransitionEntering,
@@ -415,9 +424,16 @@ export function GameCanvasEntityLayer({
</div>
</motion.div>
{sceneCombatants.map(hostileNpc => {
{sceneCombatants.map((hostileNpc, index) => {
const npcEncounter = hostileNpc.encounter;
if (!npcEncounter) return null;
const hostileRenderKey = [
hostileNpc.id,
npcEncounter.id ?? npcEncounter.npcName,
hostileNpc.xMeters,
hostileNpc.yOffset ?? 0,
index,
].join(':');
const config = monsters.find(item => item.id === hostileNpc.id);
const renderOffset = MONSTER_RENDER_OFFSETS[hostileNpc.id] ?? {x: 0, y: 0};
const npcCharacter = npcEncounter?.characterId ? getCharacterById(npcEncounter.characterId) : null;
@@ -453,7 +469,7 @@ export function GameCanvasEntityLayer({
return (
<div
key={hostileNpc.id}
key={hostileRenderKey}
className="absolute"
style={{
left: getHostileNpcOuterLeft(hostileNpc),
@@ -628,6 +644,111 @@ export function GameCanvasEntityLayer({
</div>
);
})()}
{!inBattle &&
sceneActAmbientEncounters.map((ambientEncounter, index) => {
const ambientOffsetPx = SCENE_ACT_BACK_ROW_OFFSET_PX[index];
if (ambientOffsetPx === undefined) {
return null;
}
const ambientResolvedCharacter =
ambientEncounter.kind !== 'treasure' && ambientEncounter.characterId
? getCharacterById(ambientEncounter.characterId)
: null;
const ambientMonsterConfig =
!ambientResolvedCharacter &&
ambientEncounter.kind === 'npc' &&
ambientEncounter.monsterPresetId
? monsters.find(item => item.id === ambientEncounter.monsterPresetId) ?? null
: null;
const ambientHostileBottomOffsetPx = ambientMonsterConfig
? getHostileNpcSceneBottomOffsetPx(ambientMonsterConfig)
: getSceneNpcVisualBottomOffsetPx(ambientEncounter);
const ambientBottomOffsetPx = ambientResolvedCharacter
? getEncounterCharacterBottomOffsetPx(
stageLiftPx,
ambientEncounter,
ambientResolvedCharacter,
ambientOffsetPx,
)
: stageLiftPx + ambientHostileBottomOffsetPx + ambientOffsetPx;
const ambientFacing = getFacingTowardPlayer(
SCENE_ACT_BACK_ROW_ANCHOR_X_METERS,
playerX,
);
const ambientBottom = ambientEncounter.characterId
? getEncounterCharacterOpponentBottom(
groundBottom,
stageLiftPx,
ambientEncounter,
getCharacterById(ambientEncounter.characterId),
)
: `calc(${groundBottom} + ${stageLiftPx + ambientHostileBottomOffsetPx}px)`;
return (
<div
key={`scene-act-ambient-${ambientEncounter.id ?? ambientEncounter.npcName}-${index}`}
className="absolute"
style={{
left: getMonsterWorldLeft(
sideAnchor,
SCENE_ACT_BACK_ROW_ANCHOR_X_METERS,
cameraAnchorX,
monsterAnchorMeters,
),
bottom: addCssPxOffset(ambientBottom, ambientOffsetPx),
zIndex: getSceneEntityZIndex(ambientBottomOffsetPx),
transition: 'left 260ms linear, bottom 180ms ease',
}}
>
<SceneEntityButton
onClick={
ambientEncounter.kind === 'npc'
? () => onEntitySelect?.({kind: 'npc', encounter: ambientEncounter})
: null
}
ariaLabel={
ambientEncounter.kind === 'npc'
? `查看${ambientEncounter.npcName}详情`
: undefined
}
className="relative flex w-28 flex-col items-center"
>
<div className={ROLE_CHARACTER_FRAME_CLASS}>
{ambientResolvedCharacter &&
!ambientEncounter.visual &&
!ambientEncounter.imageSrc?.trim() ? (
<RoleCharacterSprite
state={AnimationState.IDLE}
character={ambientResolvedCharacter}
facing={ambientFacing}
/>
) : ambientMonsterConfig ? (
<HostileNpcAnimator
hostileNpc={ambientMonsterConfig}
animation="idle"
flip={ambientFacing === 'right'}
className="scale-[1.82] origin-bottom"
/>
) : (
<SceneEncounterNpcSprite
encounter={ambientEncounter}
state={AnimationState.IDLE}
facing={ambientFacing}
className="drop-shadow-[0_8px_14px_rgba(0,0,0,0.32)]"
/>
)}
</div>
{/* 幕后排角色只是同幕可见实体,不抢占当前交互目标。 */}
{npcAffinityEffect?.npcId ===
(ambientEncounter.id ?? ambientEncounter.npcName) ? (
<NpcAffinityEffectBadge effect={npcAffinityEffect} />
) : null}
</SceneEntityButton>
</div>
);
})}
</>
);
}