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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ───
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user