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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
+31
-29
@@ -616,7 +616,7 @@ interface DashboardShellProps {
|
||||
function DashboardShell({ children }: DashboardShellProps) {
|
||||
return (
|
||||
<main className="min-h-[calc(100vh-4rem)] p-4 pb-20">
|
||||
<div className="container mx-auto max-w-4xl">
|
||||
<div className="mx-auto w-full max-w-4xl sm:px-6 lg:px-8">
|
||||
{/* Frosted glass dashboard container */}
|
||||
<div className="relative rounded-2xl bg-card/80 backdrop-blur-sm border border-border overflow-hidden min-h-[70vh]">
|
||||
{/* 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({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Start Incubation Button - for eggs not yet incubating */}
|
||||
{canStartIncubation && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
onClick={() => setShowIncubationDialog(true)}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="gap-2 border-primary/30 hover:border-primary/50 hover:bg-primary/5"
|
||||
>
|
||||
<Egg className="size-5" />
|
||||
Start Incubation
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 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 <Egg className="size-4" />;
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user