Wire segment display model into stat indicators and care badge
Replace scattered warning/critical status checks with careState from getBlobbiStatDisplayState in the three UI consumers: - StatIndicator (shared): new careState prop takes precedence over deprecated status prop; badge shown for attention/urgent, pulse for urgent only. - BlobbiRoomHero (inline indicator): same careState-driven logic. - BlobbiWidget: passes careState instead of getStatStatus result. - BlobbiPage companion selector: care badge now shows when any stat is urgent OR two-plus stats are attention (was: any stat < 40). Eggs are always protected so they never trigger the badge. Old threshold constants and getStatStatus are kept — no deletions. No changes to decay rates, sleep, items, status-reactions, needDetection, SVG ring rendering, or Nostr persistence.
This commit is contained in:
@@ -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`,
|
||||
}}
|
||||
>
|
||||
<StatIndicator stat={s.stat} value={s.value} color={s.color} status={s.status} />
|
||||
<StatIndicator stat={s.stat} value={s.value} color={s.color} careState={s.careState} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -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({
|
||||
<div className={cn(
|
||||
'relative size-14 sm:size-[4.5rem] rounded-full flex items-center justify-center',
|
||||
STAT_BG_COLORS[color],
|
||||
status === 'critical' && 'animate-pulse',
|
||||
showPulse && 'animate-pulse',
|
||||
)}>
|
||||
<svg className="absolute inset-0 -rotate-90" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" strokeWidth="2.5" className="text-muted/15" />
|
||||
@@ -287,9 +295,9 @@ function StatIndicator({
|
||||
</svg>
|
||||
<div className="relative">
|
||||
{IconComponent && <IconComponent className={cn('size-5 sm:size-6', STAT_COLORS[color])} strokeWidth={2.5} />}
|
||||
{isLow && (
|
||||
{showBadge && (
|
||||
<AlertTriangle
|
||||
className={cn('absolute -top-1.5 -right-2 size-3', status === 'critical' ? 'text-red-500' : 'text-amber-500')}
|
||||
className={cn('absolute -top-1.5 -right-2 size-3', badgeColor)}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 */}
|
||||
<div className="relative">
|
||||
{IconComponent && <IconComponent className={cn(preset.icon, STAT_COLORS[color])} strokeWidth={2.5} />}
|
||||
{isLow && (
|
||||
{showBadge && (
|
||||
<AlertTriangle
|
||||
className={cn('absolute', preset.alertPos, preset.alertSize, status === 'critical' ? 'text-red-500' : 'text-amber-500')}
|
||||
className={cn('absolute', preset.alertPos, preset.alertSize, badgeColor)}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
)}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
+22
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user