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:
+10
-2
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user