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:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user