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:
| Category | layer_order | Multi-equip? |
|---|---|---|
background | 10 | no |
body | 20 | no |
feet | 30 | no |
hand | 40 | no |
neck | 50 | no |
face | 60 | no |
head | 70 | no |
accessory | 80 | yes |
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
headwhile one is already equipped rejects withcategory_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.status | Meaning | What to show / do |
|---|---|---|
ready | image_url is the final composite; rendered_equipped_items matches equipped_items. | Show image_url. Nothing to do. |
pending | A 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_credits | The 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. |
failed | The 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:
- Client calls
hatched.buddies.evolve(buddyId)and receives anoperation_id. - The evolve worker re-checks readiness, then generates the next-stage
bare image and stores it as
base_image_url. - If
equipped_itemsis non-empty, the same job attempts to composite the desired items over that bare image. - If compositing succeeds,
buddy.image_urlbecomes the rendered image,rendered_equipped_itemsmatchesequipped_items, andappearance.statusisready. - If compositing is delayed or fails, the stage still advances. The buddy
keeps the new bare stage image,
appearance.statusbecomesawaiting_creditsorfailed, andappearance.operation_idpoints at the job that owns recovery. - Operation transitions to
completed, andbuddy.evolvedfires on webhooks. Readbuddy.appearanceto 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.
Related
- Marketplace — where items live.
- Evolution — stage triggers.
- Customize buddy — walking through an equip + evolve flow end to end.