2026 10 19 class outfits - xopherdeep/do-it-for-the-xp GitHub Wiki
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking. In this repo the plan is also filed on the chemx board (epic + one task per plan task); builders read their task withchemx team task show <id>.
Goal: Every class dresses the voxel hero in its own outfit at class level 1, 5 and 10; the outfit goes on at once with a fanfare; the builder's Outfit tab becomes the wardrobe of everything a hero has earned.
Architecture: A pure catalogue of look pieces (catalog.ts) and a pure ownership rule (unlocks.ts) from the 2026-10-13 Look Unlocks spec, with 45 class outfits as the first content. A pure rule (classOutfits.ts) answers which outfit to put on when class progress changes; the user store calls it at its two class write points. The voxel outfits live in voxel/classOutfits/ and plug into the existing body() builder through a map. The builder learns ownership (locked tiles, try-on, New badges) and the portal shows the fanfare for a worn, unseen outfit.
Tech Stack: Vue 3 + Ionic 7 + Pinia, TypeScript, three.js voxel rig, jest through chemx test, Cypress 8 with --browser chrome.
Spec: docs/superpowers/specs/2026-10-18-class-outfits-design.md (builds on 2026-10-13-look-unlocks-design.md).
- Every rule is a pure module under
src/lib/hero-look/with a jest spec written first; screens stay thin. - Piece ids are
outfit:<class-slug>-<level>; class slugs are the class name lower-cased with spaces as hyphens (white-mage,martial-artist); levels are 1, 5 and 10. - The 45 names are the spec's table, verbatim. The 15 colour pairs (main, trim) are fixed below and all come from
LOOK_COLORS, so no new colour piece is needed. - Class outfits are computed from
jobClassandstats.classes[<name>].level, never written tolookUnlocks. The current class counts as level 1. Old profiles need no migration. - Skin is never locked. A saved look's pieces are always owned (grandfathering).
- Files stay under 500 lines; a function makes at most 5 direct
use*()calls; hook return functions carry a verb prefix; compound conditions are named; no em dashes anywhere. - Builders run only the spec they touch:
chemx test tests/unit/<Name>.spec.ts. No typecheck, verify or build mid-wave. - Copy is in-world: "Learned as a Warrior (class level 5)", "Not yours yet", " earned".
Colour pairs (outfitColor, outfitAccent): Warrior #b8342b/#8d8d8d; Mage #2f5fb3/#e8b22b; Thief #2b2b3a/#8d8d8d; Monk #e3732c/#e8d5a8; Knight #8d8d8d/#2f5fb3; Berserker #5a3a1e/#b8342b; White Mage #f5f5f5/#b8342b; Black Mage #1f1f1f/#6b3fa0; Assassin #2b2b3a/#c54a8a; Ranger #3f7d4e/#5a3a1e; Martial Artist #f5f5f5/#1f1f1f; Mystic #6b3fa0/#2b8a8a; Paladin #e8d5a8/#e8b22b; Ninja #1f1f1f/#b8342b; Sage #2b8a8a/#e8d5a8.
- A profile whose
stats.classesis missing, null or holds a non-numeric level: ownership returns the starter set plus the current class's level 1 outfit and never throws (Task 2 test). - A saved look whose
outfitis a class value the hero no longer reaches (class changed, record edited): it stays owned and worn (Task 2 test). - A class level that jumps past two thresholds in one save (4 to 10): the level 10 outfit goes on, both are owned (Task 3 test).
- A class change to a class whose outfits were already earned and seen: the outfit goes on, no second fanfare (Task 3 and Task 9 tests).
- The builder opened for a brand-new profile that has no id yet (the Gamer Card): starter pieces plus the class's level 1 outfit are owned,
seenis ignored, Done never shows "Not yours yet" (Task 8 test).
Files:
- Modify:
src/lib/hero-look/types.ts(OutfitStyle, OUTFIT_STYLES) - Create:
src/lib/hero-look/catalog.ts - Test:
tests/unit/lookCatalog.spec.ts
Interfaces:
-
Consumes:
JOB_CLASS_OPTIONS(src/constants),LOOK_COLORS,OUTFIT_STYLES. -
Produces (used by Tasks 2, 3, 5 to 9):
export type PieceSlot = "hair" | "face" | "outfit" | "hat" | "color"; export type LookSource = { kind: "starter" } | { kind: "class"; jobClass: string; level: 1 | 5 | 10 } | { kind: "gift" }; export interface LookPiece { id: string; slot: PieceSlot; value: string; name: string; source: LookSource; colors?: { outfitColor: string; outfitAccent: string } } export const STARTER_OUTFIT_STYLES: OutfitStyle[] // the eight that exist today export const CLASS_OUTFIT_STYLES: OutfitStyle[] // the 45 new values, class order then level order export const LOOK_CATALOG: readonly LookPiece[] export const classSlug = (jobClass: string): string export const pieceById = (id: string): LookPiece | undefined export const piecesForSlot = (slot: PieceSlot): LookPiece[] // starter first, then class pieces grouped by class export const classOutfitPieces = (jobClass: string): LookPiece[] // the class's three, level order
-
OutfitStylebecomesStarterOutfitStyle | ClassOutfitStyle, whereClassOutfitStyleis the template-literal union over the 15 slugs and the three levels (${ClassSlug}-${1 | 5 | 10}).OUTFIT_STYLESstays the eight starters (the builder's starter row);CLASS_OUTFIT_STYLESis new. -
Step 1: Write the failing tests in
tests/unit/lookCatalog.spec.ts:has one outfit piece per OutfitStyle value and 53 outfit pieces in all-
gives every class in JOB_CLASS_OPTIONS three pieces at levels 1, 5 and 10 with the spec's names(assert the Warrior row: Leather Jerkin, Chainmail Hauberk, Battle Plate; the Sage row: Scholar's Robe, Sage's Vestments, Worldtree Mantle; and thatclassOutfitPieces(name).length === 3for everyJOB_CLASS_OPTIONS[i].name) -
ids are outfit:<slug>-<level> and unique(outfit:white-mage-5,outfit:martial-artist-10) every class piece carries a colour pair from LOOK_COLORSpiecesForSlot("outfit") lists the eight starters first, then classes in JOB_CLASS_OPTIONS orderpieceById returns undefined for an unknown id
-
Step 2: Run
chemx test tests/unit/lookCatalog.spec.ts. Expected: FAIL, module not found. -
Step 3: Implement
types.ts(the unions,OUTFIT_STYLESunchanged,CLASS_OUTFIT_STYLESexported fromcatalog.ts, not fromtypes.ts, to avoid a cycle) andcatalog.ts: aCLASS_OUTFIT_NAMES: Record<ClassSlug, [string, string, string]>table andCLASS_OUTFIT_COLORS: Record<ClassSlug, { outfitColor: string; outfitAccent: string }>from the constraints,LOOK_CATALOGbuilt from them plus the starter pieces (names: Tunic, Overalls, Armor, Robe, Hoodie, Dress, Tracksuit, Pyjamas; starter hair, face, hat and colour pieces as in the Look Unlocks spec withsource: { kind: "starter" }). -
Step 4: Run the spec. Expected: PASS. Also run
chemx test tests/unit/HeroLook.spec.tsandchemx test src/components/organisms/HeroModel/__tests__/voxel.spec.ts(the type widening must not break them). -
Step 5: Report on the board task; no commit (Fable commits).
Files:
- Create:
src/lib/hero-look/unlocks.ts - Test:
tests/unit/lookUnlocks.spec.ts
Interfaces:
-
Consumes: Task 1 (
LOOK_CATALOG,pieceById,classSlug),getLook(look.ts),DEFAULT_LOOK. -
Produces (used by Tasks 3, 8, 9):
export interface LookHero { id?: string; look?: unknown; jobClass?: string; stats?: { classes?: Record<string, { level?: unknown }> }; lookUnlocks?: string[]; lookSeen?: string[] } export const classLevelsOf = (hero: LookHero | null | undefined): Record<string, number> // jobClass counts as at least 1 export const ownedPieces = (hero: LookHero | null | undefined): ReadonlySet<string> export const isOwned = (owned: ReadonlySet<string>, slot: PieceSlot, value: string): boolean // skin always true export const lockedHint = (piece: LookPiece): string // class: "Learned as a Warrior (class level 5)"; gift: "A gift from the Game Master"; else "Not unlocked yet" export const newPieces = (hero: LookHero | null | undefined): ReadonlySet<string> // owned, not starter, not in the saved look, not in lookSeen export const keepOwned = (draft: HeroLook, fallback: HeroLook, owned: ReadonlySet<string>): { look: HeroLook; returned: LookPiece[] }
-
Step 1: Write the failing tests in
tests/unit/lookUnlocks.spec.ts:an empty hero owns the starter set only-
the current class counts as level 1({ jobClass: "Warrior" }ownsoutfit:warrior-1, notoutfit:warrior-5) class level 5 and 10 own their outfits; another class's stay locked-
a changed class keeps the old class's earned outfits(jobClass: "Mage",stats.classes.Warrior.level = 10owns all three Warrior pieces) -
malformed classes never throw(stats: { classes: null },{ Warrior: { level: "x" } }) -
a saved look's class outfit is owned even when the class level is not reached(Review Focus 2) stored lookUnlocks are owned and unknown ids are ignoredskin is always owned-
lockedHint copyfor a class piece newPieces excludes starter, grandfathered and seen pieceskeepOwned returns locked slots to the fallback and lists them in slot order
-
Step 2: Run
chemx test tests/unit/lookUnlocks.spec.ts. Expected: FAIL. -
Step 3: Implement
unlocks.tswith the signatures above.keepOwnedchecks hair, face, outfit, hat and the four colour keys the way the Look Unlocks spec says. -
Step 4: Run the spec. Expected: PASS.
-
Step 5: Report on the board task.
Files:
- Create:
src/lib/hero-look/classOutfits.ts - Test:
tests/unit/classOutfits.spec.ts
Interfaces:
-
Consumes: Task 1 (
classOutfitPieces,pieceById), Task 2 (classLevelsOf,ownedPieces). -
Produces (used by Tasks 4 and 9):
export interface ClassProgress { jobClass?: string; classLevels: Record<string, number> } export const classProgressOf = (hero: LookHero | null | undefined): ClassProgress export const classOutfitFor = (jobClass: string | undefined, classLevel: number): LookPiece | null // highest earned of that class export interface OutfitChange { piece: LookPiece; look: HeroLook } export const outfitOnClassProgress = (before: ClassProgress, after: ClassProgress, look: HeroLook): OutfitChange | null export const startingLookFor = (jobClass: string | undefined, base: HeroLook = DEFAULT_LOOK): HeroLook // the class's level 1 outfit and colours export const catchUpOutfit = (hero: LookHero | null | undefined): OutfitChange | null // heroes who passed the levels before this shipped (spec section)
-
Step 1: Write the failing tests:
-
picking a class puts on its level 1 outfit with the class colours(before{ classLevels: {} }, after{ jobClass: "Mage", classLevels: { Mage: 1 } }givesoutfit: "mage-1",outfitColor: "#2f5fb3",outfitAccent: "#e8b22b") crossing class level 5 puts on the level 5 outfit; crossing 10 the level 10 one-
a jump from 4 to 10 puts on the level 10 outfit(Review Focus 3) -
a level-up that crosses nothing answers null(2 to 3) -
a level-up in a class that is not current answers null(owned, not worn) changing to a class already at level 5 puts on that class's level 5 outfitthe same progress twice answers nullnever answers an outfit the hero does not ownstartingLookFor("Warrior") wears outfit:warrior-1 and keeps hair, face and skin from the basecatchUpOutfit dresses a Warrior 10 who wears a tunic and has seen no class piece in outfit:warrior-10catchUpOutfit answers null once a class piece is seen, or when a class outfit is already worn, or when nothing is owned
-
-
Step 2: Run
chemx test tests/unit/classOutfits.spec.ts. Expected: FAIL. -
Step 3: Implement
classOutfits.ts. The rule: ifafter.jobClass !== before.jobClassthe piece isclassOutfitFor(after.jobClass, after.classLevels[after.jobClass] ?? 1); else if the current class's level crossed 5 or 10 the piece isclassOutfitFor(after.jobClass, newLevel); a piece equal tolook.outfitanswers null. -
Step 4: Run the spec. Expected: PASS.
-
Step 5: Report on the board task.
Files:
- Modify:
src/lib/store/stores/user/profileFields.ts(createProfileSettersgainsgrantLook,markLookSeen) - Modify:
src/lib/store/stores/user.ts(setJobClass,updateClassXpdress the hero; both export the new setters) - Modify:
src/app/SideMenu/SwitchProfile/AddProfile/AddProfile.ts:264and:375(a new profile starts instartingLookFor(jobClass)when it has no look of its own) - Test:
tests/unit/HeroLook.spec.ts(extend; it runs the real store with ProfileDb faked), the AddProfile spec found withchemx q AddProfile -lin tests/unit (extend if it exists)
Interfaces:
-
Consumes: Task 3 (
classProgressOf,outfitOnClassProgress,startingLookFor), Task 1 (pieceById). -
Produces:
userStore.grantLook(userId: string, pieceId: string): boolean,userStore.markLookSeen(userId: string, pieceIds: readonly string[]): void,userStore.dressHero(userId: string, look: HeroLook): void(writes the look and persists; the catch-up and nothing else calls it). -
Step 1: Write the failing tests in
tests/unit/HeroLook.spec.ts:setJobClass dresses the hero in the new class's outfit and persists the lookupdateClassXp that crosses class level 5 dresses the hero in the level 5 outfitupdateClassXp that crosses nothing leaves the look alonegrantLook adds a catalogue piece once and refuses unknown or owned idsmarkLookSeen persists lookSeen- and in the AddProfile spec:
a new Warrior starts in outfit:warrior-1
-
Step 2: Run
chemx test tests/unit/HeroLook.spec.ts. Expected: FAIL. -
Step 3: Implement. In
user.ts, a privatedressForClassProgress(userId, before: ClassProgress)called at the end ofsetJobClassand insideupdateClassXpafterapplyClassXp, beforepersistUser: computesafter = classProgressOf(users[userId]), and whenoutfitOnClassProgress(before, after, getLook(users[userId]))answers, writesusers[userId].look = change.look(through the existingsetLookpath if one exists, else directly, thensyncCurrentUser). InprofileFields.tsthe two setters follow the pattern of the setters already there (write the field,persistUser(userId)). -
Step 4: Run the specs. Expected: PASS. Also
chemx test tests/unit/PlayerStats.spec.tsif it exists (class XP paths). -
Step 5: Report on the board task.
Files:
- Create:
src/components/organisms/HeroModel/voxel/classOutfits/parts.ts - Create:
src/components/organisms/HeroModel/voxel/classOutfits/base.ts(Warrior, Mage, Thief, Monk: 12 outfits) - Modify:
src/components/organisms/HeroModel/voxel/outfits.ts(exportOutfitSpec,arm,leg,footwear,LEG_X0,LEG_X1, and theslicehelper the armor uses, so the class files reuse them) - Test:
src/components/organisms/HeroModel/__tests__/classOutfits.spec.ts
Interfaces:
-
Consumes:
box,mirrorX,shade,lighten,mix,PALETTE(voxel/shape.ts),NECK_Y,SHOULDER_Y,HIP_Y,ARM_X(voxel/outfits.ts). -
Produces (used by Tasks 6 and 7):
// parts.ts: each takes the two outfit colours and returns boxes placed on the torso unless named otherwise export const collar = (C: string, A: string): Box[] export const cape = (C: string, A: string, length: "short" | "long"): Box[] export const mantle = (C: string, A: string): Box[] export const pauldrons = (C: string, A: string): Box[] // boxes for the left arm; the caller mirrors export const sash = (C: string, A: string): Box[] export const skirt = (C: string, A: string, length: number): Box[] // boxes for the left leg export const belt = (C: string, A: string): Box[] export const pouch = (C: string, A: string): Box[] export const hood = (C: string, A: string): Box[] // headGear export const scarf = (C: string, A: string): Box[] export const crest = (C: string, A: string): Box[] // headGear, above the collar // base.ts export type OutfitSpecFn = (skin: string, C: string, A: string) => OutfitSpec export const BASE_OUTFITS: Partial<Record<ClassOutfitStyle, OutfitSpecFn>> // the 12 base-class values; index.ts merges the maps into the exhaustive one
-
Step 1: Write the failing tests in
__tests__/classOutfits.spec.ts(followvoxel.spec.ts:look({ outfit }),buildHero,expectWellFormed,animateHeroonwalkandvictory):-
every base class outfit builds a well-formed hero and animates(loop the 12 values) -
level 5 and 10 outfits carry more boxes than level 1(torso box count rises per level, so each level visibly adds) -
a level 10 outfit adds nothing above the neck except headGear(hair stays visible: no torso box with y > NECK_Y)
-
-
Step 2: Run
chemx test src/components/organisms/HeroModel/__tests__/classOutfits.spec.ts. Expected: FAIL. -
Step 3: Implement
parts.tsandbase.ts. Direction from the spec: level 1 a simple two-colour cut; level 5 adds trim and one carried or worn piece; level 10 adds a large silhouette piece (cape, mantle, crest, long coat). Warrior: jerkin / hauberk with pauldrons / plate with cape. Mage: plain robe / star robe with sash / mantle with long cape. Thief: vest with hood / leathers with belt and pouch / cloak (long cape, hood). Monk: gi with belt / sash robes / vestments with scarf and mantle. Keep each file under 500 lines; the map is the only export besides the spec functions. -
Step 4: Run the spec. Expected: PASS.
-
Step 5: Report on the board task.
Files:
- Create:
src/components/organisms/HeroModel/voxel/classOutfits/advancedA.ts(Knight, Berserker, White Mage, Black Mage) - Create:
src/components/organisms/HeroModel/voxel/classOutfits/advancedB.ts(Assassin, Ranger, Martial Artist, Mystic) - Test:
src/components/organisms/HeroModel/__tests__/classOutfits.spec.ts(extend)
Interfaces:
-
Consumes: Task 5 (
parts.ts,OutfitSpecFn, the helpers exported fromoutfits.ts). -
Produces:
ADVANCED_A_OUTFITS,ADVANCED_B_OUTFITS(same shape asBASE_OUTFITS, 12 each). -
Step 1: Extend the tests: the three Task 5 cases loop all 36 values from base and both advanced maps.
-
Step 2: Run the spec. Expected: FAIL (the maps do not exist).
-
Step 3: Implement the two files. Knight: tabard / mail with pauldrons / crested plate (crest headGear, cape). Berserker: harness / warpaint hide with belt / wolfpelt mantle. White Mage: smock / white robe with sash / dawn vestments (mantle, collar). Black Mage: dark tunic / night robe with hood / void mantle (long cape, hood). Assassin: wrap with scarf / leathers with belt and pouch / nightshade cloak. Ranger: jacket / forest mail with belt / hawkfeather cloak (short cape, collar). Martial Artist: gi / belted gi with sash / dragon gi (mantle, sash). Mystic: shawl / moon robe / celestial mantle (mantle, long cape).
-
Step 4: Run the spec. Expected: PASS.
-
Step 5: Report on the board task.
Files:
- Create:
src/components/organisms/HeroModel/voxel/classOutfits/crown.ts(Paladin, Ninja, Sage: 9 outfits) - Create:
src/components/organisms/HeroModel/voxel/classOutfits/index.ts - Modify:
src/components/organisms/HeroModel/voxel/outfits.ts:233(specForlooks a class outfit up in the map before its switch) - Test:
src/components/organisms/HeroModel/__tests__/classOutfits.spec.ts(extend),src/components/organisms/HeroModel/__tests__/voxel.spec.ts(run)
Interfaces:
-
Consumes: Tasks 5 and 6.
-
Produces:
CLASS_OUTFIT_SPECS: Record<ClassOutfitStyle, OutfitSpecFn>(index.ts);body(skin, "<class>-<level>", C, A)builds the class outfit. -
Step 1: Extend the tests:
CLASS_OUTFIT_SPECS has exactly the 45 CLASS_OUTFIT_STYLES keys; the three cases loop all 45;body("peach", "paladin-10", C, A) builds the Paladin plate(named groups present). -
Step 2: Run the spec. Expected: FAIL.
-
Step 3: Implement
crown.ts(Paladin: oath tabard / radiant plate with pauldrons / sunfire regalia (mantle, cape, crest). Ninja: shinobi wrap with scarf / shadow shozoku with belt and hood / storm shozoku (long cape, hood). Sage: scholar's robe / sage's vestments with sash / worldtree mantle (mantle, long cape)),index.ts(the merged map), and inspecFor:const classSpec = CLASS_OUTFIT_SPECS[outfit as ClassOutfitStyle]; if (classSpec) return classSpec(skin, C, A);before the switch. -
Step 4: Run both specs. Expected: PASS.
-
Step 5: Report on the board task.
Files:
- Modify:
src/components/organisms/HeroModel/lookEditor.ts(the outfit tab's options and headings from the catalogue;randomLook(rng, owned?),starterLineup(count, rng, owned?);optionLabelreads catalogue names) - Modify:
src/components/organisms/HeroModel/builder/XpBuilderTray.vue(locked tiles, New badge,seenemit) - Modify:
src/components/organisms/HeroModel/XpHeroBuilder.vueandXpHeroBuilderModal.vue(heroprop,owned,newPieces, try-on banner, Done throughkeepOwned, the "Not yours yet" alert,seenemit) - Test:
tests/unit/lookEditor.spec.ts(find the existing builder specs withchemx q lookEditor -landchemx q XpBuilderTray -lin tests/unit; extend them)
Interfaces:
-
Consumes: Tasks 1 and 2.
-
Produces:
XpHeroBuilderModalandXpHeroBuildertakehero?: LookHero | nulland emitseen(pieceIds: string[]);XpBuilderTraytakesowned: ReadonlySet<string>andfresh: ReadonlySet<string>and emitsseen(pieceId); tiles carrydata-cy="look-tile",data-option,data-locked="true|false",data-new="true|false"; the stage banner carriesdata-cy="try-on-banner"; the alert's header is "Not yours yet". -
Step 1: Write the failing tests:
- lookEditor:
the outfit tab lists the eight starters under Starter and then one heading per class in JOB_CLASS_OPTIONS order;optionLabel("outfit", "warrior-5") is Chainmail Hauberk;randomLook with an owned set never picks a locked value(100 draws);starterLineup with an owned set never picks a locked value - tray:
a locked tile is dimmed, carries data-locked and an aria-label ending in ", locked";a New tile shows the badge until tapped, then emits seen - builder:
a draft holding a locked outfit shows the try-on banner with lockedHint;Done with a locked outfit shows Not yours yet; Save keeps the owned pieces and emits save with the fallback outfit;a hero with no id owns the starter set and its class's level 1 outfit; Done never shows the alert for them(Review Focus 5)
- lookEditor:
-
Step 2: Run the specs. Expected: FAIL.
-
Step 3: Implement.
lookEditor.ts: the outfit tab'sstyles.optionsbecomes[...OUTFIT_STYLES, ...CLASS_OUTFIT_STYLES]withheadingsbuilt frompiecesForSlot("outfit")({ tunic: "Starter", "warrior-1": "Warrior", "mage-1": "Mage", ... }). The builder computesowned = ownedPieces(hero)andfresh = newPieces(hero)once per open; the tray renders a lock glyph (fad fa-lock) and a "New" chip from them; the thumbnailer is unchanged but the class tiles must render lazily (only tiles scrolled into view ask for a thumbnail) if the tray does not already do so, so 53 tiles do not queue 53 renders at once. -
Step 4: Run the specs. Expected: PASS. Also
chemx test tests/unit/HeroLook.spec.ts. -
Step 5: Report on the board task.
Files:
- Modify:
src/app/SideMenu/SwitchProfile/AddProfile/GamerCard.vue,src/app/Console/MyPortal/UserHud/components/UserProfileModal.vue,src/app/Console/MyPortal/UserProfile/UserProfile.ts(passhero, handleseenwithuserStore.markLookSeen; the Gamer Card ignoresseenfor a profile not yet saved) - Create:
src/app/Console/MyPortal/useOutfitFanfare.ts - Create:
src/lib/hero-look/outfitFanfare.ts - Modify:
src/app/Console/MyPortal/MyPortal.tsandMyPortal.vue(oneuse*call:useOutfitFanfare, renderingXpHeroFanfare) - Test:
tests/unit/outfitFanfare.spec.ts, the MyPortal spec found withchemx q MyPortal -l(extend if it exists)
Interfaces:
-
Consumes: Task 2 (
newPieces), Task 1 (pieceById), Task 4 (markLookSeen),XpHeroFanfare(open,name,jobClass,title,look,actionText). -
Produces:
// outfitFanfare.ts (pure) export const outfitFanfareFor = (hero: LookHero | null | undefined): LookPiece | null // the worn, unseen class outfit, else null export const wardrobeCountFor = (hero: LookHero | null | undefined, worn: LookPiece): number // other owned class outfits, for "and N more in your wardrobe" // useOutfitFanfare.ts export function useOutfitFanfare(heroId: () => string): { fanfare: Ref<{ piece: LookPiece; look: HeroLook } | null>; dismissFanfare: () => void }
-
Step 1: Write the failing tests:
outfitFanfareFor answers the worn outfit when it is owned and unseenanswers null when the worn outfit is seen, starter, or not a class piece-
answers null for an owned but unworn class outfit(Review Focus 4) useOutfitFanfare opens once on enter and marks the piece seen on dismisson enter, a hero catchUpOutfit answers for is dressed through the store first, then the fanfare opens with "and N more in your wardrobe"
-
Step 2: Run
chemx test tests/unit/outfitFanfare.spec.ts. Expected: FAIL. -
Step 3: Implement. The composable reads the hero on
onIonViewDidEnter(and on mount); ifcatchUpOutfit(hero)answers it calls a store actiondressHero(heroId, look)(Task 4 adds it beside grantLook: writes the look and persists) and re-reads the hero; then it setsfanfarefromoutfitFanfareFor, anddismissFanfarecallsmarkLookSeen(heroId(), [piece.id]).MyPortal.vuerenders<xp-hero-fanfare :open="Boolean(fanfare)" :name="fanfare?.piece.name" :job-class="user.jobClass" title="New outfit earned" action-text="Wear it" :look="fanfare?.look" @close="dismissFanfare" />(match the fanfare's real close event name; read the component). -
Step 4: Run the specs. Expected: PASS.
-
Step 5: Report on the board task.
Files:
-
Modify:
tests/e2e/specs/mylook.spec.js(one new case) -
Modify:
tests/e2e/fixtures/battle-backup.jsononly if the seeded hero needs class levels (prefer setting them throughwin.__xpseams if one exists for stats; otherwise extend the fixture's hero-1 withstats.classes.Warrior.level = 5andjobClass: "Warrior") -
Modify:
src/app/SideMenu/XpSupport/helpContent.ts(one entry: "How do I get new outfits?") -
Test:
tests/unit/helpContent.spec.ts(run) -
Step 1: Write the Cypress case: a Warrior at class level 5 opens My Look;
[data-cy=look-tile][data-option=warrior-1]andwarrior-5havedata-locked="false",warrior-10hasdata-locked="true"; tappingwarrior-10shows[data-cy=try-on-banner]containing "Learned as a Warrior (class level 10)"; Done shows the "Not yours yet" alert; Save;win.__xp.getUser("hero-1").look.outfitiswarrior-5. -
Step 2: Run it:
pnpm vue-cli-service test:e2e --headless --browser chrome --url http://localhost:8100 --spec tests/e2e/specs/mylook.spec.js(the dev server on :8100 must already be running; never start a second). Expected: PASS. -
Step 3: Add the help entry in the hero's section of
helpContent.ts: picking a class dresses you in its outfit; class level 5 and 10 earn the next two; everything you have earned stays in the builder's Outfit tab; a locked one can be tried on but goes back on the shelf. Runchemx test tests/unit/helpContent.spec.ts. -
Step 4: Report on the board task.
- Wave 1 (parallel): Task 1, Task 5 (parts and base; it can start from the spec's names before Task 1 lands because it keys on string values).
- Wave 2 (parallel, after Task 1): Task 2, Task 6, Task 7 (Task 7 waits for 5 and 6).
- Wave 3 (after 2): Task 3, then Task 4; Task 8 (after 1, 2, 7).
- Wave 4 (after 4 and 8): Task 9, Task 10.
- One
chemx typecheck,chemx lintand production build after the last wave, then the PR.