From 62cc2611ea2347e3c880fcae32183c8a468f4145 Mon Sep 17 00:00:00 2001 From: filemon Date: Sun, 3 May 2026 02:10:28 -0300 Subject: [PATCH] Fix dizzy-eye SMIL restart during nausea recovery drain Replace blobbi reference equality with stable visual-identity primitive comparisons in MemoizedBlobbiVisual memo and SVG renderer useMemo deps. This prevents SVG DOM rebuilds (which restart SMIL animateTransform) when the upstream companion object gets a new reference during the imperative gradient-drain loop. --- .../companion/components/BlobbiCompanionVisual.tsx | 11 ++++++++++- src/blobbi/ui/BlobbiAdultSvgRenderer.tsx | 9 +++++---- src/blobbi/ui/BlobbiBabySvgRenderer.tsx | 6 +++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/blobbi/companion/components/BlobbiCompanionVisual.tsx b/src/blobbi/companion/components/BlobbiCompanionVisual.tsx index 1c02d652..28fb9bc3 100644 --- a/src/blobbi/companion/components/BlobbiCompanionVisual.tsx +++ b/src/blobbi/companion/components/BlobbiCompanionVisual.tsx @@ -123,7 +123,16 @@ const MemoizedBlobbiVisual = memo(function MemoizedBlobbiVisual({ }, (prev, next) => { return ( prev.stage === next.stage && - prev.blobbi === next.blobbi && + // Compare blobbi by visual-identity primitives, NOT by reference. + // This prevents SVG rebuilds (and SMIL animation restarts) when the + // upstream companion object gets a new reference with identical content + // — e.g. during nausea recovery where only angerRise.level changes. + prev.blobbi.id === next.blobbi.id && + prev.blobbi.baseColor === next.blobbi.baseColor && + prev.blobbi.secondaryColor === next.blobbi.secondaryColor && + prev.blobbi.eyeColor === next.blobbi.eyeColor && + prev.blobbi.adult?.evolutionForm === next.blobbi.adult?.evolutionForm && + prev.blobbi.seed === next.blobbi.seed && prev.recipeFingerprint === next.recipeFingerprint && prev.recipeLabel === next.recipeLabel && prev.emotion === next.emotion && diff --git a/src/blobbi/ui/BlobbiAdultSvgRenderer.tsx b/src/blobbi/ui/BlobbiAdultSvgRenderer.tsx index 9b5466ca..12e22a2f 100644 --- a/src/blobbi/ui/BlobbiAdultSvgRenderer.tsx +++ b/src/blobbi/ui/BlobbiAdultSvgRenderer.tsx @@ -94,11 +94,12 @@ export function BlobbiAdultSvgRenderer({ } return animatedSvg; - // recipeFingerprint replaces recipeProp in the dep list so that - // level-only changes do NOT trigger a full SVG rebuild. The closure - // captures the current recipeProp for the rare structural rebuilds. + // Deps use stable primitives from blobbi (not the object reference) and + // recipeFingerprint (not recipeProp) so that level-only changes and + // upstream reference churn do NOT trigger full SVG rebuilds. The closure + // captures the current blobbi/recipeProp for the rare structural rebuilds. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blobbi, recipeFingerprint, recipeLabel, emotion, bodyEffects]); + }, [blobbi.id, blobbi.baseColor, blobbi.secondaryColor, blobbi.eyeColor, blobbi.adult?.evolutionForm, blobbi.seed, recipeFingerprint, recipeLabel, emotion, bodyEffects]); const safeSvg = useMemo(() => sanitizeBlobbiSvg(customizedSvg), [customizedSvg]); diff --git a/src/blobbi/ui/BlobbiBabySvgRenderer.tsx b/src/blobbi/ui/BlobbiBabySvgRenderer.tsx index e8438d21..ba5f96c1 100644 --- a/src/blobbi/ui/BlobbiBabySvgRenderer.tsx +++ b/src/blobbi/ui/BlobbiBabySvgRenderer.tsx @@ -90,8 +90,12 @@ export function BlobbiBabySvgRenderer({ } return animatedSvg; + // Deps use stable primitives from blobbi (not the object reference) and + // recipeFingerprint (not recipeProp) so that level-only changes and + // upstream reference churn do NOT trigger full SVG rebuilds. The closure + // captures the current blobbi/recipeProp for the rare structural rebuilds. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blobbi, recipeFingerprint, recipeLabel, emotion, bodyEffects]); + }, [blobbi.id, blobbi.baseColor, blobbi.secondaryColor, blobbi.eyeColor, blobbi.seed, recipeFingerprint, recipeLabel, emotion, bodyEffects]); const safeSvg = useMemo(() => sanitizeBlobbiSvg(customizedSvg), [customizedSvg]);