Skip to content

Roasty can be dressed, and every screen draws the outfit he has on - #590

Merged
maximsan merged 9 commits into
mainfrom
feat/roasty-studio
Sep 11, 2026
Merged

Roasty can be dressed, and every screen draws the outfit he has on#590
maximsan merged 9 commits into
mainfrom
feat/roasty-studio

Conversation

@maximsan

Copy link
Copy Markdown
Owner

Why

The paywall has sold "Roasty and Coffee Tree Studios" since the monetization
shape landed, and no ticket built the Roasty half. Four axes — roast, hat,
accessory, sprout — over a live preview, worn on every screen in the app.

Closes #367.

Watch for

  • One acceptance criterion was reversed, on the owner's ruling. Roasty Studio: dress the mascot, and every screen shows him wearing it #367 asked
    that Reset Progress return the plain outfit. The shipped storage layer rules
    the opposite — "the grove, the companion and every account-scoped key
    survive: 'reset everything' means the trail, not the wardrobe"
    — with a test
    already holding it, and Roasty Studio: dress the mascot, and every screen shows him wearing it #367's own "beside the grove" points the same way.
    The outfit survives Reset. The ticket's criterion is corrected to match.
  • The wardrobe route was not gated, and is now. The redirect held the
    grove's door behind the entitlement with a comment warning that a screen
    guarding itself is a guard one route can be added around. The wardrobe was
    that route: a free learner deep-linking to /profile/roasty reached a
    working, writable wardrobe. Both doors are named there now.
  • The door draws the gated outfit. It read the snapshot directly, so a
    learner whose Plus had lapsed met a dressed mascot on it while every other
    screen drew the plain bean.
  • CompanionOutfitScope is ambient, like context.mood. That is what
    makes the outfit app-wide without threading it through eleven call sites —
    and a widget test that pumps the mascot alone still gets the plain bean.
  • Three painter files join the metric exclusions. Same reason as the four
    already there: one method per drawing.
  • The wardrobe is its own route, not a section inside the grove chooser.
    The design draws two doors side by side, which is what shipped; Studio: the grove's three species, four lights, and the tree they compose over #135's "can
    join later without moving the grove" is satisfied by the grove being
    untouched.

Checks

Driven on the simulator: all four axes picked, the preview redrawing under
each tap, and the mascot ending in espresso, beanie, headphones and cherry.

The four axes reach the app as banks off `customize.jsx`, and the art they
name is Dart: roast gradients thread through the body paint, hats and gear
ride the body transform under the face, and a sprout grows through a hat when
one is worn.

`CompanionOutfitScope` is what makes it app-wide. Roasty reads the outfit the
way it already reads `context.mood` — ambient, installed once above the
router — so a mascot three hosts deep cannot be handed the wrong one, and a
widget test that pumps him alone still gets the plain bean.

The outfit itself was already on the snapshot with its merge envelope; what it
lacked was a writer. `dressCompanion` is `plantGrove`'s twin, no-op included,
because an identical write moves a last-writer-wins stamp and beats a real
pick from another device.

Gated in one place: `companionOutfitProvider` awaits the entitlement and hands
back the plain bean without it, leaving what the learner picked untouched in
storage.

The sprout art now comes from the running prototype for all four variants,
which drops the mascot design page's stem `M100 88 Q100 80 100 70` and its
1–2px-higher leaf (ADR-0009).
The grove chooser keeps its door and the mascot gets a second one beside it,
so neither has to know the other exists. Four option rows over a live preview:
picking redraws the bean where it stands, and the confirm stays dead until the
pick differs from what he already has on.

The screen reads the snapshot rather than `companionOutfit`, deliberately —
this is what *edits* the outfit, so it has to see what is stored even while the
gate would hide it. The door that opens it is Plus-locked already.
# Conflicts:
#	docs/CHANGELOG.md
#	lib/app/app_router.dart
#	lib/app/app_router.g.dart
The door read the snapshot directly, so a learner whose Plus had lapsed met a
dressed mascot on it — and its subtitle named the picks — while every other
screen in the app drew the plain bean. The gate is meant to hide the wardrobe,
not just lock the room.

`subtitleFor` takes the outfit it describes rather than reading the stored one,
which is what lets the door describe the gated one. The Studio screen behind it
still reads the snapshot: that is what edits the outfit, so it has to see it.

The merge's named cases gain the outfit — the laws already covered it through
their generator, but a criterion that says two devices converge on one outfit
should be readable as a test that says so.
…tated

**The wardrobe route was not gated.** The redirect had the grove's door behind
the entitlement with a comment saying a screen that guards itself is a guard
one route can be added around — and the wardrobe is exactly the route that was
added around it. A free learner deep-linking to it got a working, writable
wardrobe. Both doors are named there now.

**Nothing held the outfit app-wide.** The scope is installed in one line at the
app root, and deleting that line left the whole suite green while every screen
went plain — the failure mode the ticket names. A guard now fails there, and
the wardrobe door's gate is walked rather than only its pill asserted.

`build()` no longer seeds the draft by assignment: `??` instead of `??=` drops
the write and lets a re-read `worn` through, which the old one swallowed. The
error state announced itself twice, and the door's subtitle moved onto the
banks it reads.
`COMPANION`, not `Studio`, and the axes rather than the picks — the grove's
door names what is planted, this one names what can be changed. That also
takes the gated outfit out of the subtitle, leaving the drawn mascot as the
only thing the gate has to hide there, and the bank read the door no longer
needs.
`CompanionDraft` restated `CompanionConfig`'s four fields, its `==` and its
`hashCode`, and its four `withX` were a hand-written `copyWith`. The config had
no `copyWith`, which is the only reason the class existed — so it has one now
and the draft is gone. The screen holds an outfit, `dirty` is `!=`, and 78
lines go with it.

Each painter paired a `switch` on the id with a `const` set of the same ids,
kept in step by hand. One table per axis now: a drawing the table does not
list cannot be reached, and an id it lists cannot draw nothing — which the
parallel set could not catch, because it only ever saw the ids.

`copyWith` keeps the coverage the draft's tests held.
# Conflicts:
#	lib/features/companion/presentation/roasty.dart
@maximsan
maximsan merged commit a0a029a into main Sep 11, 2026
7 checks passed
@maximsan
maximsan deleted the feat/roasty-studio branch September 11, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Roasty Studio: dress the mascot, and every screen shows him wearing it

1 participant