Make all stat icons trigger the guided-care flow

Previously only low-status (warning/critical) stat icons were clickable
and the guide auto-cleared when the stat recovered to normal. Now every
stat icon fires onGuide on tap regardless of status, and the guide is
only dismissed by completion events (item used, sleep started, or a
different stat clicked).

Changes:
- StatsCrown: remove the status-gate from onClick and cursor-pointer
- StatsCrown: add z-10 so stat icons layer above the Blobbi visual
- Blobbi animation wrapper: pointer-events-none so it cannot intercept
  taps on overlapping stat icons
- BlobbiDashboard: remove the effect that cleared the guide when the
  stat was normal; drop unused getStatStatus import
- Update comments and prop docs to reflect the new behaviour
This commit is contained in:
filemon
2026-04-24 10:54:43 -03:00
parent ca260497cc
commit 1eb126bdf8
4 changed files with 22 additions and 44 deletions
@@ -79,7 +79,7 @@ export interface BlobbiRoomHeroProps {
roomId: BlobbiRoomId;
/** Room order for dot indicators */
roomOrder?: BlobbiRoomId[];
/** Called when the user taps a low-status stat icon to start the guide. */
/** Called when the user taps any stat icon to start the guide. */
onGuide?: (stat: keyof BlobbiStats) => void;
className?: string;
}
@@ -144,12 +144,12 @@ export function BlobbiRoomHero({
<StatsCrown companion={companion} currentStats={currentStats} heroWidth={heroWidth} onGuide={onGuide} />
<div
className="relative transition-all duration-500"
className="relative transition-all duration-500 pointer-events-none"
style={!isSleeping ? {
animation: `blobbi-bob ${4 - (currentStats.happiness / 100) * 1.5}s ease-in-out infinite, blobbi-sway ${6 - (currentStats.happiness / 100) * 2}s ease-in-out infinite`,
} : undefined}
>
<div className="absolute inset-0 -m-16 sm:-m-20 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
<div className="absolute inset-0 -m-16 sm:-m-20 bg-primary/5 rounded-full blur-3xl" />
<BlobbiStageVisual
companion={companion}
size="lg"
@@ -232,7 +232,7 @@ function StatsCrown({
: allStats.map((_, i) => -arcHalf + (arcSpread / (count - 1)) * i);
return (
<div className="relative flex items-end justify-center w-full mb-4 sm:mb-8" style={{ height: 40, animation: 'stat-glow-clock 2s linear infinite' }}>
<div className="relative z-10 flex items-end justify-center w-full mb-4 sm:mb-8" style={{ height: 40, animation: 'stat-glow-clock 2s linear infinite' }}>
{allStats.map((s, i) => {
const angleDeg = angles[i];
const angleRad = (angleDeg * Math.PI) / 180;
@@ -243,13 +243,13 @@ function StatsCrown({
return (
<div
key={s.stat}
className={cn('absolute transition-all duration-500', s.status !== 'normal' && onGuide && 'cursor-pointer')}
className={cn('absolute transition-all duration-500', onGuide && 'cursor-pointer')}
style={{
transform: 'translate(-50%, 0)',
left: `calc(50% + ${x.toFixed(1)}px)`,
bottom: `${y.toFixed(1)}px`,
}}
onClick={s.status !== 'normal' && onGuide ? () => onGuide(s.stat as keyof BlobbiStats) : undefined}
onClick={onGuide ? () => onGuide(s.stat as keyof BlobbiStats) : undefined}
>
<StatIndicator stat={s.stat} value={s.value} color={s.color} status={s.status} />
</div>
+2 -11
View File
@@ -1,5 +1,5 @@
/**
* Stat Guide Configuration — mappings and helpers for the low-status guided UX.
* Stat Guide Configuration — mappings and helpers for the stat-guided UX.
*
* Centralises:
* stat → target room
@@ -57,15 +57,6 @@ export const STAT_GUIDE_ACTION: Partial<Record<keyof BlobbiStats, string>> = {
energy: 'sleep',
};
/** Contextual help text shown in the stat popover. */
export const STAT_HELP_TEXT: Record<keyof BlobbiStats, { title: string; description: string }> = {
hunger: { title: 'Blobbi is hungry!', description: 'Head to the Kitchen and pick some food.' },
happiness: { title: 'Blobbi is feeling sad…', description: 'Go Home and play with a toy!' },
health: { title: 'Blobbi needs care!', description: 'Visit the Care Room for medicine.' },
hygiene: { title: 'Blobbi needs a bath!', description: 'Go to the Care Room for hygiene items.' },
energy: { title: 'Blobbi is tired!', description: 'Head to the Bedroom and put Blobbi to sleep.' },
};
// ─── Room carousel item constraints ───────────────────────────────────────────
/**
@@ -117,7 +108,7 @@ export function findGuideItemForStat(stat: keyof BlobbiStats): string | null {
// ─── Guide target builder ─────────────────────────────────────────────────────
/**
* Build a `GuideTarget` for a low stat, automatically resolving the correct
* Build a `GuideTarget` for a stat, automatically resolving the correct
* room, target type, item/action, and initial step.
*
* `currentRoom` determines whether the guide starts at the 'room' step
+2 -2
View File
@@ -745,8 +745,8 @@
}
/* Guide-target glow — quick blink + pause for nav arrows & carousel.
1.4 s cycle, linear: rise (0→8%), brief peak (8→18%), fall (18→28%),
idle (28→100% ≈ 1 s pause). */
Applied at 1.1s linear: rise (0→8%), brief peak (8→18%), fall (18→28%),
idle (28→100% ≈ 0.8 s pause). */
@keyframes guide-glow-slow {
0%, 28%, 100% { box-shadow: 0 0 0 0 transparent; }
8%, 18% { box-shadow: 0 0 4px 1.5px currentColor; }
+12 -25
View File
@@ -45,8 +45,7 @@ import {
type StorageItem,
} from '@/blobbi/core/lib/blobbi';
import { applyBlobbiDecay, getStatStatus } from '@/blobbi/core/lib/blobbi-decay';
import { calculateProjectedDecay } from '@/blobbi/core/hooks/useProjectedBlobbiState';
import { applyBlobbiDecay } from '@/blobbi/core/lib/blobbi-decay';
import type { BlobbiStats } from '@/blobbi/core/types/blobbi';
import { useSeedIdentitySync } from '@/blobbi/core/hooks/useSeedIdentitySync';
@@ -128,17 +127,16 @@ const DEBUG_BLOBBI = import.meta.env.DEV;
const CARE_THRESHOLD = 40;
/**
* Check if a companion needs care based on projected (decay-applied) stats.
* Uses the same projection as the main UI so the badge is consistent with
* what the user sees on the Blobbi page.
* Check if a companion needs care based on stat thresholds.
* A Blobbi needs care if any stat is below CARE_THRESHOLD.
*/
function companionNeedsCare(companion: BlobbiCompanion): boolean {
const { stats } = calculateProjectedDecay(companion);
const { stats } = companion;
return (
stats.hunger < CARE_THRESHOLD ||
stats.happiness < CARE_THRESHOLD ||
stats.hygiene < CARE_THRESHOLD ||
stats.health < CARE_THRESHOLD
(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)
);
}
@@ -944,7 +942,7 @@ function BlobbiDashboard({
}
}, [isSleeping]);
// ─── Low-Status Guide Flow ───
// ─── Stat Guide Flow ───
const [guideTarget, setGuideTarget] = useState<GuideTarget | null>(null);
// Start a guide: build the target and set state
@@ -972,11 +970,6 @@ function BlobbiDashboard({
// Derived: action glow (only when in correct room + on 'action' step)
const guideActionGlow = guideTarget?.step === 'action' ? guideTarget.targetAction : null;
// Wrap onRest — guide cleanup is handled by the isSleeping effect below
const handleRestWithGuide = useCallback(() => {
onRest();
}, [onRest]);
// Toggle drawer: tapping same tab closes it, tapping another opens that one
const toggleDrawer = useCallback((drawer: DashboardDrawer) => {
setActiveDrawer(prev => prev === drawer ? 'none' : drawer);
@@ -1013,14 +1006,8 @@ function BlobbiDashboard({
}
}, [isSleeping, guideTarget]);
// Clear guide when the guided stat recovers above warning threshold
useEffect(() => {
if (!guideTarget || !projectedState) return;
const val = projectedState.stats[guideTarget.stat] ?? 100;
if (getStatStatus(companion.stage, guideTarget.stat, val) === 'normal') {
setGuideTarget(null);
}
}, [guideTarget, projectedState, companion.stage]);
// Guide is cleared by completion events (item used, sleep started,
// or a different stat clicked), not by the stat recovering to normal.
// Measure hero container width for responsive stat arc radius
const heroRef = useRef<HTMLDivElement>(null);
@@ -1602,7 +1589,7 @@ function BlobbiDashboard({
handleUseItemFromTab={handleUseItemFromTab}
handleDirectAction={handleDirectAction}
onUseItem={onUseItem}
onRest={handleRestWithGuide}
onRest={onRest}
setShowPhotoModal={setShowPhotoModal}
poopStateRef={poopStateRef}
guideHighlightId={guideHighlightId}