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.
This commit is contained in:
filemon
2026-03-20 09:47:11 -03:00
parent 14644d0cb3
commit 8097f0e5fb
3 changed files with 49 additions and 20 deletions
+10 -2
View File
@@ -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));
}
/**
+9 -9
View File
@@ -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]);
+30 -9
View File
@@ -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<string | null>(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