HatchedDocs
Concepts

Compositing & stages

How equipped items layer onto the buddy, and how evolution preserves them atomically.

When a user equips a hat on a stage-1 egg and then evolves to stage 2, the hat does not disappear. This page is how that invariant is maintained end-to-end.

The 8 canonical categories

Every marketplace item belongs to one of eight slots:

Categorylayer_orderMulti-equip?
background10no
body20no
feet30no
hand40no
neck50no
face60no
head70no
accessory80yes

layer_order is the compositing z-order (back → front). background paints first, accessory paints last. accessory is the only slot that accepts multiple equipped items — everything else rejects the second item in the same category with category_conflict.

Equip bounds

  • Max 4 equipped items. The fifth rejects with too_many_items.
  • Non-accessory categories are exclusive. Equipping a second head while one is already equipped rejects with category_conflict.
  • Items sort deterministically by (layer_order, item_id) before reaching the image pipeline, so two equipped items always composite in the same order.

These checks happen at the API boundary, surfaced in the SDK as TooManyItemsError and CategoryConflictError.

Stage-aware item assets

Items can ship a stage-specific override via stage_image_urls:

{
  "image_url": "https://cdn.hatched.live/items/wizard_hat/base.png",
  "stage_image_urls": {
    "3": "https://cdn.hatched.live/items/wizard_hat/stage3.png",
    "5": "https://cdn.hatched.live/items/wizard_hat/stage5.png"
  }
}

The compositing pipeline reads stage_image_urls[currentStage] and falls back to image_url when there's no override. Designers only have to ship overrides for the stages where the base asset would look wrong.

The appearance state machine

Anything that changes the buddy's image — hatch, equip / unequip, evolve — runs through the image pipeline asynchronously. The buddy carries an appearance block so you always know whether what you're showing is the final render. This is the single source of truth for "is the visual ready"; the buddy's economy state (coins, skills, stage) is already committed regardless.

appearance.statusMeaningWhat to show / do
readyimage_url is the final composite; rendered_equipped_items matches equipped_items.Show image_url. Nothing to do.
pendingA job is generating or compositing. appearance.operation_id points at it.Show image_url (the last good render) and an optional "updating…" hint. operations.wait(operationId) to know when it's done.
awaiting_creditsThe job is blocked on insufficient image credits.Show the last good image_url. Surface a top-up prompt; the job resumes once credits land. See Credits.
failedThe job failed. Check appearance.error.code.Show the last good image_url. If error.code === 'needs_rerender' (typically a migrated buddy with no usable bare-stage image), call buddies.rerenderAppearance(buddyId) — or the widget POST /widget/appearance/rerender with an items:equip session — wait for ready, then re-equip. For other error codes, retry the originating action.

Two fields make recovery deterministic: base_image_url is the trustworthy bare-stage image, and equipped_items is the desired item set. image_url and rendered_equipped_items are "what's currently on screen". A rerender regenerates the bare stage from scratch, after which you re-apply equipped_items.

Atomic evolve × equip

The invariant that unlocks the whole feature: the stage transition is committed atomically, while the item composite is tracked as appearance state.

What happens when a user with an equipped hat evolves:

  1. Client calls hatched.buddies.evolve(buddyId) and receives an operation_id.
  2. The evolve worker re-checks readiness, then generates the next-stage bare image and stores it as base_image_url.
  3. If equipped_items is non-empty, the same job attempts to composite the desired items over that bare image.
  4. If compositing succeeds, buddy.image_url becomes the rendered image, rendered_equipped_items matches equipped_items, and appearance.status is ready.
  5. If compositing is delayed or fails, the stage still advances. The buddy keeps the new bare stage image, appearance.status becomes awaiting_credits or failed, and appearance.operation_id points at the job that owns recovery.
  6. Operation transitions to completed, and buddy.evolved fires on webhooks. Read buddy.appearance to decide whether the visual composite is also done.
const op = await hatched.buddies.evolve(buddyId);
const result = await hatched.operations.wait(op.operationId);
// result.buddy.evolutionStage has advanced.
// result.buddy.appearance?.status tells you whether item compositing is ready.

The split matters for recovery. base_image_url is the trustworthy bare stage. image_url is the currently displayable render. equipped_items is the desired item set, while rendered_equipped_items is what actually made it into the current image. If a migrated buddy reports appearance.status === 'failed' with error.code === 'needs_rerender', call buddies.rerenderAppearance(buddyId) or the widget POST /widget/appearance/rerender endpoint, wait until ready, then re-equip the desired items.

Demo path parity

The demo widget (publishable-key widget_sessions.demo) runs through the same atomic pipeline via a mock image provider. Stage + equipped items still composite; evolution history rows are still written with source: 'demo'. That's why the marketing demo and production builds show identical behavior for this flow.