This commit is contained in:
2026-04-27 22:50:18 +08:00
parent ded6f6ee2a
commit b6c6640548
77 changed files with 5240 additions and 833 deletions

View File

@@ -27,6 +27,7 @@ import {
DialogueBubbleIcon,
type GameCanvasEntitySelection,
GENERIC_NPC_SCENE_SCALE,
getBattleCompanionSlotOffset,
getCompanionSlotOffset,
getEncounterCharacterBottomOffsetPx,
getEncounterCharacterOpponentBottom,
@@ -222,11 +223,23 @@ export function GameCanvasEntityLayer({
const [combatFeedbackEvents, setCombatFeedbackEvents] = useState<CombatFeedbackEvent[]>([]);
const previousCombatSamplesRef = useRef<Map<string, CombatFeedbackHealthSample> | null>(null);
const combatFeedbackSequenceRef = useRef(0);
const hasCombatAfterimage = useMemo(
() =>
combatFeedbackEvents.length > 0 ||
sceneCombatants.some(
(hostileNpc) =>
hostileNpc.hp < hostileNpc.maxHp ||
hostileNpc.animation === 'attack' ||
hostileNpc.animation === 'die',
),
[combatFeedbackEvents.length, sceneCombatants],
);
const shouldRenderCombatPresentation = inBattle || hasCombatAfterimage;
const shouldRenderPeacefulEncounter =
Boolean(encounter) && (!inBattle || sceneCombatants.length === 0);
const combatHealthSamples = useMemo<CombatFeedbackHealthSample[]>(
() => {
if (!inBattle) return [];
if (!shouldRenderCombatPresentation) return [];
return [
{key: 'player', kind: 'player', hp: playerHp},
@@ -242,7 +255,7 @@ export function GameCanvasEntityLayer({
})),
];
},
[companions, inBattle, playerHp, sceneCombatants],
[companions, playerHp, sceneCombatants, shouldRenderCombatPresentation],
);
const combatFeedbackByTarget = useMemo(() => {
const feedbackByTarget = new Map<string, CombatFeedbackEvent[]>();
@@ -259,7 +272,7 @@ export function GameCanvasEntityLayer({
};
useEffect(() => {
if (!inBattle) {
if (!shouldRenderCombatPresentation) {
previousCombatSamplesRef.current = null;
setCombatFeedbackEvents([]);
return;
@@ -283,12 +296,14 @@ export function GameCanvasEntityLayer({
previousCombatSamplesRef.current = new Map(
combatHealthSamples.map(sample => [sample.key, sample]),
);
}, [combatHealthSamples, inBattle]);
}, [combatHealthSamples, shouldRenderCombatPresentation]);
return (
<>
{companions.map(companion => {
const slotOffset = getCompanionSlotOffset(companion.slot);
const slotOffset = inBattle
? getBattleCompanionSlotOffset(companion.slot)
: getCompanionSlotOffset(companion.slot);
const feedbackTargetKey = `companion:${companion.npcId}`;
const feedbackEvents = combatFeedbackByTarget.get(feedbackTargetKey) ?? [];
const companionFacing = companion.facing ?? 'right';
@@ -314,7 +329,7 @@ export function GameCanvasEntityLayer({
style={{
left: companionAnchorLeft,
bottom: companionAnchorBottom,
zIndex: getSceneEntityZIndex(playerBottomOffsetPx + slotOffset.bottom),
zIndex: getSceneEntityZIndex(playerBottomOffsetPx + slotOffset.bottom) + (inBattle ? 1 : 0),
transition: 'left 260ms linear, bottom 180ms ease',
}}
>
@@ -336,7 +351,7 @@ export function GameCanvasEntityLayer({
className="relative flex w-28 flex-col items-center"
>
<CombatFeedbackNumbers events={feedbackEvents} onDone={removeCombatFeedbackEvent} />
{inBattle && (
{shouldRenderCombatPresentation && (
<div
className="absolute left-1/2 -translate-x-1/2"
style={{top: `${CHARACTER_COMBAT_HP_TOP_PX}px`}}
@@ -385,7 +400,7 @@ export function GameCanvasEntityLayer({
events={combatFeedbackByTarget.get('player') ?? []}
onDone={removeCombatFeedbackEvent}
/>
{inBattle && (
{shouldRenderCombatPresentation && (
<div
className="absolute left-1/2 -translate-x-1/2"
style={{top: `${CHARACTER_COMBAT_HP_TOP_PX}px`}}
@@ -484,7 +499,7 @@ export function GameCanvasEntityLayer({
className="relative flex w-28 flex-col items-center"
>
<CombatFeedbackNumbers events={feedbackEvents} onDone={removeCombatFeedbackEvent} />
{inBattle && (
{shouldRenderCombatPresentation && (
<div
className="absolute left-1/2 -translate-x-1/2"
style={{top: `${npcCombatHpTop}px`}}