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 canonical categories
Every marketplace item belongs to one of nine categories — eight visual
compositing slots, plus a non-visual booster category:
| 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 |
booster | 90 | no |
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. booster is a
non-visual consumable category (layer_order 90) rather than a
compositing slot, but it is a creatable category via the public API.
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; appearance.rendered_equipped_item_ids matches appearance.desired_equipped_item_ids. | 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 appearance.desired_equipped_item_ids is the desired set
of item ids. image_url and appearance.rendered_equipped_item_ids are "what's
currently on screen". A rerender regenerates the bare stage from scratch, after
which you re-equip those item ids via the marketplace equip endpoint (you don't
write an equipped_items field directly).
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
appearance.desired_equipped_item_idsis non-empty, the same job attempts to composite the desired items over that bare image. - If compositing succeeds,
buddy.image_urlbecomes the rendered image,appearance.rendered_equipped_item_idsmatchesappearance.desired_equipped_item_ids, 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.
appearance.desired_equipped_item_ids is the desired set of item ids, while
appearance.rendered_equipped_item_ids 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 item ids.
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.