Files
d683e85395 feat(buddy): hero pixel-art companions with signature Enter animations (#1972)
* feat(buddy): hero pixel-art companions with signature Enter animations

Rebuild the buddy system as heroes-only. The 18 legacy rolled species are
removed; the hatch pool is now 7 hero forms — robinhood, kaio, strawhat,
merlin, kage, ember, corsair — each hand-pickable via /buddy set.

Every hero has 22x16 truecolor half-block pixel art (idle + action poses),
a line-art fallback for low-color terminals, a narrow-mode face, and a
signature effect that fires on every message submission: arrow with impact
thunk, charging full-width energy wave, stretchy punch that extends and
snaps back, twinkling sparkle stream, spinning shuriken, gradient fire
cone, and cannonball with smoke trail.

Engine: companion animation moves from a raw 500ms setInterval to the
shared animation clock (useAnimationFrame; pauses when hidden, respects
prefersReducedMotion), with a one-shot 50ms burst driver (useShotClock,
arm-then-anchor to avoid stale-tick draw-phase skips) and a general
ActionEffect system (pure draw/travel/impact functions, frame-tested).
Effects travel right-to-left toward the prompt — matching where the
sprite actually stands.

Commands: /buddy set <form|random>, /buddy name <name>, muted-buddy
feedback (silent no-op pets now explain themselves), and a hatch-message
fix so the announced species always matches the displayed sprite (the
message previously rolled with a different seed).

BREAKING: existing rolled pets transform into a hero on upgrade (name and
personality persist; speciesOverride pins are unaffected).

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(buddy): address CodeRabbit review on PR #1972

- useShotClock: consume an in-flight shot when playback becomes
  ineligible mid-flight (mute/reduced-motion/resize), so re-enabling
  can't resume a stale animation.
- /buddy unmute: emit a greeting reaction — the sprite reads
  companionMuted non-reactively and its clock is paused while hidden,
  so a config-only unmute left it invisible until an unrelated
  re-render.
- /buddy name: strip ANSI escapes and control/format characters before
  saving, and cap by display width (stringWidth) instead of UTF-16
  length.
- CompanionSprite: track bubble age in sync-render state instead of an
  effect-updated ref, so a fresh reaction can't render pre-faded.
- companion_intro already keyed on name+species (prior commit); tests
  now pin exact faces for all seven heroes, separate idle/shoot pixel
  frame counts, and decode-guard the charCode species constants.
- CompanionActionFX tests: deterministic companion fixture via
  complete-config module mock; raw (untrimmed) output compared against
  a rendered-null baseline so a spurious blank FX row fails.
- companion.test: re-register the real config module in afterAll
  (mock.restore does not undo mock.module).
- Types: SPECIES_COLORS and FORM_FLAVOR are full Records (compile
  error on a colorless/flavorless future hero); dead RARITY_COLORS
  removed.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(buddy): regression coverage for bubble age reset on reaction change

Renders CompanionSprite against a fake shared clock: ages the first
bubble past the fade threshold, swaps the reaction WITHOUT advancing
the clock, and asserts the fresh bubble renders unfaded. Fading is
detected structurally (border and text collapse to one color when
fading) so the test is independent of the active theme's exact values.
Requested by CodeRabbit on PR #1972.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-15 21:50:10 +08:00
..