From 06f820d355ef47b162ee9e22c74e34f63fcb464f Mon Sep 17 00:00:00 2001 From: filemon Date: Thu, 19 Mar 2026 09:04:08 -0300 Subject: [PATCH] Refine Blobbi incubation/hatch task system - Fix Color Moment URL: espy.social -> espy.you - Improve shape-change task detection: only counts true post-start changes - Remove duplicate Start Incubation button, integrate into evolve/hatch button - Extract shared incrementInteractionTaskTags helper for code reuse - Update floating controls to show incubation action for eggs not yet incubating The evolve/hatch button now serves as the single entry point: - Egg (not incubating): Opens incubation dialog - Egg (incubating): Button hidden, hatch action in HatchTasksPanel - Baby: Evolves to adult --- .../actions/hooks/useBlobbiDirectAction.ts | 30 ++-------- .../hooks/useBlobbiUseInventoryItem.ts | 30 ++-------- src/blobbi/actions/hooks/useHatchTasks.ts | 18 +++++- src/blobbi/actions/index.ts | 2 + src/blobbi/actions/lib/blobbi-action-utils.ts | 59 ++++++++++++++++++ src/pages/BlobbiPage.tsx | 60 ++++++++++--------- 6 files changed, 115 insertions(+), 84 deletions(-) diff --git a/src/blobbi/actions/hooks/useBlobbiDirectAction.ts b/src/blobbi/actions/hooks/useBlobbiDirectAction.ts index d8730b36..09892bd0 100644 --- a/src/blobbi/actions/hooks/useBlobbiDirectAction.ts +++ b/src/blobbi/actions/hooks/useBlobbiDirectAction.ts @@ -16,6 +16,7 @@ import { clampStat, applyStat, DIRECT_ACTION_METADATA, + incrementInteractionTaskTags, type DirectAction, } from '../lib/blobbi-action-utils'; @@ -144,32 +145,9 @@ export function useBlobbiDirectAction({ const nowStr = now.toString(); // If incubating, increment the interaction counter for hatch tasks - let updatedTags = canonical.allTags; - if (canonical.companion.state === 'incubating') { - // Get current interaction count from task tags - const interactionTag = canonical.allTags.find(tag => - tag[0] === 'task' && tag[1]?.startsWith('interactions:') - ); - const currentCount = interactionTag - ? parseInt(interactionTag[1].split(':')[1] || '0', 10) - : 0; - const newCount = currentCount + 1; - - // Remove old interaction task tag and add new one - updatedTags = canonical.allTags.filter(tag => - !(tag[0] === 'task' && tag[1]?.startsWith('interactions:')) - ); - updatedTags = [...updatedTags, ['task', `interactions:${newCount}`]]; - - // Mark as completed if reached 7 - if (newCount >= 7) { - // Remove any existing task_completed for interactions - updatedTags = updatedTags.filter(tag => - !(tag[0] === 'task_completed' && tag[1] === 'interactions') - ); - updatedTags = [...updatedTags, ['task_completed', 'interactions']]; - } - } + const updatedTags = canonical.companion.state === 'incubating' + ? incrementInteractionTaskTags(canonical.allTags).updatedTags + : canonical.allTags; const blobbiTags = updateBlobbiTags(updatedTags, { ...statsUpdate, diff --git a/src/blobbi/actions/hooks/useBlobbiUseInventoryItem.ts b/src/blobbi/actions/hooks/useBlobbiUseInventoryItem.ts index 154e1eb0..c966ed8a 100644 --- a/src/blobbi/actions/hooks/useBlobbiUseInventoryItem.ts +++ b/src/blobbi/actions/hooks/useBlobbiUseInventoryItem.ts @@ -25,6 +25,7 @@ import { applyStat, hasMedicineEffectForEgg, hasHygieneEffectForEgg, + incrementInteractionTaskTags, type InventoryAction, ACTION_METADATA, } from '../lib/blobbi-action-utils'; @@ -271,32 +272,9 @@ export function useBlobbiUseInventoryItem({ const nowStr = now.toString(); // If incubating, increment the interaction counter for hatch tasks - let updatedTags = canonical.allTags; - if (canonical.companion.state === 'incubating') { - // Get current interaction count from task tags - const interactionTag = canonical.allTags.find(tag => - tag[0] === 'task' && tag[1]?.startsWith('interactions:') - ); - const currentCount = interactionTag - ? parseInt(interactionTag[1].split(':')[1] || '0', 10) - : 0; - const newCount = currentCount + 1; - - // Remove old interaction task tag and add new one - updatedTags = canonical.allTags.filter(tag => - !(tag[0] === 'task' && tag[1]?.startsWith('interactions:')) - ); - updatedTags = [...updatedTags, ['task', `interactions:${newCount}`]]; - - // Mark as completed if reached 7 - if (newCount >= 7) { - // Remove any existing task_completed for interactions - updatedTags = updatedTags.filter(tag => - !(tag[0] === 'task_completed' && tag[1] === 'interactions') - ); - updatedTags = [...updatedTags, ['task_completed', 'interactions']]; - } - } + const updatedTags = canonical.companion.state === 'incubating' + ? incrementInteractionTaskTags(canonical.allTags).updatedTags + : canonical.allTags; const blobbiTags = updateBlobbiTags(updatedTags, { ...statsUpdate, diff --git a/src/blobbi/actions/hooks/useHatchTasks.ts b/src/blobbi/actions/hooks/useHatchTasks.ts index 40df3272..ea71085a 100644 --- a/src/blobbi/actions/hooks/useHatchTasks.ts +++ b/src/blobbi/actions/hooks/useHatchTasks.ts @@ -240,15 +240,27 @@ export function useHatchTasks( required: 1, completed: hasColorMoment, action: 'external_link', - actionTarget: 'https://espy.social/', + actionTarget: 'https://espy.you/', actionLabel: 'Open espy', }); // 3. Change Avatar Shape + // Requirements for completion: + // 1. There must be a kind 0 profile update with created_at >= state_started_at + // 2. The shape value must be different from what it was before incubation started + // 3. If there was no profile before start, any shape set after start counts + // 4. If shape is undefined/same after the update, task is NOT complete const shapeBefore = data?.profileBefore ? extractShapeFromMetadata(data.profileBefore) : undefined; const shapeAfter = data?.profileAfter ? extractShapeFromMetadata(data.profileAfter) : undefined; - // Task completes if shape changed (and there was an update after start) - const shapeChanged = data?.profileAfter && shapeAfter !== shapeBefore; + + // Task completes only if: + // - There is a profile update after incubation started (profileAfter exists) + // - AND the shape after is defined (user actually set a shape) + // - AND the shape is different from before (actual change occurred) + const hasPostStartProfileUpdate = !!data?.profileAfter; + const hasNewShapeValue = shapeAfter !== undefined && shapeAfter !== ''; + const shapeActuallyChanged = shapeAfter !== shapeBefore; + const shapeChanged = hasPostStartProfileUpdate && hasNewShapeValue && shapeActuallyChanged; tasks.push({ id: 'change_shape', name: 'Change Avatar Shape', diff --git a/src/blobbi/actions/index.ts b/src/blobbi/actions/index.ts index b6492ba6..db8e7be4 100644 --- a/src/blobbi/actions/index.ts +++ b/src/blobbi/actions/index.ts @@ -80,6 +80,7 @@ export { type ResolvedInventoryItem, type EggStatPreview, type ItemUsabilityResult, + type IncrementInteractionResult, // Constants ACTION_TO_ITEM_TYPE, ACTION_METADATA, @@ -110,4 +111,5 @@ export { hasHygieneEffectForEgg, canUseItemForStage, getActionForItem, + incrementInteractionTaskTags, } from './lib/blobbi-action-utils'; diff --git a/src/blobbi/actions/lib/blobbi-action-utils.ts b/src/blobbi/actions/lib/blobbi-action-utils.ts index 02ce11da..c289b6c5 100644 --- a/src/blobbi/actions/lib/blobbi-action-utils.ts +++ b/src/blobbi/actions/lib/blobbi-action-utils.ts @@ -545,3 +545,62 @@ export function previewCleanForEgg( return results; } + +// ─── Interaction Task Helpers ───────────────────────────────────────────────── + +/** Required interactions to complete the interactions hatch task */ +const INTERACTION_TASK_REQUIRED = 7; + +/** + * Result of incrementing interaction task tags + */ +export interface IncrementInteractionResult { + /** Updated tags array */ + updatedTags: string[][]; + /** New interaction count after increment */ + newCount: number; + /** Whether the task is now complete */ + isCompleted: boolean; +} + +/** + * Increment the interaction task counter in the tags array. + * + * This is used by both useBlobbiDirectAction and useBlobbiUseInventoryItem + * to track progress on the "Interact with Blobbi" hatch task. + * + * Tag format: + * - Progress: ["task", "interactions:N"] + * - Completion: ["task_completed", "interactions"] + * + * @param currentTags - Current tags array from the Blobbi state + * @returns Updated tags array with incremented interaction count + */ +export function incrementInteractionTaskTags(currentTags: string[][]): IncrementInteractionResult { + // Get current interaction count from task tags + const interactionTag = currentTags.find(tag => + tag[0] === 'task' && tag[1]?.startsWith('interactions:') + ); + const currentCount = interactionTag + ? parseInt(interactionTag[1].split(':')[1] || '0', 10) + : 0; + const newCount = currentCount + 1; + + // Remove old interaction task tag and add new one + let updatedTags = currentTags.filter(tag => + !(tag[0] === 'task' && tag[1]?.startsWith('interactions:')) + ); + updatedTags = [...updatedTags, ['task', `interactions:${newCount}`]]; + + // Mark as completed if reached required count + const isCompleted = newCount >= INTERACTION_TASK_REQUIRED; + if (isCompleted) { + // Remove any existing task_completed for interactions (avoid duplicates) + updatedTags = updatedTags.filter(tag => + !(tag[0] === 'task_completed' && tag[1] === 'interactions') + ); + updatedTags = [...updatedTags, ['task_completed', 'interactions']]; + } + + return { updatedTags, newCount, isCompleted }; +} diff --git a/src/pages/BlobbiPage.tsx b/src/pages/BlobbiPage.tsx index 40ef59ee..294d5903 100644 --- a/src/pages/BlobbiPage.tsx +++ b/src/pages/BlobbiPage.tsx @@ -616,7 +616,7 @@ interface DashboardShellProps { function DashboardShell({ children }: DashboardShellProps) { return (
-
+
{/* Frosted glass dashboard container */}
{/* Subtle decorative gradient overlay */} @@ -935,11 +935,23 @@ function BlobbiDashboard({ onSetAsCompanion={() => console.log('TODO: set as companion')} onTakePhoto={() => console.log('TODO: take photo')} onOpenPiP={() => console.log('TODO: open PiP')} - onEvolve={isEgg ? onHatch : onEvolve} - isTransitioning={isHatching || isEvolving} + onEvolve={ + // For eggs not yet incubating: show incubation dialog + // For eggs incubating with all tasks complete: hatch action handled in HatchTasksPanel + // For baby: evolve to adult + canStartIncubation + ? () => setShowIncubationDialog(true) + : isEgg + ? onHatch + : onEvolve + } + isTransitioning={isHatching || isEvolving || isStartingIncubation} onInfo={() => setShowInfoModal(true)} - // Hide hatch button when incubating (hatch is in HatchTasksPanel instead) + // Hide hatch button only when actively incubating (hatch is in HatchTasksPanel instead) + // Show button for eggs not yet incubating (to start incubation) hideEvolveButton={isIncubating} + // When canStartIncubation is true, the button triggers incubation start + isIncubationAction={canStartIncubation} /> {/* Blobbi Name */} @@ -1049,20 +1061,7 @@ function BlobbiDashboard({
)} - {/* Start Incubation Button - for eggs not yet incubating */} - {canStartIncubation && ( -
- -
- )} + {/* Inline Activity Area - inside padded container for proper spacing above bottom bar */} {inlineActivity.type === 'music' && ( @@ -1275,7 +1274,6 @@ interface FloatingActionDef { } interface BlobbiDashboardFloatingControlsProps { - /** Current Blobbi's life stage - affects evolve button icon */ stage: 'egg' | 'baby' | 'adult'; onBack?: () => void; onSetAsCompanion: () => void; @@ -1287,14 +1285,17 @@ interface BlobbiDashboardFloatingControlsProps { onInfo: () => void; /** Whether to hide the evolve/hatch button (e.g., when incubating) */ hideEvolveButton?: boolean; + /** Whether the button should show incubation action (for eggs not yet incubating) */ + isIncubationAction?: boolean; } /** - * Get the appropriate icon for the evolve/hatch button based on stage. - * - egg stage: Egg icon (hatching action) + * Get the appropriate icon for the evolve/hatch button based on stage and incubation state. + * - egg stage (not incubating): Egg icon (start incubation action) + * - egg stage (incubating): Egg icon (hatching action) * - baby/adult stages: Sparkles icon (evolution/transformation) */ -function getEvolveIcon(stage: 'egg' | 'baby' | 'adult'): React.ReactNode { +function getEvolveIcon(stage: 'egg' | 'baby' | 'adult', _isIncubationAction?: boolean): React.ReactNode { if (stage === 'egg') { return ; } @@ -1303,11 +1304,11 @@ function getEvolveIcon(stage: 'egg' | 'baby' | 'adult'): React.ReactNode { } /** - * Get the appropriate tooltip for the evolve/hatch button based on stage. + * Get the appropriate tooltip for the evolve/hatch button based on stage and incubation state. */ -function getEvolveTooltip(stage: 'egg' | 'baby' | 'adult'): string { +function getEvolveTooltip(stage: 'egg' | 'baby' | 'adult', isIncubationAction?: boolean): string { if (stage === 'egg') { - return 'Hatch'; + return isIncubationAction ? 'Start Incubation' : 'Hatch'; } return 'Evolve'; } @@ -1326,6 +1327,7 @@ function BlobbiDashboardFloatingControls({ isTransitioning = false, onInfo, hideEvolveButton = false, + isIncubationAction = false, }: BlobbiDashboardFloatingControlsProps) { // Left-side buttons const leftButtons: FloatingActionDef[] = [ @@ -1365,12 +1367,12 @@ function BlobbiDashboardFloatingControls({ }, ]; - // Evolve/Hatch button (emphasized, at the bottom of right cluster) - // Icon and tooltip are stage-aware + // Evolve/Hatch/Incubation button (emphasized, at the bottom of right cluster) + // Icon and tooltip are stage-aware and incubation-aware const evolveButton: FloatingActionDef = { id: 'evolve', - icon: getEvolveIcon(stage), - tooltip: getEvolveTooltip(stage), + icon: getEvolveIcon(stage, isIncubationAction), + tooltip: getEvolveTooltip(stage, isIncubationAction), onClick: onEvolve, variant: 'accent', };