Skip to content

feat: call-sprite — emotion bubble + bounce animation on tkm call#55

Open
eulneul wants to merge 10 commits into
ThunderConch:masterfrom
eulneul:feat/call-sprite-bubble-animation
Open

feat: call-sprite — emotion bubble + bounce animation on tkm call#55
eulneul wants to merge 10 commits into
ThunderConch:masterfrom
eulneul:feat/call-sprite-bubble-animation

Conversation

@eulneul
Copy link
Copy Markdown
Contributor

@eulneul eulneul commented Apr 19, 2026

Summary

  • Adds a new sprite <id> <ev> CLI command to tokenmon.ts
  • When tkm call is used, renders the Pokémon's braille sprite with a small ASCII speech bubble showing its emotional state based on EV bond level
  • Plays a brief up-down bounce animation (sprite hops twice) using ANSI cursor movement, so it feels alive when you call it

Emotion mapping

EV range Bubble Meaning
0 ? Cold / confused
1–50 ... Shy / hesitant
51–120 :) Warming up
121–200 <3 Happy / fond
201–252 <3! Deep bond

How it looks

The bubble appears to the upper-right of the sprite, with a tail pointing at the Pokémon:

                     ╭──────╮
                     │ <3   │
                     ╰───╮──╯
       ⣀  ⣀⡀             │
      ⠘⣿⣷⣾⣿⡿
      ⢰⣿⣿⣿⣿⡷⣠⣄
      ⠈⠁⢽⣿⣿⣿⡿⠏
        ⠈⠡⠗⠃⠛

The sprite bounces up → down → up → settles with the bubble.

Changes

  • src/cli/tokenmon.ts: adds SPRITES_BRAILLE_DIR/SPRITES_TERMINAL_DIR import, cmdSprite() function with helpers, case 'sprite': dispatch
  • skills/call/SKILL.md: Step 3 updated from pokedex <id>sprite <id> <ev>

🤖 Generated with Claude Code

@eulneul eulneul force-pushed the feat/call-sprite-bubble-animation branch from c537acd to 7bb81c0 Compare April 19, 2026 11:53
@ThunderConch ThunderConch marked this pull request as draft April 20, 2026 05:10
@eulneul eulneul marked this pull request as ready for review April 20, 2026 07:00
Copy link
Copy Markdown
Owner

@ThunderConch ThunderConch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 고마워요! 스프라이트/말풍선 아이디어 자체는 좋은데, 머지 전에 크게 두 갈래로 정리가 필요해 보입니다.


1. 🚨 브랜치가 낡은 master 기반이라 무관한 기능들이 삭제되고 있어요

현재 mergeStateStatus: DIRTY이고, diff를 보면 sprite와 전혀 관계없는 최근 작업분이 같이 지워지고 있습니다:

삭제된 것 출처
friendly-battle 커맨드 + help 텍스트 PR #40-#46 (v0.7.0)
met / met_detail 트래킹 (starter / cheat / fateful encounter) 기존 기능
formatMetInfo import + pokedex "trainer_memo" 블록 기존 기능
toShinyKey 기반 진화 가드 (eligible 필터) PR #49
evolution.ts 특수 진화 조건 (newLevel > oldLevelfriendship >= 200) 기존 로직

의도된 변경이 아니라면, 최신 master로 리베이스 후 force-push 부탁드려요:

git fetch origin
git rebase origin/master
# 충돌 해결 시 master 쪽 라인 유지 (friendly-battle, met_detail 등)
git push --force-with-lease

리베이스 후 diff는 대략 +160 / -10 미만으로 줄어드는 게 정상입니다 (순수 sprite 추가분만).


2. 스프라이트/말풍선 구현 자체에 대한 피드백

리베이스된 상태를 가정하고 의도된 추가분만 봤을 때:

🔴 Critical

sleepSync busy-waitsrc/cli/tokenmon.ts cmdSprite

function sleepSync(ms: number): void {
  const end = Date.now() + ms;
  while (Date.now() < end) { /* busy-wait */ }
}

Node 싱글스레드를 100~140ms × 4회 (~450ms) CPU 100%로 점유합니다. Claude Code 내부에서 호출되면 다른 I/O가 완전히 블로킹돼요.

execSync('sleep 0.1') 또는 Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) 권장.

🟡 Major

  1. Emotion 로직이 두 군데에 중복 하드코딩tokenmon.tsemotionBubble()status-line.ts의 call bubble 구성이 EV threshold(0 / 50 / 120 / 200)와 inner 문자열(? / ... / :) / <3 / <3!)을 각자 들고 있습니다. 한쪽만 바꾸면 말풍선 불일치 확정.
    src/core/emotion.ts 같은 곳에 getEmotionInner(ev: number): string 공유 함수 추출.

  2. \u2800 처리가 같은 PR 안에서 반대 방향spriteFrameLines()padded.replace(/\u2800/g, ' ')로 공백 치환하는데, status-line.ts는 같은 diff 안 주석에서 "CJK 터미널 폭 유지 위해 \u2800 유지 필수"라고 명시합니다. CLI 단독 출력이라 의도적인 거면 주석으로 근거를 남겨주세요.

  3. Shiny 분기 누락loadSpriteForCall(pokemonId)pData.id만 사용합니다. shiny 포켓몬을 부르면 일반 스프라이트가 뜹니다. (SPRITES_BRAILLE_DIR에 shiny 경로 있으면 그쪽으로 분기 필요)

🟢 Minor (별도 이슈로 넘겨도 OK)

  • parseInt(args[2] ?? '0', 10) NaN 가드 없음 → sprite pikachu abc 입력 시 bubble이 ?로 fallthrough
  • doReset() defaultState에 last_called 초기화 누락
  • sprite 커맨드 usage/help 메시지 없음
  • status-line 15초 TTL이 갱신 주기 따라 15~20초로 늘어날 수 있음 (UX 영향 작음)

머지 전 체크리스트

  • master로 리베이스 + 무관한 삭제분 복구
  • sleepSync busy-wait 교체
  • emotion 로직 공유 함수로 추출
  • \u2800 처리 통일 (또는 근거 주석)
  • shiny 스프라이트 분기 추가

Minor 4건은 별도 이슈로 트래킹해도 괜찮을 것 같습니다.

eulneul and others added 10 commits April 20, 2026 17:15
Renders the Pokémon's braille sprite with an ASCII speech bubble
to the right, showing its emotional state based on EV bond level.
Plays a bounce animation when stdout is a real TTY (not piped).

After `tkm call`, State.last_called records { pokemon, ev, ts }.
status-line.ts reads this and renders the speech bubble next to
the called Pokémon's sprite for 15 seconds, then auto-clears.

| EV      | Bubble | Feeling         |
|---------|--------|-----------------|
| 0       | ?      | Cold / confused |
| 1–50    | ...    | Shy / hesitant  |
| 51–120  | :)     | Warming up      |
| 121–200 | <3     | Happy / fond    |
| 201–252 | <3!    | Deep bond       |

- src/core/types.ts: add `last_called` field to State
- src/cli/tokenmon.ts: add cmdSprite(), case 'sprite':, write last_called in cmdCall()
- src/status-line.ts: read last_called, render bubble next to sprite
- skills/call/SKILL.md: Step 3 updated to use sprite command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bubble was previously appended as a suffix to the right of the sprite,
shifting sibling sprites rightward and breaking row alignment.
Now rendered as separate lines above the called pokemon's column,
centered over its sprite position.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After rendering the sprite with call bubble, plays a brief bounce
animation (sprite shifts up 1 row × 2 cycles) on TTY output.
Only fires when a pokemon was called within the last 15s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
process.stdout.isTTY is false when Claude Code pipes hook output,
so animation never ran. Writing directly to /dev/tty bypasses the
pipe and reaches the terminal unconditionally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ruption

Previously the initial bubble+sprite was printed via console.log (stdout),
then animation ran via /dev/tty. When stdout flushed asynchronously, frames
appeared in the chat window. Now all output for animated groups goes through
/dev/tty, keeping stdout and /dev/tty writes fully separate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cursor-movement animation in the stop hook races with Claude Code's TUI
rendering — /dev/tty writes land at unpredictable cursor positions and
corrupt the chat window. The bubble already conveys the call reaction;
bounce animation remains available in the chat-window sprite command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces /dev/tty approach with writeSync to stdout fd directly.
writeSync bypasses Node's buffer so frames reach Claude Code's
pipe reader immediately; sleepSync provides inter-frame timing.
All animation output stays in the hook's stdout stream, avoiding
cursor-position races with the Claude Code TUI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Code buffers hook stdout so animation timing is lost.
Bubble renders statically above the called pokemon's sprite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'special' condition was triggering on every level-up, causing
3-stage chains (e.g. Pawmi→Pawmo→Pawmot) to skip the middle stage:
Pawmo would evolve to Pawmot on its very first level-up after evolving.

Changed to require friendship>=200, matching the spirit of walk-based
special evolutions and preventing the instant skip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract getEmotionInner(ev) to src/core/emotion.ts, eliminating
  duplicate EV threshold logic between tokenmon.ts and status-line.ts
- Replace sleepSync busy-wait with Atomics.wait() to avoid blocking
  Node event loop during sprite animation
- Add shiny sprite support in loadSpriteForCall via shiftAnsiHue()
- Add last_called: null to doReset() defaultState
- Guard parseInt(args[2]) against NaN with || 0 fallback
- Add comment explaining why CLI converts \u2800 → space while
  status-line.ts keeps \u2800 for CJK terminal width consistency
@eulneul eulneul force-pushed the feat/call-sprite-bubble-animation branch from 32f955f to 5d4513d Compare April 20, 2026 08:18
ThunderConch added a commit that referenced this pull request Apr 20, 2026
…on, met memo) + add emotion tests

The call-sprite feature itself stays; this commit reverts three unrelated
changes that the reviewer flagged and adds the missing unit coverage.

- Restore the `friendly-battle` subcommand dispatch and its help line in
  `src/cli/tokenmon.ts`. Removing them silently broke the
  `tokenmon friendly-battle …` CLI path that `friendly-battle-cli.test.ts`
  asserts against (7 failing subtests on master).
- Revert the global `condition === 'special'` evolution check in
  `src/core/evolution.ts` back to the level-up fallback
  (`newLevel > oldLevel`). The friendship>=200 rule from the PR was
  meant to patch a 3-stage chain skip on a specific pokémon but it
  changed semantics for all 18 pokémon with `evolves_condition:
  "special"` (verified via data/gen*/pokemon.json). Mantyke (458)
  regressed to `null` under the new rule; restoring the old rule puts
  it back to the expected `226` transition.
- Restore `met` / `met_detail` writes in the four spots that stripped
  them (starter, cheat unlock, legendary claim, setup starter) and the
  trainer-memo block in `cmdPokedex`. `types.ts` / `pokemon-data.ts`
  still ship the schema, so stripping writes without stripping the
  reader was a partial-delete that left `cmdPokedex` rendering empty
  memos for newly-acquired pokémon.
- Add `test/emotion.test.ts` with 11 cases covering `getEmotionInner`
  tier boundaries and the new `isCallBubbleActive` helper (extracted
  from the inline TTL check in `status-line.ts` so the 15 s window is
  testable in isolation). No functional change to the bubble
  rendering — `status-line.ts` now calls the shared helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants