2026 10 18 class outfits design - xopherdeep/do-it-for-the-xp GitHub Wiki

Class Outfits: the voxel hero dresses for the class it has earned (design)

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.

Why

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.

Decisions (owner, 2026-10-18)

  • 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.

What is built, in one line

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.

The catalogue

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 in JOB_CLASS_OPTIONS order 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.

The 45 outfits

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.

What a hero owns

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.

  • lockedHint for 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) and markLookSeen(userId, ids) as specified; lookUnlocks and lookSeen read as empty on old profiles. No class outfit is ever written to lookUnlocks; they are computed.

Wearing it at once

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
  • outfitOnClassProgress answers 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 is null. It never answers with an outfit the hero does not own.
  • The change writes look.outfit, look.outfitColor and look.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, a PlayerStatsService class 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, so newPieces lists it. The hero's portal shell (MyPortal) runs newPieces when it enters and, for an outfit piece the hero is wearing, shows XpHeroFanfare with the hero in the new outfit and the piece's name ("Chainmail Hauberk earned"), then calls markLookSeen. 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.

Heroes who already passed the levels (owner, 2026-10-19)

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.

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.

Voxel content

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 by OutfitStyle value. 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.

Data

  • HeroLook.outfit accepts the 45 new values; isHeroLook keeps validating by shape, not by value.
  • Old saves carry no lookUnlocks or lookSeen; 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.

Testing

Jest (through chemx test):

  • Catalogue integrity: 45 class outfit pieces, ids unique and well-formed, exactly one piece per OutfitStyle value, every class in JOB_CLASS_OPTIONS has 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; lockedHint copy; 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.

Out of scope

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.

⚠️ **GitHub.com Fallback** ⚠️