diff --git a/src/blobbi/rooms/components/BlobbiRoomHero.tsx b/src/blobbi/rooms/components/BlobbiRoomHero.tsx index c648b630..0ba88a72 100644 --- a/src/blobbi/rooms/components/BlobbiRoomHero.tsx +++ b/src/blobbi/rooms/components/BlobbiRoomHero.tsx @@ -13,8 +13,10 @@ import { } from 'lucide-react'; import { BlobbiStageVisual } from '@/blobbi/ui/BlobbiStageVisual'; -import { getVisibleStats, getStatStatus } from '@/blobbi/core/lib/blobbi-decay'; -import type { BlobbiCompanion } from '@/blobbi/core/lib/blobbi'; +import { getVisibleStats } from '@/blobbi/core/lib/blobbi-decay'; +import { getBlobbiStatDisplayState } from '@/blobbi/core/lib/blobbi-segments'; +import type { CareState } from '@/blobbi/core/lib/blobbi-segments'; +import type { BlobbiCompanion, BlobbiStats } from '@/blobbi/core/lib/blobbi'; import type { BlobbiEmotion } from '@/blobbi/ui/lib/emotion-types'; import type { BlobbiVisualRecipe } from '@/blobbi/ui/lib/recipe'; import type { BlobbiReactionState } from '@/blobbi/actions'; @@ -205,12 +207,16 @@ function StatsCrown({ heroWidth: number; }) { const allStats = useMemo(() => - getVisibleStats(companion.stage).map(stat => ({ - stat, - value: currentStats[stat] ?? 100, - status: getStatStatus(companion.stage, stat, currentStats[stat] ?? 100), - color: STAT_COLOR_MAP[stat], - })), + getVisibleStats(companion.stage).map(stat => { + const value = currentStats[stat] ?? 100; + const display = getBlobbiStatDisplayState({ stage: companion.stage, stat: stat as keyof BlobbiStats, value }); + return { + stat, + value, + careState: display.careState, + color: STAT_COLOR_MAP[stat], + }; + }), [companion.stage, currentStats]); if (allStats.length === 0) return null; @@ -244,7 +250,7 @@ function StatsCrown({ bottom: `${y.toFixed(1)}px`, }} > - + ); })} @@ -258,15 +264,17 @@ function StatIndicator({ stat, value, color, - status = 'normal', + careState = 'good', }: { stat: string; value: number | undefined; color: 'orange' | 'yellow' | 'green' | 'blue' | 'violet'; - status?: 'normal' | 'warning' | 'critical'; + careState?: CareState; }) { const displayValue = value ?? 0; - const isLow = status === 'warning' || status === 'critical'; + const showBadge = careState === 'attention' || careState === 'urgent'; + const showPulse = careState === 'urgent'; + const badgeColor = careState === 'urgent' ? 'text-red-500' : 'text-amber-500'; const ringHex = STAT_RING_HEX[color]; const IconComponent = STAT_ICON_MAP[stat]; @@ -274,7 +282,7 @@ function StatIndicator({
@@ -287,9 +295,9 @@ function StatIndicator({
{IconComponent && } - {isLow && ( + {showBadge && ( )} diff --git a/src/blobbi/ui/StatIndicator.tsx b/src/blobbi/ui/StatIndicator.tsx index c6518740..0973bbc2 100644 --- a/src/blobbi/ui/StatIndicator.tsx +++ b/src/blobbi/ui/StatIndicator.tsx @@ -1,5 +1,6 @@ import { AlertTriangle, Utensils, Gamepad2, Heart, Droplets, Zap } from 'lucide-react'; import { cn } from '@/lib/utils'; +import type { CareState } from '@/blobbi/core/lib/blobbi-segments'; // ── Constants ───────────────────────────────────────────────────────────────── @@ -61,7 +62,10 @@ export interface StatIndicatorProps { stat: string; value: number | undefined; color: 'orange' | 'yellow' | 'green' | 'blue' | 'violet'; + /** @deprecated Prefer `careState` from blobbi-segments. Kept for unmigrated callers. */ status?: 'normal' | 'warning' | 'critical'; + /** Segment-model care state. When provided, takes precedence over `status`. */ + careState?: CareState; /** Visual size preset. Default: 'md'. */ size?: 'sm' | 'md'; /** When provided, renders as a clickable button. */ @@ -75,12 +79,25 @@ export function StatIndicator({ value, color, status = 'normal', + careState, size = 'md', onClick, disabled, }: StatIndicatorProps) { const displayValue = value ?? 0; - const isLow = status === 'warning' || status === 'critical'; + + // When careState is provided (new segment model), derive badge/pulse from it. + // Otherwise fall back to old status-based behaviour for unmigrated callers. + const showBadge = careState + ? (careState === 'attention' || careState === 'urgent') + : (status === 'warning' || status === 'critical'); + const showPulse = careState + ? careState === 'urgent' + : status === 'critical'; + const badgeColor = careState + ? (careState === 'urgent' ? 'text-red-500' : 'text-amber-500') + : (status === 'critical' ? 'text-red-500' : 'text-amber-500'); + const ringHex = STAT_RING_HEX[color]; const IconComponent = STAT_ICON_MAP[stat]; const preset = SIZE_PRESETS[size]; @@ -100,9 +117,9 @@ export function StatIndicator({ {/* Icon with warning badge */}
{IconComponent && } - {isLow && ( + {showBadge && ( )} @@ -114,7 +131,7 @@ export function StatIndicator({ 'relative rounded-full flex items-center justify-center', preset.container, STAT_BG_COLORS[color], - status === 'critical' && 'animate-pulse', + showPulse && 'animate-pulse', ); if (onClick) { diff --git a/src/components/widgets/BlobbiWidget.tsx b/src/components/widgets/BlobbiWidget.tsx index 3eb8c2d0..8f2a26e1 100644 --- a/src/components/widgets/BlobbiWidget.tsx +++ b/src/components/widgets/BlobbiWidget.tsx @@ -12,7 +12,8 @@ import { useBlobbiCompanionData } from '@/blobbi/companion/hooks/useBlobbiCompan import { useBlobbiMigration } from '@/blobbi/core/hooks/useBlobbiMigration'; import { useBlobbiUseInventoryItem } from '@/blobbi/actions/hooks/useBlobbiUseInventoryItem'; import { isActionVisibleForStage, type InventoryAction, type BlobbiAction } from '@/blobbi/actions/lib/blobbi-action-utils'; -import { getVisibleStats, getStatStatus } from '@/blobbi/core/lib/blobbi-decay'; +import { getVisibleStats } from '@/blobbi/core/lib/blobbi-decay'; +import { getBlobbiStatDisplayState } from '@/blobbi/core/lib/blobbi-segments'; import { KIND_BLOBBI_STATE, KIND_BLOBBONAUT_PROFILE, updateBlobbiTags, updateBlobbonautTags, filterMigratedLegacyCompanions } from '@/blobbi/core/lib/blobbi'; import { applyBlobbiDecay } from '@/blobbi/core/lib/blobbi-decay'; import { getStreakTagUpdates } from '@/blobbi/actions/lib/blobbi-streak'; @@ -364,7 +365,7 @@ function BlobbiWidgetContent({ companion, onUseItem, onRest, isActionPending, is stat={stat} value={stats[stat]} color={STAT_COLOR_MAP[stat]} - status={getStatStatus(stage, stat, stats[stat] ?? 100)} + careState={getBlobbiStatDisplayState({ stage, stat, value: stats[stat] ?? 100 }).careState} size="sm" onClick={() => handleStatClick(stat)} disabled={isActionPending} diff --git a/src/pages/BlobbiPage.tsx b/src/pages/BlobbiPage.tsx index ecf0f682..d18d5de3 100644 --- a/src/pages/BlobbiPage.tsx +++ b/src/pages/BlobbiPage.tsx @@ -48,6 +48,7 @@ import { import { useSeedIdentitySync } from '@/blobbi/core/hooks/useSeedIdentitySync'; import { applyBlobbiDecay } from '@/blobbi/core/lib/blobbi-decay'; +import { getBlobbiStatDisplayState } from '@/blobbi/core/lib/blobbi-segments'; import { getLiveShopItems } from '@/blobbi/shop/lib/blobbi-shop-items'; @@ -121,21 +122,31 @@ function getSelectedBlobbiKey(pubkey: string): string { /** Enable debug logging in development only */ const DEBUG_BLOBBI = import.meta.env.DEV; -/** Stat threshold below which a Blobbi is considered to need care */ -const CARE_THRESHOLD = 40; +/** @deprecated Kept for reference — care badge now uses segment model. */ +const _CARE_THRESHOLD = 40; +void _CARE_THRESHOLD; + +/** Stat keys checked for the companion selector care badge (excludes energy). */ +const CARE_BADGE_STATS = ['hunger', 'happiness', 'hygiene', 'health'] as const; /** - * Check if a companion needs care based on stat thresholds. - * A Blobbi needs care if any stat is below CARE_THRESHOLD. + * Check if a companion needs care using the segment display model. + * + * Shows a care badge when: + * - any stat is `urgent`, OR + * - two or more stats are `attention`. + * + * Eggs always return `protected` from the helper, so they never show a badge. */ function companionNeedsCare(companion: BlobbiCompanion): boolean { - const { stats } = companion; - return ( - (stats.hunger !== undefined && stats.hunger < CARE_THRESHOLD) || - (stats.happiness !== undefined && stats.happiness < CARE_THRESHOLD) || - (stats.hygiene !== undefined && stats.hygiene < CARE_THRESHOLD) || - (stats.health !== undefined && stats.health < CARE_THRESHOLD) - ); + let attentionCount = 0; + for (const stat of CARE_BADGE_STATS) { + const value = companion.stats[stat] ?? 100; + const { careState } = getBlobbiStatDisplayState({ stage: companion.stage, stat, value }); + if (careState === 'urgent') return true; + if (careState === 'attention') attentionCount++; + } + return attentionCount >= 2; }