2026-04-15

Pixel layer v1 shipped — member portrait on dashboard

First pixel-art render landed in Guild. feat/pixel-portrait-mvp branch pushed. Commit f173d0d. PR URL to open when ready: https://github.com/dungeonbooks/guild/pull/new/feat/pixel-portrait-mvp

What shipped

  • PixiJS v8 + @pixi/react v8 installed and integrated in the Next.js 16 / React 19 app
  • 115 Mini Medieval 8×8 portraits (VEXED, CC-BY 4.0) copied to public/pixel/portraits/
  • src/lib/pixel/portrait-map.ts — class → portrait mapping for 5 Guild classes
  • src/components/member-portrait.tsx — client-only PixiJS renderer with nearest-neighbor scaling
  • src/components/member-portrait-client.tsx — dynamic-import wrapper (ssr: false) to isolate PixiJS from SSR boundary
  • Dashboard character header — replaces the SwordIcon Avatar fallback with the member’s class portrait at 96×96

Class → portrait mapping locked (v1)

ClassDefault spriteRationale
Freelancersprite-16-5Plain male commoner, no hat; default unassigned wanderer
Warriorsprite-13-2Helmed unisex sword knight
Magesprite-12-1Green-hat wizard with staff
Healersprite-10-1Grey-haired queen; crown reads as holy/light
Roguesprite-15-1Wide-brim hat obscuring face + blade

Design primitives established with future semantic weight

  • Rows 16-1 to 16-4 and 17-1 to 17-4 (hatted commoners) reserved for staff. “Hats = staff” becomes a clean in-game signal for Panat / Carrie / future staff without needing a separate visual treatment.
  • Rows 5-10 (kings and queens, 42 portraits) parked for name-level treatment. Level 9 members get a crowned portrait upgrade when we ship the name-level ritual from name-level.
  • Rows 11 (archers), 14 (spear knights), 18-19 (commoners with tools) reserved. Future classes, Warrior subclass variants, or earned-achievement visual upgrades.

Technical notes worth preserving

  • PixiJS needs dynamic import with ssr: false — touches browser globals at module load. Next 15+ forbids dynamic({ ssr: false }) inside server components, so a client wrapper (member-portrait-client.tsx) is the required boundary.
  • Cold-cache load takes ~1 second (pixi.js bundle + dynamic chunk download). Subsequent loads are fast via browser cache. Acceptable for MVP; if it becomes user-visible pain, options are: preload the chunk, better loading placeholder, or swap static sprites to <img> + image-rendering: pixelated and reserve PixiJS for animated/dynamic surfaces.
  • extend({ Container, Sprite }) at module scope, JSX elements use lowercase tag names (<pixiSprite>). v8 extend API convention.
  • Pre-commit hook caught stale .next/dev/types/validator.ts (auto-generated Next.js file that got corrupted mid-dev-server). rm -rf .next resolved. Not a real issue with our code.

Non-obvious decisions that shaped this ship

Long stack-choice debate landed on PixiJS + @pixi/react after working through Phaser, Excalibur, Defold, Bevy, macroquad, Godot, Haxe/Heaps, bracket-lib, rot.js, malwoden. Decision notes in game-engine-choice. Key reason: @pixi/react is a React reconciler — pixi-rendered elements compose with React DOM at any level of the tree, which matches the “wizard shopping on eBay” vision where pixel sprites bleed into dashboards, headers, cursors, etc.

Scope reduction before ship: earlier iteration was a full CRPG character screen (paper doll + equipment slots + inventory grid + stats panel) estimated at 2-3 weeks. Panat pulled this back to “just show the member’s class as a pixel sprite” — 1 ship day. The imagination-space argument: 8×8 sprites are evocative precisely because they’re under-defined; members project identity onto them.

Mini Medieval over Demonic Dungeon: Carrie flagged Demonic Dungeon’s Mystic-16 palette as too hell-like for a cozy indie bookshop. Fruitpunch24 palette is warmer, more nostalgic, NES-era register. See guild-1bit-aesthetic v1-spine section.

Creative direction now anchored to “fantasy internet” / “wizard shopping on eBay”

Today’s conversation crystallized the broader aesthetic: not just “pixel art on the site” but ambient fantasy-pixel layer across the whole app, including web-platform infrastructure metaphors (loading = spellcasting, cart = goblin satchel, search = gremlin-fetched). Treatment is genuinely magical, not ironic — Neopets believed in Neopia, that’s the tonal reference.

Captured in guild-1bit-aesthetic (visual) and rpg-loyalty-system-creative-direction “Fantasy Internet” section (voice/metaphors).

MUD lineage note written

Also captured today: mud-lineage — Guild as next step in the MUDs → DeviousMUD → RuneScape genealogy. Four MUD bones (persistent world, progression, economy, community) + the commerce flip (game actions → real goods instead of real money → in-game power) + why this is structurally un-extractive. This is a founding-principle-level framing.

Next steps for the pixel layer

Priority order, each shippable independently.

Short list

  • Member portrait picker — let members choose their preferred portrait from within their class row. Needs a portraitSprite field on Members (nullable, string). portraitSrcFor prefers member pick, falls back to class default. Settings page UI to browse + pick. Clearest user value of the near-term options.
  • Idle animation (2-frame breath/bob) — first real PixiJS animation. Exercises useTick hook. Respects prefers-reduced-motion. Defers to pixel-layer-v1 step 4.
  • Leaderboard + activity feed integration — same portrait primitive reused next to each member’s name in Guild Hall leaderboard and the activity feed. Highest leverage-per-effort since the primitive now exists; just a lookup + MemberPortraitClient render in two more places.
  • Staff portrait treatment — when a member has staff role, pull from the hatted-commoner pool (rows 16-1 to 16-4 / 17-1 to 17-4). Free visual differentiation.

Medium-term (after short list ships)

  • Cold-load perf polish — preload the pixi chunk or show a better loading placeholder (pixel-register skeleton instead of muted-gray pulse)
  • Name-level (level 9) portrait upgrade — member’s portrait becomes a crowned king/queen from rows 5-10 when they reach level 9
  • Achievement-based portrait unlocks — earning certain achievements unlocks additional portraits from the reserved rows

Parked (bigger bets, after checkpoint on MVP signal)

  • Ambient pixel layer elsewhere in the portal — header sprite, cursor trail, or loading sigil — to validate the treatment outside the character page
  • Shop interior top-down scene using Mini Medieval Kingdom Interior — Phase 2 from guild-1bit-aesthetic
  • Fantasy-internet metaphors on web platform primitives (loading = spellcasting, error = scrying bowl cracked, etc.)

Open questions to decide before next ship

  • After member picker ships: collect a season’s worth of member picks and see if certain portraits are heavily favored → tells us which assets are working and which to retire
  • Portrait size at 96×96 works on dashboard — verify at other surfaces (leaderboard may need 32×32 or 48×48, activity feed may need 24×24)
  • Staff-pool assignment: deterministic by staff user ID (so Panat always gets the same hatted variant) or staff-pickable like members?