feat: buddy color progression (species x rarity x XP)#126
Conversation
Brainstormed three-input color model (species × rarity × XP) that replaces the current single-MAGENTA sprite color with a continuous gradient through 21 species palettes (84 colors) and 5 rarity-metal tiers (10 colors). Rarity uses a tier-break ladder (Iron/Copper/Gold/Diamond/Aurum) so "rare" is visibly rare. Saturation tint and bold weight at Rare+ make rarity readable from Lv 1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spec self-review pass: tighten three ambiguities. - Saturation tint applies uniformly to species AND metal segments (intentional). - Buddy's user-given name stays CYAN; only sprite-art colors change. - Acknowledge Lv 30-40 bridge can look muddy for some (species, rarity) combos; flag for per-species tuning post-launch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
18-task plan covering: new src/lib/color.ts module (palettes, capability detection, RGB math), integration into statusline-wrapper.ts and card.ts, unit + snapshot + integration tests, manual verification across truecolor / 256-color / 16-color / NO_COLOR modes. Each task is TDD-disciplined (test → fail → impl → pass → commit) with exact file paths, commands, and code shown inline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the design spec and implementation plan files that were used as working artifacts during development. They've served their purpose and the code is now the source of truth. Also strip the now-stale spec reference comment from src/lib/color.ts.
There was a problem hiding this comment.
Pull request overview
Introduces a new buddy sprite color system that evolves based on (species, rarity, totalXp) and wires it into terminal rendering (statusline + card) and share HTML output.
Changes:
- Added
src/lib/color.tswith palette/rarity anchoring, XP-driven interpolation, and terminal capability detection (truecolor/256/16/NO_COLOR). - Updated statusline wrapper and card rendering to use
colorFor(...)instead of a single hard-coded sprite color. - Updated share HTML rendering to style the sprite placeholder with an inline
rgb(...)color and bold weight for Rare+; added unit + snapshot + integration tests.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/color.ts | New color computation + terminal capability detection + colorFor public API. |
| src/statusline-wrapper.ts | Uses colorFor for sprite art in normal + bubble modes. |
| src/lib/card.ts | Wraps sprite art lines in colorFor(...) escape codes. |
| src/lib/share.ts | Applies computed RGB + bold weight to the share sprite placeholder via inline style. |
| src/tests/color.test.ts | Unit tests for constants/helpers/capability detection and colorFor. |
| src/tests/color-snapshot.test.ts | Snapshot fixtures to lock down ANSI escape output. |
| src/tests/card-color.test.ts | Card integration tests for ANSI color injection. |
| src/tests/share-color.test.ts | Share HTML integration tests for inline RGB/bold behavior. |
| src/tests/snapshots/color-snapshot.test.ts.snap | Snapshot outputs for the fixture contract. |
Comments suppressed due to low confidence (1)
src/statusline-wrapper.ts:285
- Same issue as bubble mode:
buddy.raritycomes from JSON and can be missing/invalid, but it’s passed directly intocolorFor(). Ifrarityis undefined this will throw and break the statusline rendering. Please default/validate rarity before callingcolorFor(consistent with the earlierbuddy.rarity || 'common'fallback).
const artWidth = Math.max(...asciiLines.map((l: string) => l.length));
const spriteColor = colorFor(buddy.species, buddy.rarity, buddy.xp);
for (let i = 0; i < asciiLines.length; i++) {
const artPart = `${spriteColor}${(asciiLines[i] || "").padEnd(artWidth)}${RESET}`;
if (i === 0) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Three robustness fixes from the PR review: 1. colorFor respects caps.ansi16 — previously fell back to ANSI-16 escapes even when the flag was false, making TerminalCapabilities lie. Now returns '' if no capability flag is set. 2. card.ts skips RESET when spriteColor is empty — NO_COLOR mode no longer leaks a stray \x1b[0m on every sprite line. 3. statusline-wrapper.ts defaults buddy.rarity to 'common' and buddy.xp to 0 at both colorFor call sites — buddy state is parsed from JSON and rarity can be missing/invalid; without the default, RARITY_METALS lookup would throw. Mirrors the existing buddy.rarity || 'common' pattern on line 86. Also makes the RESET conditional in both bubble and normal modes, same as card.ts.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/statusline-wrapper.ts:286
- Same runtime-safety issue as bubble mode:
buddy.rarity || 'common'won’t protect against an invalid-but-truthy rarity string from buddy-status.json. IfcolorFor()throws here, the statusline wrapper will output nothing. Recommend validating rarity before callingcolorFor()and falling back to'common'/no color for unknown values.
const artWidth = Math.max(...asciiLines.map((l: string) => l.length));
const spriteColor = colorFor(buddy.species, buddy.rarity || 'common', buddy.xp ?? 0);
const spriteReset = spriteColor ? RESET : '';
for (let i = 0; i < asciiLines.length; i++) {
const artPart = `${spriteColor}${(asciiLines[i] || "").padEnd(artWidth)}${spriteReset}`;
| const inner = padded.slice(prefix.length, padded.length - suffix.length); | ||
| return `${prefix}${spriteColor}${inner}${spriteReset}${suffix}`; |
Four findings: 1. computeRGB now falls back to common metals/saturation for invalid rarities (e.g., stale state file post schema migration). Better to render a muted sprite than crash the whole statusline. This defends both the buddy.rarity || 'common' seam in statusline-wrapper and any future JSON callers — TS still enforces typed callers. 2. card.ts renames the shadowed `inner` to `artInner` to avoid collision with the outer card-inner-width binding. 3. card-color.test.ts adds a precondition that expectedColor is not empty, so the toContain assertion isn't vacuous under NO_COLOR. 4. New unit test covers computeRGB's invalid-rarity fallback path, asserting it produces the same RGB as the common-rarity baseline.
|
I reviewed PR #126 locally with your “impact on existing users” concern in mind. I also kicked off a parallel Codex review, but it timed out before returning, so Bottom line
What changes for existing users
So existing users should mostly experience:
Existing-user impact assessment
My main concern
What I’d specifically ask before merge
Merge recommendation
Suggested review comment
|
Summary
Replaces the single hard-coded MAGENTA sprite color with a three-input gradient driven by
(species, rarity, totalXp), so every buddy reads as a distinct, evolving color identity.src/lib/color.ts— 21 species palettes (84 RGB anchors) + 5 rarity metal tiers (10 anchors), saturation tint, bold weight at Rare+, and a four-tier terminal capability cascade (truecolor / 256-color / 16-color / NO_COLOR).colorFor(species, rarity, totalXp, caps?)returns the appropriate ANSI escape (or empty string for NO_COLOR).statusline-wrapper.tsnormal sprite modestatusline-wrapper.tsbubble sprite modecard.tsrenderCard()share.tsrenderShareHtml()— Puppeteer/PNG output uses inlinergb()+font-weight: boldfor Rare+Spec:
docs/superpowers/specs/2026-05-12-buddy-color-progression-design.mdPlan:
docs/superpowers/plans/2026-05-12-buddy-color-progression.mdTest plan
src/__tests__/color.test.ts(every exported function covered)src/__tests__/color-snapshot.test.tssrc/__tests__/card-color.test.ts(3 tests)src/__tests__/share-color.test.ts(7 tests)Notes