From 8097f0e5fb265f2c06a18ee4e46692ea3a5e7e63 Mon Sep 17 00:00:00 2001 From: filemon Date: Fri, 20 Mar 2026 09:47:11 -0300 Subject: [PATCH] fix(blobbi): fix selection reset bug, stat safety, and migration visual traits - Remove auto-save effect that was overwriting user selection during WebSocket/query updates. User selection now only persists via explicit handleSelectBlobbi() call. - Add debug logging to trace selection changes in development mode. - Add STAT_MIN=1 and STAT_MAX=100 constants to blobbi-decay.ts and update clamp() to use STAT_MIN instead of 0, preventing soft-lock issues. - Fix buildMigrationTags() to always derive and include all visual traits during migration, ensuring every migrated event has complete visual data. --- src/lib/blobbi-decay.ts | 12 ++++++++++-- src/lib/blobbi.ts | 18 +++++++++--------- src/pages/BlobbiPage.tsx | 39 ++++++++++++++++++++++++++++++--------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/lib/blobbi-decay.ts b/src/lib/blobbi-decay.ts index 4029d948..0266a1bc 100644 --- a/src/lib/blobbi-decay.ts +++ b/src/lib/blobbi-decay.ts @@ -187,13 +187,21 @@ export const CRITICAL_THRESHOLDS = { }, } as const; +// ─── Constants: Stat Bounds ─────────────────────────────────────────────────── + +/** Minimum stat value - stats can never go below this */ +export const STAT_MIN = 1; +/** Maximum stat value - stats can never exceed this */ +export const STAT_MAX = 100; + // ─── Helper Functions ───────────────────────────────────────────────────────── /** - * Clamp a value to the 0-100 range. + * Clamp a value to the STAT_MIN-STAT_MAX range (1-100). + * Stats can never reach true zero - minimum is always 1. */ function clamp(value: number): number { - return Math.max(0, Math.min(100, value)); + return Math.max(STAT_MIN, Math.min(STAT_MAX, value)); } /** diff --git a/src/lib/blobbi.ts b/src/lib/blobbi.ts index 6d44d1c8..bc79a375 100644 --- a/src/lib/blobbi.ts +++ b/src/lib/blobbi.ts @@ -1340,15 +1340,15 @@ export function buildMigrationTags( } } - // EXPLICITLY preserve visual trait tags for backwards compatibility - // These are TRANSITIONAL - seed is the future source of truth - // Do not overwrite if they exist in the legacy event - for (const visualTag of VISUAL_TRAIT_TAG_NAMES) { - const value = getTagValue(legacyTags, visualTag); - if (value !== undefined) { - newTags.push([visualTag, value]); - } - } + // ALWAYS include visual trait tags - derived from seed, with legacy tag fallbacks + // This ensures every migrated event has complete visual traits for consistent rendering + const visualTraits = deriveVisualTraits(legacyTags, seed); + newTags.push(['base_color', visualTraits.baseColor]); + newTags.push(['secondary_color', visualTraits.secondaryColor]); + newTags.push(['eye_color', visualTraits.eyeColor]); + newTags.push(['pattern', visualTraits.pattern]); + newTags.push(['special_mark', visualTraits.specialMark]); + newTags.push(['size', visualTraits.size]); // Update timestamps newTags.push(['last_interaction', now]); diff --git a/src/pages/BlobbiPage.tsx b/src/pages/BlobbiPage.tsx index c8ba4cf5..e7645768 100644 --- a/src/pages/BlobbiPage.tsx +++ b/src/pages/BlobbiPage.tsx @@ -176,34 +176,51 @@ function BlobbiContent() { const [showAdoptionFlow, setShowAdoptionFlow] = useState(false); // STEP 6: Selection Priority - // 1) localStorage selection (if valid and exists in collection) - // 2) first item from profile.has that exists in companionsByD + // 1) localStorage selection (if valid and exists in collection) - USER SELECTION ALWAYS WINS + // 2) first item from profile.has that exists in companionsByD - DEFAULT ONLY, never persisted // 3) undefined (show selector) + // + // CRITICAL: Default selection must NEVER overwrite localStorage. + // User selection persists only via handleSelectBlobbi, not via this computed value. const selectedD = useMemo(() => { if (!profile) return undefined; // Priority 1: localStorage selection (if it exists in loaded collection) + // USER SELECTION ALWAYS WINS - this is the authoritative source if (storedSelectedD && companionsByD[storedSelectedD]) { + if (DEBUG_BLOBBI) { + console.log('[BlobbiPage] selectedD: using localStorage selection:', storedSelectedD); + } return storedSelectedD; } // Priority 2: First item from profile.has that exists in companionsByD + // This is a DEFAULT - it should NOT be persisted to localStorage for (const d of profile.has) { if (companionsByD[d]) { + if (DEBUG_BLOBBI) { + console.log('[BlobbiPage] selectedD: using default from profile.has:', d, + '(storedSelectedD was:', storedSelectedD, + storedSelectedD ? (companionsByD[storedSelectedD] ? 'exists' : 'NOT in companionsByD') : 'null', ')'); + } return d; } } // Priority 3: No valid selection + if (DEBUG_BLOBBI) { + console.log('[BlobbiPage] selectedD: no valid selection available'); + } return undefined; }, [profile, storedSelectedD, companionsByD]); - // Auto-save selection to localStorage when it changes - useEffect(() => { - if (selectedD && selectedD !== storedSelectedD) { - setStoredSelectedD(selectedD); - } - }, [selectedD, storedSelectedD, setStoredSelectedD]); + // NOTE: We intentionally do NOT auto-save the computed selectedD to localStorage. + // This prevents the default selection from overwriting user selections during: + // - WebSocket updates + // - Query refetches + // - Race conditions where storedSelectedD is not yet in companionsByD + // + // User selections are only persisted via handleSelectBlobbi (line ~232). // Get the selected companion from the collection const companion = selectedD ? companionsByD[selectedD] ?? null : null; @@ -229,10 +246,14 @@ function BlobbiContent() { const [actionInProgress, setActionInProgress] = useState(null); // Handler for selecting a Blobbi from the selector + // This is the ONLY place where user selection is persisted to localStorage const handleSelectBlobbi = useCallback((d: string) => { + if (DEBUG_BLOBBI) { + console.log('[BlobbiPage] handleSelectBlobbi: user selected:', d, '(previous storedSelectedD was:', storedSelectedD, ')'); + } setStoredSelectedD(d); setShowSelector(false); - }, [setStoredSelectedD]); + }, [setStoredSelectedD, storedSelectedD]); // ─── Helper: Ensure Canonical Before Action ─── // Centralized migration helper that auto-migrates legacy pets before any action