2026 10 18 class outfits design - xopherdeep/do-it-for-the-xp GitHub Wiki
Date: 2026-10-18. Status: design approved in chat by the owner; this spec awaits review.
Builds on: 2026-10-13-hero-builder-design.md (the Mii-style Hero Builder, shipped) and
2026-10-13-look-unlocks-design.md (the catalogue and ownership model, approved, not built). This spec builds that
model for real, with class outfits as its first content.
The owner wants the voxel hero to show the class it has chosen, and to show it growing: picking a class unlocks that class's outfit, and the outfit gets better as the class levels up. Later the owner wants heroes to equip different outfits the way RPGs and Animal Crossing let you, so what is built now must already be a wardrobe: outfits a hero owns, tries on and swaps, not a costume painted over the hero by the class.
- Three distinct outfits per class, earned at class level 1, 5 and 10. All 15 classes get three, the advanced and crown classes too. That is 45 voxel outfits, each its own geometry, not a recolour.
- Worn at once, with a fanfare. Picking a class, or reaching class level 5 or 10, dresses the hero in the new outfit on the spot and the hero fanfare shows it. The outfit it replaces stays in the wardrobe.
- Kept forever. A class outfit, once earned, stays owned after a change of class (the owner's 2026-10-13 rule for class pieces).
- The builder's Outfit tab is the wardrobe for now. No inventory screen, no bag items, no prices.
- Earned outfits are computed, never stored (the 2026-10-13 decision): no migration, nothing to lose.
- Everything else from the Look Unlocks decisions stands: the starter set stays free, skin is never locked, locked pieces can be tried on but not kept, a saved look is grandfathered, one grant call for every later source.
The Look Unlocks catalogue and ownership (catalog.ts, unlocks.ts, grantLook, locks and try-on in the builder)
exactly as the 2026-10-13 spec describes them, with these changes: the first content batch is 45 class outfits instead
of 31 faces and 9 milestone pieces, and a new pure rule dresses the hero when class progress earns an outfit.
src/lib/hero-look/catalog.ts (pure), as in the Look Unlocks spec: PieceSlot, LookSource, LookPiece,
LOOK_CATALOG, pieceById, piecesForSlot. Two additions:
- A class outfit piece carries its own default colours:
colors?: { outfitColor: string; outfitAccent: string }. They are put on with the outfit the first time it is worn, and a hero who owns the outfit counts as owning those two colours (so the padlock never falls on the outfit's own colours). -
piecesForSlot("outfit")returns the eight starter outfits first, then the class outfits grouped by class inJOB_CLASS_OPTIONSorder and level order, so the builder can show headings per class.
OutfitStyle grows from 8 values to 53. A class outfit's value is <class-slug>-<level>: warrior-1, warrior-5,
warrior-10, white-mage-5, martial-artist-10. The piece id is outfit:<value>. Display names come from the
catalogue.
| Class | Lv 1 | Lv 5 | Lv 10 |
|---|---|---|---|
| Warrior | Leather Jerkin | Chainmail Hauberk | Battle Plate |
| Mage | Apprentice Robe | Star Robe | Archmage Mantle |
| Thief | Hooded Vest | Shadow Leathers | Phantom Cloak |
| Monk | Training Gi | Sash Robes | Iron Fist Vestments |
| Knight | Squire's Tabard | Knight's Mail | Crested Plate |
| Berserker | Fur Harness | Warpaint Hide | Wolfpelt Mantle |
| White Mage | Healer's Smock | White Robe | Dawn Vestments |
| Black Mage | Dark Tunic | Night Robe | Void Mantle |
| Assassin | Grey Wrap | Blade Leathers | Nightshade Cloak |
| Ranger | Scout's Jacket | Forest Mail | Hawkfeather Cloak |
| Martial Artist | Sparring Gi | Belted Gi | Dragon Gi |
| Mystic | Seer's Shawl | Moon Robe | Celestial Mantle |
| Paladin | Oath Tabard | Radiant Plate | Sunfire Regalia |
| Ninja | Shinobi Wrap | Shadow Shozoku | Storm Shozoku |
| Sage | Scholar's Robe | Sage's Vestments | Worldtree Mantle |
Each row's three outfits share the class's palette (the default colour pair) so a Warrior reads as a Warrior at every level, and differ in cut: level 1 is a simple two-colour garment; level 5 adds trim and one carried or worn piece (pauldrons, a sash, a belt pouch, a scarf); level 10 adds a large silhouette piece (a cape, a mantle, a crest, a long coat) that reads at a glance on the small hero. The names are the catalogue's; a builder may tune a name that does not fit its voxel look, but never the slot values.
src/lib/hero-look/unlocks.ts (pure), as in the Look Unlocks spec: LookHero, ownedPieces, isOwned,
lockedHint, newPieces, keepOwned. The class source reads stats.classes[<name>].level; the hero's current
jobClass counts as at least level 1, so picking a class owns its first outfit at once.
-
lockedHintfor a class piece:Learned as a Warrior (class level 5). - The saved look is grandfathered: a hero whose saved outfit is a class outfit they no longer "reach" (the record was edited by hand, say) keeps it.
- Store (
src/lib/store/stores/user/profileFields.ts):grantLook(userId, pieceId)andmarkLookSeen(userId, ids)as specified;lookUnlocksandlookSeenread as empty on old profiles. No class outfit is ever written tolookUnlocks; they are computed.
src/lib/hero-look/classOutfits.ts (pure):
interface ClassProgress { jobClass?: string; classLevels: Record<string, number> }
interface OutfitChange { piece: LookPiece; look: HeroLook } // the look with the new outfit and its colours
outfitOnClassProgress(before: ClassProgress, after: ClassProgress, look: HeroLook): OutfitChange | null
classOutfitFor(jobClass: string, classLevel: number): LookPiece | null // the highest earned outfit of that class-
outfitOnClassProgressanswers with the outfit to put on when: the class changed (the new class's highest earned outfit, level 1 at least); or the current class's level crossed 5 or 10 (that level's outfit). Any other change isnull. It never answers with an outfit the hero does not own. - The change writes
look.outfit,look.outfitColorandlook.outfitAccent(the piece's default pair). Hair, hat, face and skin are untouched. The replaced outfit stays owned. -
Where it runs. The user store's save path (
persistUser) compares the hero's class progress before and after a save, and when the rule answers it saves the new look in the same write. The plan confirms every write point of a class pick, aPlayerStatsServiceclass level-up and a class evolution goes through that path, and hooks any that does not. A brand-new profile gets its class's level 1 outfit as its starting outfit (the Gamer Card's builder shows it instead of the plain tunic). -
The fanfare. The piece is owned and not in
lookSeen, sonewPieceslists it. The hero's portal shell (MyPortal) runsnewPieceswhen it enters and, for an outfit piece the hero is wearing, showsXpHeroFanfarewith the hero in the new outfit and the piece's name ("Chainmail Hauberk earned"), then callsmarkLookSeen. A level-up a Game Master approved from the console is therefore celebrated the next time the hero's portal opens. Pieces earned that the hero is not wearing (a class outfit kept after a class change) only get the New badge.
A hero whose class levels were earned before this shipped owns their outfits at once (ownership is computed), but
outfitOnClassProgress only answers to a change, so nothing would dress them or say anything. One catch-up fixes
that: catchUpOutfit(hero) (in classOutfits.ts) answers the hero's current class's highest earned outfit when the
hero owns at least one class outfit, wears none of them, and lookSeen holds no class piece. The portal runs it once
on enter (Task 9's composable, before the fanfare check), the store writes the look, and the fanfare reads
" earned" with a second line "and N more in your wardrobe" when N > 0. Marking the worn piece seen ends the
catch-up for good; the other earned outfits keep their New badges in the builder. A hero who chose a starter outfit
on purpose is dressed once and can change back in the builder.
As in the Look Unlocks spec (hero prop, owned, newPieces, seen emit, dimmed padlocked tiles, the try-on banner
with lockedHint, Random and the starter lineup drawing from owned values, Done through keepOwned with the
"Not yours yet" alert). The Outfit tab shows the eight starter outfits under "Starter" and then one heading per class
through the tray's existing headings support, in catalogue order. Tile thumbnails come from useLookThumbnails.
src/components/organisms/HeroModel/voxel/outfits.ts keeps the eight starter outfits and its body() builder; its
specFor looks class outfits up in a map instead of growing its switch. The class outfits live in
voxel/classOutfits/:
-
parts.ts: the shared pieces (collar, cape, mantle, pauldrons, sash, skirt, belt, pouch, hood, scarf, crest), each a function of the body's measurements (NECK_Y,SHOULDER_Y,HIP_Y,ARM_X) and the two outfit colours. -
base.ts(Warrior, Mage, Thief, Monk),advancedA.ts(Knight, Berserker, White Mage, Black Mage),advancedB.ts(Assassin, Ranger, Martial Artist, Mystic),crown.ts(Paladin, Ninja, Sage): each exports its outfits' specs keyed byOutfitStylevalue. Each file stays under the 500-line budget. -
index.ts:CLASS_OUTFIT_SPECS: Record<OutfitStyle, OutfitSpecFn>for the 45 values.
Every outfit builds a hero that animates with the existing rig (the parts attach to the torso, arms and legs groups, so walk and victory still work), hides nothing of the head, and reads at the builder's tile size.
-
HeroLook.outfitaccepts the 45 new values;isHeroLookkeeps validating by shape, not by value. - Old saves carry no
lookUnlocksorlookSeen; they read as empty. A hero made before this change wears what they wore; their class's earned outfits simply appear owned in the builder. - Backups carry the look as part of the profile, as today. Nothing new to back up.
Jest (through chemx test):
- Catalogue integrity: 45 class outfit pieces, ids unique and well-formed, exactly one piece per
OutfitStylevalue, every class inJOB_CLASS_OPTIONShas levels 1, 5 and 10, every class piece has a colour pair. - Ownership: picking a class owns its level 1 outfit; class level 5 and 10 own theirs; a changed class keeps the old
class's earned outfits; the outfit's colour pair counts as owned;
lockedHintcopy; grandfathering. -
outfitOnClassProgress: class pick, class change, crossing 5 and 10, a level-up that crosses nothing, a level-up in a class that is not current (owned, not worn), never an unowned outfit. - Store: a save that changes class progress writes the new look; a new profile starts in its class's level 1 outfit.
- Portal: a worn, unseen outfit piece shows the fanfare once and marks it seen; an unworn one does not.
- Builder: headings per class, locked tiles dimmed and labelled, New badge, try-on banner, Done with a locked outfit returns it to the shelf.
- Voxel: every one of the 45 outfits builds a hero with the named groups and animates.
Cypress: mylook.spec.js gains one case: a Warrior at class level 5 sees Leather Jerkin and Chainmail Hauberk unlocked
and Battle Plate locked, tries on Battle Plate, Done, Save: the saved outfit is Chainmail Hauberk.
Gate: the PR's Tests check (lint, audit, typecheck, the related specs), and chemx build once at the end for the
.vue script types.
The 31 class faces and the 9 hero-level milestone pieces (the next content drop), more colours, rarity, shops, the barber, the merchant, temples, beasts, quests and seasons as sources, Game Master gift and reprice, unlock toasts outside the fanfare, selling or trading, and an inventory-style wardrobe screen.