Separate progression state from activity state to prevent evolution reset

Sleep/wake toggles overwrote 'evolving' with 'sleeping', permanently
destroying evolution progress. The root cause was using a single state
tag for two orthogonal concerns: activity (active/sleeping/hibernating)
and progression (incubating/evolving).

Introduce progression_state and progression_started_at as new tags
orthogonal to the activity state tag. Sleep, wake, and hibernation
changes now never touch progression. Parser auto-migrates legacy
events that stored progression in the state tag on read.
This commit is contained in:
filemon
2026-04-18 06:46:17 -03:00
parent 363e39d72c
commit bf6788c141
19 changed files with 201 additions and 66 deletions
@@ -434,8 +434,8 @@ export function BlobbiMissionsModal({
showMissionCard,
onToggleMissionCard,
}: BlobbiMissionsModalProps) {
const isIncubating = companion.state === 'incubating';
const isEvolvingState = companion.state === 'evolving';
const isIncubating = companion.progressionState === 'incubating';
const isEvolvingState = companion.progressionState === 'evolving';
const isEgg = companion.stage === 'egg';
const isBaby = companion.stage === 'baby';
@@ -46,7 +46,7 @@ export function StartEvolutionDialog({
isPending,
}: StartEvolutionDialogProps) {
// Check if the current Blobbi is already evolving
const isAlreadyEvolving = companion?.state === 'evolving';
const isAlreadyEvolving = companion?.progressionState === 'evolving';
// Determine title and description based on state
const getDialogContent = () => {
@@ -54,14 +54,14 @@ export function StartIncubationDialog({
isPending,
}: StartIncubationDialogProps) {
// Check if the current Blobbi is already in a task state
const isAlreadyInTaskState = companion?.state === 'incubating' || companion?.state === 'evolving';
const isAlreadyInTaskState = companion?.progressionState === 'incubating' || companion?.progressionState === 'evolving';
// Check if another Blobbi (not this one) is currently incubating
const otherIncubatingBlobbi = useMemo(() => {
if (!companion) return null;
return companions.find(c =>
c.d !== companion.d &&
c.state === 'incubating' &&
c.progressionState === 'incubating' &&
c.stage === 'egg'
) ?? null;
}, [companion, companions]);
@@ -139,8 +139,8 @@ export function useActiveTaskProcess(
// Determine which process is active
const processType = useMemo((): TaskProcessType => {
if (!companion) return null;
if (companion.state === 'incubating') return 'hatch';
if (companion.state === 'evolving') return 'evolve';
if (companion.progressionState === 'incubating') return 'hatch';
if (companion.progressionState === 'evolving') return 'evolve';
return null;
}, [companion]);
@@ -147,9 +147,9 @@ export function useBlobbiDirectAction({
const nowStr = now.toString();
// If incubating or evolving, increment the interaction counter in evolution missions
const companionState = canonical.companion.state;
const progressionState = canonical.companion.progressionState;
const updatedTags = canonical.allTags;
if (companionState === 'incubating' || companionState === 'evolving') {
if (progressionState === 'incubating' || progressionState === 'evolving') {
trackEvolutionMissionTally('interactions', 1, user.pubkey);
}
+20 -17
View File
@@ -181,17 +181,18 @@ export function useStartIncubation({
// Apply decay to the other Blobbi
const otherDecayResult = applyBlobbiDecay({
stage: 'egg',
state: 'incubating',
state: 'active',
stats: otherStats,
lastDecayAt: otherLastDecayAt,
now,
});
// Remove task tags and state_started_at from the other Blobbi
// Remove task tags and progression timing from the other Blobbi
const otherCleanedTags = otherEvent.tags.filter(tag =>
tag[0] !== 'task' &&
tag[0] !== 'task_completed' &&
tag[0] !== 'state_started_at'
tag[0] !== 'state_started_at' &&
tag[0] !== 'progression_started_at'
);
const otherNewTags = updateBlobbiTags(otherCleanedTags, {
@@ -200,7 +201,7 @@ export function useStartIncubation({
happiness: otherDecayResult.stats.happiness.toString(),
hunger: '100',
energy: '100',
state: 'active',
progression_state: 'none',
last_interaction: nowStr,
last_decay_at: nowStr,
});
@@ -254,8 +255,8 @@ export function useStartIncubation({
const newTags = updateBlobbiTags(cleanedTags, {
...statsUpdate,
state: 'incubating',
state_started_at: nowStr,
progression_state: 'incubating',
progression_started_at: nowStr,
last_interaction: nowStr,
last_decay_at: nowStr,
});
@@ -375,7 +376,7 @@ export function useStopIncubation({
throw new Error('No companion selected');
}
if (companion.state !== 'incubating') {
if (companion.progressionState !== 'incubating') {
throw new Error('This Blobbi is not incubating');
}
@@ -398,11 +399,12 @@ export function useStopIncubation({
});
// ─── Build Updated Tags ───
// Remove task tags and state_started_at
// Remove task tags and progression timing
const cleanedTags = canonical.allTags.filter(tag =>
tag[0] !== 'task' &&
tag[0] !== 'task_completed' &&
tag[0] !== 'state_started_at'
tag[0] !== 'state_started_at' &&
tag[0] !== 'progression_started_at'
);
// Build stats update with decayed values
@@ -417,7 +419,7 @@ export function useStopIncubation({
const newTags = updateBlobbiTags(cleanedTags, {
...statsUpdate,
state: 'active',
progression_state: 'none',
last_interaction: nowStr,
last_decay_at: nowStr,
});
@@ -510,7 +512,7 @@ export function useStartEvolution({
throw new Error('Only baby Blobbis can evolve');
}
if (companion.state === 'evolving') {
if (companion.progressionState === 'evolving') {
throw new Error('This Blobbi is already evolving');
}
@@ -549,8 +551,8 @@ export function useStartEvolution({
const newTags = updateBlobbiTags(cleanedTags, {
...statsUpdate,
state: 'evolving',
state_started_at: nowStr,
progression_state: 'evolving',
progression_started_at: nowStr,
last_interaction: nowStr,
last_decay_at: nowStr,
});
@@ -656,7 +658,7 @@ export function useStopEvolution({
throw new Error('No companion selected');
}
if (companion.state !== 'evolving') {
if (companion.progressionState !== 'evolving') {
throw new Error('This Blobbi is not evolving');
}
@@ -679,11 +681,12 @@ export function useStopEvolution({
});
// ─── Build Updated Tags ───
// Remove task tags and state_started_at
// Remove task tags and progression timing
const cleanedTags = canonical.allTags.filter(tag =>
tag[0] !== 'task' &&
tag[0] !== 'task_completed' &&
tag[0] !== 'state_started_at'
tag[0] !== 'state_started_at' &&
tag[0] !== 'progression_started_at'
);
// Build stats update with decayed values
@@ -697,7 +700,7 @@ export function useStopEvolution({
const newTags = updateBlobbiTags(cleanedTags, {
...statsUpdate,
state: 'active',
progression_state: 'none',
last_interaction: nowStr,
last_decay_at: nowStr,
});
@@ -201,12 +201,12 @@ export function useBlobbiHatch({
}
// ─── Auto-start evolution for newly hatched babies ───
// Applied AFTER tag validation because cleanupTaskTags repairs
// task-process states to 'active'. We intentionally set 'evolving'
// here so the baby starts its evolution journey immediately.
// Applied AFTER tag validation because cleanupTaskTags clears
// progression tags. We set the new progression_state here so the
// baby starts its evolution journey immediately.
const newTags = updateBlobbiTags(repairResult.tags, {
state: 'evolving',
state_started_at: nowStr,
progression_state: 'evolving',
progression_started_at: nowStr,
});
// ─── Generate New Content for Baby Stage ───
@@ -355,7 +355,10 @@ export function useBlobbiEvolve({
console.log('[Evolve] Tag repairs applied:', repairResult.repairs);
}
const newTags = repairResult.tags;
// Ensure progression is cleared after evolve
const newTags = updateBlobbiTags(repairResult.tags, {
progression_state: 'none',
});
// ─── Generate New Content for Adult Stage ───
// CRITICAL: Content must reflect the new stage
@@ -241,9 +241,9 @@ export function useBlobbiUseInventoryItem({
const nowStr = now.toString();
// If incubating or evolving, increment the interaction counter in evolution missions
const companionState = canonical.companion.state;
const progressionState = canonical.companion.progressionState;
const updatedTags = canonical.allTags;
if (companionState === 'incubating' || companionState === 'evolving') {
if (progressionState === 'incubating' || progressionState === 'evolving') {
trackEvolutionMissionTally('interactions', 1, user?.pubkey);
}
+1 -1
View File
@@ -90,7 +90,7 @@ export function useEvolveTasks(
const { nostr } = useNostr();
const pubkey = user?.pubkey;
const isEvolving = companion?.state === 'evolving';
const isEvolving = companion?.progressionState === 'evolving';
const evolution = useMemo(() => missions?.evolution ?? [], [missions?.evolution]);
// ─── Ensure evolution missions exist and match current definitions ───
+1 -1
View File
@@ -109,7 +109,7 @@ export function useHatchTasks(
const { nostr } = useNostr();
const pubkey = user?.pubkey;
const isIncubating = companion?.state === 'incubating';
const isIncubating = companion?.progressionState === 'incubating';
const evolution = useMemo(() => missions?.evolution ?? [], [missions?.evolution]);
// ─── Ensure evolution missions exist and match current definitions ───
@@ -351,9 +351,9 @@ export function useBlobbiItemUse(options: UseBlobbiItemUseOptions = {}): UseBlob
const nowStr = now.toString();
// If incubating or evolving, increment the interaction counter in evolution missions
const companionState = companion.state;
const progressionState = companion.progressionState;
const updatedTags = companion.allTags;
if (companionState === 'incubating' || companionState === 'evolving') {
if (progressionState === 'incubating' || progressionState === 'evolving') {
trackEvolutionMissionTally('interactions', 1, user?.pubkey);
}
+35 -8
View File
@@ -382,8 +382,8 @@ export const BLOBBI_TAG_SCHEMA: readonly BlobbiTagSchema[] = [
persistent: false,
source: 'system',
regenerable: false,
format: 'active | sleeping | hibernating | incubating | evolving',
notes: 'incubating is for eggs, evolving is for babies. Reset to active after transition.',
format: 'active | sleeping | hibernating',
notes: 'Activity state only. Progression (incubation/evolution) is tracked in progression_state.',
},
{
tag: 'last_interaction',
@@ -414,8 +414,21 @@ export const BLOBBI_TAG_SCHEMA: readonly BlobbiTagSchema[] = [
// TASK SYSTEM TAGS
// ═══════════════════════════════════════════════════════════════════════════
{
tag: 'state_started_at',
description: 'Unix timestamp when current state (incubating/evolving) started',
tag: 'progression_state',
description: 'Current progression process state (orthogonal to activity state)',
category: 'task',
required: false,
stages: ['egg', 'baby', 'adult'],
persistent: false,
source: 'system',
regenerable: false,
format: 'none | incubating | evolving',
defaultValue: 'none',
notes: 'Set to incubating/evolving when a progression process starts. Cleared to none after hatch/evolve completes. Survives sleep/wake/hibernation changes.',
},
{
tag: 'progression_started_at',
description: 'Unix timestamp when current progression (incubating/evolving) started',
category: 'task',
required: false,
stages: ['egg', 'baby', 'adult'],
@@ -425,6 +438,18 @@ export const BLOBBI_TAG_SCHEMA: readonly BlobbiTagSchema[] = [
format: 'Unix timestamp (seconds)',
notes: 'Set when entering incubating/evolving. REMOVED after hatch/evolve completes.',
},
{
tag: 'state_started_at',
description: '@deprecated Use progression_started_at instead. Unix timestamp when current state (incubating/evolving) started',
category: 'task',
required: false,
stages: ['egg', 'baby', 'adult'],
persistent: false,
source: 'system',
regenerable: false,
format: 'Unix timestamp (seconds)',
notes: 'Legacy tag. New events should use progression_started_at instead.',
},
{
tag: 'task',
description: 'Task progress tracking',
@@ -668,7 +693,7 @@ export function getPersistentTagNames(): Set<string> {
* These are task-related and state-specific tags.
*/
export function getTransitionCleanupTagNames(): Set<string> {
return new Set(['task', 'task_completed', 'state_started_at']);
return new Set(['task', 'task_completed', 'state_started_at', 'progression_started_at', 'progression_state']);
}
/**
@@ -776,13 +801,15 @@ const NEVER_INVENT_TAGS = new Set([
* Valid states for each stage. Used to validate/repair state after transitions.
*/
const VALID_STATES_BY_STAGE: Record<BlobbiStage, Set<string>> = {
egg: new Set(['active', 'sleeping', 'hibernating', 'incubating']),
baby: new Set(['active', 'sleeping', 'hibernating', 'evolving']),
egg: new Set(['active', 'sleeping', 'hibernating']),
baby: new Set(['active', 'sleeping', 'hibernating']),
adult: new Set(['active', 'sleeping', 'hibernating']),
};
/**
* Task-process states that should NOT remain after stage transitions.
* Task-process states that should NOT remain in the `state` tag after stage transitions.
* With the new model, state should never be 'incubating'/'evolving' — but we keep this
* for migration: if a legacy event has them in state, the repair will fix it.
*/
const TASK_PROCESS_STATES = new Set(['incubating', 'evolving']);
+84 -5
View File
@@ -94,7 +94,16 @@ export function getDaysDifference(dayA: string, dayB: string): number {
// ─── Types ────────────────────────────────────────────────────────────────────
export type BlobbiStage = 'egg' | 'baby' | 'adult';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating' | 'incubating' | 'evolving';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating';
/**
* Progression process state — orthogonal to BlobbiState.
*
* 'none' — no progression process active
* 'incubating' — egg is being incubated (hatch tasks)
* 'evolving' — baby is being evolved (evolve tasks)
*/
export type BlobbiProgressionState = 'none' | 'incubating' | 'evolving';
export interface BlobbiStats {
hunger: number;
@@ -239,8 +248,10 @@ export interface BlobbiCompanion {
name: string;
/** Lifecycle stage */
stage: BlobbiStage;
/** Activity state */
/** Activity state (active, sleeping, hibernating — never progression) */
state: BlobbiState;
/** Progression process state (none, incubating, evolving — orthogonal to state) */
progressionState: BlobbiProgressionState;
/** Deterministic identity seed (64-char hex) */
seed: string | undefined;
/** Visual traits (derived from seed or legacy tags) */
@@ -277,8 +288,14 @@ export interface BlobbiCompanion {
startIncubation: number | undefined;
/** Adult evolution form type (adult only) */
adultType: string | undefined;
/** Timestamp when current state (incubating/evolving) started (unix seconds) */
/**
* @deprecated Use progressionStartedAt instead.
* Timestamp when current state (incubating/evolving) started (unix seconds).
* Kept for migration compatibility.
*/
stateStartedAt: number | undefined;
/** Timestamp when current progression (incubating/evolving) started (unix seconds) */
progressionStartedAt: number | undefined;
/** Task progress cache (source of truth is computed from Nostr events) */
tasks: BlobbiTaskProgress[];
/** Completed task names */
@@ -769,6 +786,8 @@ export function isValidBlobbiEvent(event: NostrEvent): boolean {
if (!d) return false;
if (b !== BLOBBI_ECOSYSTEM_NAMESPACE) return false;
if (!stage || !['egg', 'baby', 'adult'].includes(stage)) return false;
// Accept both new states (active/sleeping/hibernating) and legacy states (incubating/evolving)
// for backwards compatibility during migration
if (!state || !['active', 'sleeping', 'hibernating', 'incubating', 'evolving'].includes(state)) return false;
if (!lastInteraction) return false;
@@ -887,9 +906,33 @@ export function parseBlobbiEvent(event: NostrEvent): BlobbiCompanion | undefined
const d = getTagValue(tags, 'd')!;
const nameTag = getTagValue(tags, 'name');
const stage = getTagValue(tags, 'stage') as BlobbiStage;
const state = getTagValue(tags, 'state') as BlobbiState;
const rawState = getTagValue(tags, 'state')!;
const seed = getTagValue(tags, 'seed');
// ─── Progression state resolution (migration-aware) ───
// New model: progression lives in progression_state tag.
// Old model: progression lived in the state tag ('incubating', 'evolving').
// On read we normalise both into the new model.
const progressionStateTag = getTagValue(tags, 'progression_state') as BlobbiProgressionState | undefined;
let state: BlobbiState;
let progressionState: BlobbiProgressionState;
if (progressionStateTag) {
// New-format event: progression_state tag is authoritative
state = rawState as BlobbiState;
progressionState = progressionStateTag;
} else if (rawState === 'incubating' || rawState === 'evolving') {
// Legacy event: progression was stored in state tag.
// Normalise: move it to progressionState, set activity state to 'active'.
state = 'active';
progressionState = rawState as BlobbiProgressionState;
} else {
// No progression
state = rawState as BlobbiState;
progressionState = 'none';
}
// Resolve name: tag > legacy d-tag derivation > fallback
const name = nameTag ?? deriveNameFromLegacyD(d);
@@ -933,6 +976,7 @@ export function parseBlobbiEvent(event: NostrEvent): BlobbiCompanion | undefined
name,
stage,
state,
progressionState,
seed,
visualTraits,
isLegacy,
@@ -955,6 +999,7 @@ export function parseBlobbiEvent(event: NostrEvent): BlobbiCompanion | undefined
startIncubation: parseNumericTag(tags, 'start_incubation'),
adultType: getTagValue(tags, 'adult_type'),
stateStartedAt: parseNumericTag(tags, 'state_started_at'),
progressionStartedAt: parseNumericTag(tags, 'progression_started_at') ?? parseNumericTag(tags, 'state_started_at'),
tasks,
tasksCompleted,
allTags: tags,
@@ -1045,6 +1090,7 @@ export function buildEggTags(
['name', name],
['stage', 'egg'],
['state', 'active'],
['progression_state', 'none'],
['seed', seed],
['generation', '1'],
['breeding_ready', 'false'],
@@ -1094,6 +1140,8 @@ export const MANAGED_BLOBBI_STATE_TAG_NAMES = new Set([
'experience', 'care_streak', 'care_streak_last_at', 'care_streak_last_day',
// Social/flag tags
'breeding_ready',
// Progression tags (orthogonal to activity state)
'progression_state', 'progression_started_at',
// Task system tags (removed after stage transitions)
'state_started_at', 'task', 'task_completed',
// Evolution tags (adult only)
@@ -1497,11 +1545,15 @@ export function buildMigrationTags(
// Per blobbi-tag-schema.md: Do NOT invent values for tags that don't exist
const persistentTagNames = [
// State/lifecycle tags
'stage', 'state',
'stage',
// Stat tags
'hunger', 'happiness', 'health', 'hygiene', 'energy',
// Progression tags
'experience', 'care_streak', 'care_streak_last_at', 'care_streak_last_day',
// Progression process tags
'progression_state', 'progression_started_at',
// Legacy progression timing (also preserve for fallback)
'state_started_at',
// Social/flag tags
'generation', 'breeding_ready',
// Personality tags (preserve if they exist, do NOT generate)
@@ -1519,6 +1571,33 @@ export function buildMigrationTags(
}
}
// ─── Normalise legacy state → progression_state ───
// If the legacy event has state='incubating' or state='evolving', migrate
// to the new split model during migration.
const legacyState = getTagValue(legacyTags, 'state');
if (legacyState === 'incubating' || legacyState === 'evolving') {
// Set activity state to 'active' (the progression process is tracked separately)
newTags.push(['state', 'active']);
// Only set progression_state if not already set from persistentTagNames
if (!getTagValue(legacyTags, 'progression_state')) {
newTags.push(['progression_state', legacyState]);
// Migrate state_started_at → progression_started_at if present
const startedAt = getTagValue(legacyTags, 'state_started_at');
if (startedAt && !getTagValue(legacyTags, 'progression_started_at')) {
newTags.push(['progression_started_at', startedAt]);
}
}
} else if (legacyState) {
newTags.push(['state', legacyState]);
// Ensure progression_state is set
if (!getTagValue(legacyTags, 'progression_state')) {
newTags.push(['progression_state', 'none']);
}
} else {
newTags.push(['state', 'active']);
newTags.push(['progression_state', 'none']);
}
// ALWAYS include visual trait tags - derived from seed, with legacy tag fallbacks
// This ensures every migrated event has complete visual traits for consistent rendering
const visualTraits = deriveVisualTraits(legacyTags, seed);
+6 -1
View File
@@ -16,7 +16,12 @@
* ────────────────────────────────────────────────────────────────────────── */
export type BlobbiLifeStage = 'egg' | 'baby' | 'adult';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating' | 'incubating' | 'evolving';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating';
/**
* Progression process state — orthogonal to BlobbiState.
*/
export type BlobbiProgressionState = 'none' | 'incubating' | 'evolving';
/* ────────────────────────────────────────────────────────────────────────── *
* Visual traits
+8 -3
View File
@@ -79,9 +79,14 @@ export function useBlobbiDevUpdate({
tagUpdates.state = updates.state;
changedFields.push('state');
// If changing to evolving/incubating, set state_started_at
if (updates.state === 'evolving' || updates.state === 'incubating') {
tagUpdates.state_started_at = now.toString();
// If changing to evolving/incubating (legacy dev support), set progression tags
if ((updates.state as string) === 'evolving' || (updates.state as string) === 'incubating') {
// Dev editor: treat these as progression, not activity state
tagUpdates.progression_state = updates.state;
tagUpdates.progression_started_at = now.toString();
// Override: don't put progression in the state tag
tagUpdates.state = 'active';
changedFields.push('progression_state');
}
}
@@ -188,7 +188,7 @@ export function BlobbiHatchingCeremony({
// Baby companion (same visual data but stage=baby)
const babyCompanion = useMemo((): BlobbiCompanion | null => {
if (!eggCompanion) return null;
return { ...eggCompanion, stage: 'baby', state: 'evolving' };
return { ...eggCompanion, stage: 'baby', state: 'active' as const, progressionState: 'evolving' as const };
}, [eggCompanion]);
const eggColor = preview?.visualTraits.baseColor ?? '#f59e0b';
@@ -211,7 +211,8 @@ export function BlobbiHatchingCeremony({
ownerPubkey: user?.pubkey ?? '',
name: existingCompanion.name,
stage: 'egg',
state: (existingCompanion.state === 'incubating' ? 'incubating' : 'active') as 'incubating' | 'active',
state: 'active' as const,
progressionState: (existingCompanion.progressionState === 'incubating' ? 'incubating' : 'none') as 'incubating' | 'none',
seed: existingCompanion.seed ?? '',
stats: {
hunger: existingCompanion.stats.hunger ?? STAT_MAX,
@@ -387,8 +388,9 @@ export function BlobbiHatchingCeremony({
const babyTags = updateBlobbiTags(tags, {
stage: 'baby',
state: 'evolving',
state_started_at: nowStr,
state: 'active',
progression_state: 'evolving',
progression_started_at: nowStr,
hunger: STAT_MAX.toString(),
happiness: STAT_MAX.toString(),
health: STAT_MAX.toString(),
+10 -4
View File
@@ -35,8 +35,10 @@ export interface BlobbiEggPreview {
name: string;
/** Life stage - always 'egg' for previews */
stage: 'egg';
/** Activity state - new eggs start incubating; older eggs may be 'active' */
state: 'incubating' | 'active';
/** Activity state */
state: 'active';
/** Progression state - new eggs start incubating; older eggs may be 'none' */
progressionState: 'incubating' | 'none';
/** Visual traits derived from seed */
visualTraits: BlobbiVisualTraits;
/** Default stats for a new egg */
@@ -79,7 +81,8 @@ export function generateEggPreview(
seed,
name,
stage: 'egg',
state: 'incubating',
state: 'active',
progressionState: 'incubating',
visualTraits,
stats: { ...DEFAULT_EGG_STATS },
createdAt,
@@ -134,6 +137,7 @@ export function previewToEventTags(preview: BlobbiEggPreview): string[][] {
['name', preview.name],
['stage', preview.stage],
['state', preview.state],
['progression_state', preview.progressionState],
['seed', preview.seed],
['generation', '1'],
['breeding_ready', 'false'],
@@ -148,7 +152,7 @@ export function previewToEventTags(preview: BlobbiEggPreview): string[][] {
['energy', preview.stats.energy.toString()],
['last_interaction', now],
['last_decay_at', now],
['state_started_at', now],
['progression_started_at', now],
// Visual trait tags - ensures deterministic rendering
['base_color', visualTraits.baseColor],
['secondary_color', visualTraits.secondaryColor],
@@ -192,7 +196,9 @@ export function previewToBlobbiCompanion(preview: BlobbiEggPreview) {
adultType: undefined, // Eggs don't have adult type
// Task-related fields
progressionState: preview.progressionState,
stateStartedAt: preview.createdAt,
progressionStartedAt: preview.createdAt,
tasks: [],
tasksCompleted: [],
+2 -2
View File
@@ -1026,8 +1026,8 @@ function BlobbiDashboard({
// State detection for tasks
// Note: isEvolving prop = mutation pending state, isEvolvingState = companion in evolving state
const isIncubating = companion.state === 'incubating';
const isEvolvingState = companion.state === 'evolving';
const isIncubating = companion.progressionState === 'incubating';
const isEvolvingState = companion.progressionState === 'evolving';
const isBaby = companion.stage === 'baby';
const canStartIncubation = isEgg && !isIncubating && !isEvolvingState;
const canStartEvolution = isBaby && !isEvolvingState && !isIncubating;
+6 -1
View File
@@ -16,7 +16,12 @@
* ────────────────────────────────────────────────────────────────────────── */
export type BlobbiLifeStage = 'egg' | 'baby' | 'adult';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating' | 'incubating' | 'evolving';
export type BlobbiState = 'active' | 'sleeping' | 'hibernating';
/**
* Progression process state — orthogonal to BlobbiState.
*/
export type BlobbiProgressionState = 'none' | 'incubating' | 'evolving';
/* ────────────────────────────────────────────────────────────────────────── *
* Visual traits