Repository navigation
Hero Look Contract
Goal: every hero has an Unrailed-style blocky voxel character they dress and customise. It lives on a new My Look screen reached from the profile, animates (idle breathing, wave, walk, victory, hurt, dance), is saved on the profile, and a rendered snapshot of it becomes the hero's avatar everywhere the app shows one.
Shared types (frozen): src/lib/hero-look/types.ts — HeroLook, HeroAnimation, DEFAULT_LOOK, SKIN_TONES, LOOK_COLORS, the *_STYLES lists.
Dependency: three 0.186 is installed (import * as THREE from "three"). Keep the bundle lean: import from "three" only (tree-shakes), no examples loaders, no postprocessing.
-
voxel/parts.ts: a part library. A part is a list of boxes{ x, y, z, w, h, d, color }in a 16-unit-tall character space (1 unit = 1 voxel, origin at the feet centre, +Y up, +Z toward the camera). Parts:body(skin, outfit, outfitColor, outfitAccent)(torso, arms as separate groups named "armL"/"armR", legs "legL"/"legR"),head(skin, face, eyeColor)(head block + eyes + mouth per FaceStyle as small voxels),hair(style, color)for every HairStyle,hat(style, color)for every HatStyle,held(iconName)(a small generic tool/sword/shield/wand/book shape chosen from the icon name, or null). Colours come from the look; outfits must look distinct from each other (overalls bib, robe is long and hides legs, armor has shoulder plates, hoodie has a hood, dress is a skirt, tracksuit has stripes, pyjamas are striped). -
voxel/build.ts:buildHero(look: HeroLook): THREE.Groupwith named child groups:head(includes hair, hat, face),torso,armL,armR,legL,legR,handL,handR. Use ONE mergedBufferGeometryper group built from box faces with vertex colours (MeshLambertMaterial({ vertexColors: true })), no per-voxel meshes, no textures. Add a flat dark disc shadow under the feet.disposeHero(group)frees geometries/materials. -
voxel/animate.ts:animateHero(group, animation: HeroAnimation, t: seconds)sets rotations/positions of the named groups procedurally: idle (breathing scale on torso, slight head bob, arms sway), wave (right arm up waving), walk (legs/arms swing, body bob), victory (hop + both arms up, repeat), hurt (lean back, flash: tint vertex colours is not needed, use a quick shake), dance (hip sway, arms alternate). Pure function of t, no stored state. -
useHeroScene.ts(canvasRef, look: Ref<HeroLook>, animation: Ref<HeroAnimation>, options?: { interactive?: boolean; background?: 'transparent' }): WebGLRenderer (alpha, antialias off on dpr>1.5, pixel ratio capped at 2), orthographic-ish PerspectiveCamera at a 3/4 front view, hemisphere + directional light with soft shadow OFF (keep it cheap), one rAF loop that pauses when the tab is hidden or the canvas is off-screen (IntersectionObserver), ResizeObserver sizing, drag/touch to turn the hero (y rotation with inertia) when interactive, rebuilds the hero whenlookchanges (dispose the old one),dispose()on unmount. Exposesnapshot(size = 256): stringthat renders one frame atsize×sizewith a transparent background into a data URL (PNG). -
XpHeroModel.vue: propslook: HeroLook,animation?: HeroAnimation = 'idle',interactive?: boolean = true,size?: number(CSS px, default fills parent). Renders the canvas, exposessnapshot()via defineExpose. No Ionic imports. - Tests
__tests__/voxel.spec.ts: buildHero returns the named groups for every hair/hat/outfit/face combination sample (loop over the style lists), each group has a geometry with vertex colours, disposeHero does not throw; animateHero mutates transforms differently for different animations and is deterministic for the same t. (jsdom has no WebGL: do NOT instantiate the renderer in tests; the scene hook is only type-checked.)
- Store:
useUserStore().setLook(userId, look: HeroLook, avatarImage?: string)writesusers[id].lookand, when given,users[id].avatarImage(a PNG data URL), syncs currentUser, persists.getLook(user)helper insrc/lib/hero-look/look.tsreturnsuser.lookwhenisHeroLook, elseDEFAULT_LOOK. - Avatar everywhere:
appConfig.$getUserAvatar(user)andXpFabUserHud.userAvatar,UserProfilehero avatar,BattleHeroCard/useBattlePlayer.avatarSrc,XpProfileCard,XpChatMessage,FamilySettingsand the SwitchProfile cards must preferuser.avatarImagewhen present and fall back to the SVG as today. Keep it to a small helperheroAvatarSrc(user, requireAvatar)insrc/lib/hero-look/look.tsand use it in those places. - My Look screen: route
my-lookunder the portal (/my-portal/:userId/my-look, see src/router/Console/my-portal.routes.ts), pagesrc/app/Console/MyPortal/UserProfile/MyLook/MyLook.vue(+ .ts/.scss): top half<XpHeroModel :look="draft" :animation="preview" interactive />on abg-slidebackground inside an eb-box, a row of animation chips (Idle, Wave, Walk, Dance, Victory) to preview, bottom half an RPG-styled tab strip (Body, Hair, Face, Outfit, Hat) with pickers: style grid (big buttons with the style name, Apple Kid font) and a colour grid (LOOK_COLORS swatches, SKIN_TONES for body). Buttons: Save look (callssetLook(userId, draft, await model.snapshot(256)), toast "Looking good!", playsyes), Reset (DEFAULT_LOOK). Unsaved changes stay in the draft until Save; leaving without saving discards. - Entry point: on the profile page (
UserProfile.vuehero section) add a button under the avatar: "My Look" (fad fa-tshirt) that routes tomy-look; the profile avatar shows the snapshot when present. - Hands: the look's
leftHand/rightHandmirroruser.equipment.leftHand[0]?.faIcon/ right at render time (compute in the page; do not store). - Tests:
tests/unit/HeroLook.spec.ts(setLook persists look and avatarImage through the store; getLook falls back to DEFAULT_LOOK; heroAvatarSrc prefers the image).
Edit only your files; the screen agent imports XpHeroModel from @/components/organisms/HeroModel/XpHeroModel.vue and treats the contract as given even before the file exists. pnpm typecheck clean, pnpm lint 0 errors, your spec green. No git, no dev server. Keep the Earthbound styling (eb-box mixin, Apple Kid / StatusPlz fonts, cream text) like the rest of the app.
- Architecture
- Component-Library
- Hooks-Reference
- Temple-System
- Battle-Field-Architecture
- current_context - For AI/Human Handoff
- User Menu
- General Settings
- Family Settings
- Sound Settings
- Notification Settings
- Reward Settings
- Party Settings
- Theme Settings
- Chore Settings