Roasty can be dressed, and every screen draws the outfit he has on - #590
Merged
Conversation
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
23 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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.
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/roastyreached aworking, writable wardrobe. Both doors are named there now.
learner whose Plus had lapsed met a dressed mascot on it while every other
screen drew the plain bean.
CompanionOutfitScopeis ambient, likecontext.mood. That is whatmakes 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.
already there: one method per drawing.
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.