From 9b30213c9ac5c58c2ab44bba13b02a08273e089c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:18:54 +0200 Subject: [PATCH 1/5] feat(claude): replace abra mod with pokemon mod - rename abra plugin to pokemon and keep abra sprites - add bulbasaur sprites and tests - update marketplace, readme and enabled plugins in settings --- ai-stuff/claude/README.md | 2 +- .../mods/.claude-plugin/marketplace.json | 6 +- .../mods/abra/.claude-plugin/plugin.json | 6 - ai-stuff/claude/mods/abra/README.md | 47 -- ai-stuff/claude/mods/abra/hooks/frames.js | 205 ------ ai-stuff/claude/mods/abra/hooks/register.js | 116 ---- .../claude/mods/abra/scripts/build-frames.mjs | 82 --- .../mods/pokemon/.claude-plugin/plugin.json | 6 + ai-stuff/claude/mods/pokemon/README.md | 62 ++ ai-stuff/claude/mods/pokemon/hooks/frames.js | 627 ++++++++++++++++++ .../mods/{abra => pokemon}/hooks/hooks.json | 0 .../claude/mods/pokemon/hooks/register.js | 156 +++++ .../mods/pokemon/scripts/build-frames.mjs | 101 +++ .../sprites/abra}/default_idle_8fps.gif | Bin .../sprites/abra}/default_walk_8fps.gif | Bin .../sprites/abra}/shiny_idle_8fps.gif | Bin .../sprites/abra}/shiny_walk_8fps.gif | Bin .../sprites/bulbasaur/default_idle_8fps.gif | Bin 0 -> 793 bytes .../sprites/bulbasaur/default_walk_8fps.gif | Bin 0 -> 804 bytes .../sprites/bulbasaur/shiny_idle_8fps.gif | Bin 0 -> 508 bytes .../sprites/bulbasaur/shiny_walk_8fps.gif | Bin 0 -> 517 bytes .../tests/pokemon.test.ts} | 39 +- ai-stuff/claude/settings.json | 4 +- 23 files changed, 983 insertions(+), 476 deletions(-) delete mode 100644 ai-stuff/claude/mods/abra/.claude-plugin/plugin.json delete mode 100644 ai-stuff/claude/mods/abra/README.md delete mode 100644 ai-stuff/claude/mods/abra/hooks/frames.js delete mode 100644 ai-stuff/claude/mods/abra/hooks/register.js delete mode 100644 ai-stuff/claude/mods/abra/scripts/build-frames.mjs create mode 100644 ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json create mode 100644 ai-stuff/claude/mods/pokemon/README.md create mode 100644 ai-stuff/claude/mods/pokemon/hooks/frames.js rename ai-stuff/claude/mods/{abra => pokemon}/hooks/hooks.json (100%) create mode 100644 ai-stuff/claude/mods/pokemon/hooks/register.js create mode 100644 ai-stuff/claude/mods/pokemon/scripts/build-frames.mjs rename ai-stuff/claude/mods/{abra/sprites => pokemon/sprites/abra}/default_idle_8fps.gif (100%) rename ai-stuff/claude/mods/{abra/sprites => pokemon/sprites/abra}/default_walk_8fps.gif (100%) rename ai-stuff/claude/mods/{abra/sprites => pokemon/sprites/abra}/shiny_idle_8fps.gif (100%) rename ai-stuff/claude/mods/{abra/sprites => pokemon/sprites/abra}/shiny_walk_8fps.gif (100%) create mode 100644 ai-stuff/claude/mods/pokemon/sprites/bulbasaur/default_idle_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/bulbasaur/default_walk_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/bulbasaur/shiny_idle_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/bulbasaur/shiny_walk_8fps.gif rename ai-stuff/claude/mods/{abra/tests/abra.test.ts => pokemon/tests/pokemon.test.ts} (60%) diff --git a/ai-stuff/claude/README.md b/ai-stuff/claude/README.md index e543d9dc..06810380 100644 --- a/ai-stuff/claude/README.md +++ b/ai-stuff/claude/README.md @@ -23,7 +23,7 @@ ai-stuff/claude/ │ worktree-cleanup skill helpers moved to ai-stuff/_shared/scripts/ — │ shared across tools) ├── mods/ # Local plugin marketplace `dotfiles-mods` for Claude Code mods -│ └── abra/ # Pixel Abra in the band above the prompt (see its README) +│ └── pokemon/ # Pixel Pokémon in the band above the prompt (see its README) ├── settings.json # Hooks, permissions, statusline, plugins (→ ~/.claude/settings.json) └── README.md ``` diff --git a/ai-stuff/claude/mods/.claude-plugin/marketplace.json b/ai-stuff/claude/mods/.claude-plugin/marketplace.json index ad96d4e5..d814bfd0 100644 --- a/ai-stuff/claude/mods/.claude-plugin/marketplace.json +++ b/ai-stuff/claude/mods/.claude-plugin/marketplace.json @@ -6,9 +6,9 @@ }, "plugins": [ { - "name": "abra", - "source": "./abra", - "description": "A pixel Abra that lives at the right edge of the band above the prompt" + "name": "pokemon", + "source": "./pokemon", + "description": "A pixel Pokémon that lives at the right edge of the band above the prompt" } ] } diff --git a/ai-stuff/claude/mods/abra/.claude-plugin/plugin.json b/ai-stuff/claude/mods/abra/.claude-plugin/plugin.json deleted file mode 100644 index 2c2fb73a..00000000 --- a/ai-stuff/claude/mods/abra/.claude-plugin/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "abra", - "version": "0.3.0", - "description": "A pixel Abra that lives at the right edge of the band above the prompt", - "author": { "name": "deniz gökçin" } -} diff --git a/ai-stuff/claude/mods/abra/README.md b/ai-stuff/claude/mods/abra/README.md deleted file mode 100644 index d5f29294..00000000 --- a/ai-stuff/claude/mods/abra/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# abra - -A Claude Code mod that draws a pixel Abra at the right edge of the band above -the prompt. It bobs in place while you're idle and paces back and forth while -Claude works. - -## Requirements - -- Claude Code v2.1.287 or later -- The terminal app. The Desktop app has no `Raster`, so the mod draws nothing there. -- A truecolor terminal (iTerm2, Ghostty, kitty, WezTerm) - -## Usage - -| Command | Effect | -| --- | --- | -| `/abra` | Toggle between default and shiny, saved across sessions | -| `/abra shiny`, `/abra default` | Pick a variant | -| Ctrl+X Ctrl+A | Collapse or expand the band (Claude Code's own binding) | - -## Layout - -| Path | Contents | -| --- | --- | -| `hooks/register.js` | Band renderer, animation timer, `/abra` command | -| `hooks/frames.js` | Generated pixel frames. Don't edit by hand. | -| `sprites/*.gif` | Source GIFs, 32x32 at 2 frames each | -| `scripts/build-frames.mjs` | Regenerates `frames.js` from the GIFs with ffmpeg | -| `tests/abra.test.ts` | `claude plugin test` suite | - -## Development - -```bash -node scripts/build-frames.mjs # after changing sprites/ -claude plugin validate . -claude plugin test -claude --plugin-dir . # live-reloading session -``` - -The sprite is 19x19 pixels. Each terminal cell holds two vertical pixels with -`▀`/`▄` half blocks, so Abra takes 19 columns by 10 rows inside a 40 column strip. - -## Credits - -Sprites come from [jakobhoeg/vscode-pokemon](https://github.com/jakobhoeg/vscode-pokemon/tree/main/media/gen1/abra). -Pokémon sprites are © The Pokémon Company / Nintendo / Game Freak, used here -for a personal, non-commercial fan project. diff --git a/ai-stuff/claude/mods/abra/hooks/frames.js b/ai-stuff/claude/mods/abra/hooks/frames.js deleted file mode 100644 index ce252649..00000000 --- a/ai-stuff/claude/mods/abra/hooks/frames.js +++ /dev/null @@ -1,205 +0,0 @@ -// Generated by scripts/build-frames.mjs from sprites/*.gif. Do not edit by hand. -// Each frame is rows of palette letters; '.' is transparent. - -export const SPRITE = { - "width": 19, - "height": 19, - "variants": { - "default": { - "palette": [ - "#000000", - "#483818", - "#b89828", - "#f8d860", - "#907860", - "#786038" - ], - "idle": [ - [ - "...................", - "...................", - "........aaaaa......", - "....bbbacdddcabbb..", - "....bddcdddddcddb..", - "....bcdddddddddcb..", - ".....bdddddddddb...", - "....bcdddddddddcb..", - "....acbadddddabca..", - "....acddadddaddca..", - ".....aadddddddaa...", - "....beeaadddaaeeb..", - "....affbcaaacbffa..", - ".....aaccbfbccaa...", - "...bbdcaccbccacdbb.", - "...bdccaddaddaccdb.", - "....bbccaaaaaccbb..", - "......aa.....aa....", - "..................." - ], - [ - "...................", - "........aaaaa......", - "....bbbacdddcabbb..", - "....bddcdddddcddb..", - "....bcdddddddddcb..", - ".....bdddddddddb...", - "....bcdddddddddcb..", - "....acbadddddabca..", - "....acddadddaddca..", - ".....aadddddddaa...", - "....beeaadddaaeeb..", - "....affbcaaacbffa..", - ".....aaccbfbccaa...", - "...bbdcaccbccacdbb.", - "...bdccaddaddaccdb.", - "....bbccaaaaaccbb..", - "......aa.....aa....", - "...................", - "..................." - ] - ], - "walk": [ - [ - "...................", - ".........bb........", - "........bccbba.....", - "........aabcddba...", - ".......addcbdddda..", - ".......accddddddda.", - "........accddddddb.", - ".......bcbdddddddda", - ".......acddddbadddb", - ".......adddddddaddb", - ".bb.....accdddddbb.", - "bcb....afaaaaaaa...", - "bccbbb.afffab......", - "bccfccbcaaacbbb....", - ".bffcccaffbacaaa...", - "..bbcccabbccacda...", - "....babbaccccdb....", - ".........aabccb....", - "............bb....." - ], - [ - ".........bb........", - "........bccbba.....", - "........aabcddba...", - ".......addcbdddda..", - ".......accddddddda.", - "........accddddddb.", - ".......bcbdddddddda", - "..bb...acddddbadddb", - ".bcb...adddddddaddb", - ".bcb....accdddddbb.", - ".bcfb..afaaaaaaa...", - ".affcbbafffab......", - "..bcccccaaacbbb....", - "..accccaffbacaaa...", - "...abccabbccacda...", - ".....abbaccccdb....", - ".........aabccb....", - "............bb.....", - "..................." - ] - ] - }, - "shiny": { - "palette": [ - "#000000", - "#604820", - "#d8b870", - "#f8e0a0", - "#a89080", - "#a08048" - ], - "idle": [ - [ - "...................", - "...................", - "........aaaaa......", - "....bbbacdddcabbb..", - "....bddcdddddcddb..", - "....bcdddddddddcb..", - ".....bdddddddddb...", - "....bcdddddddddcb..", - "....acbadddddabca..", - "....acddadddaddca..", - ".....aadddddddaa...", - "....beeaadddaaeeb..", - "....affbcaaacbffa..", - ".....aaccbfbccaa...", - "...bbdcaccbccacdbb.", - "...bdccaddaddaccdb.", - "....bbccaaaaaccbb..", - "......aa.....aa....", - "..................." - ], - [ - "...................", - "........aaaaa......", - "....bbbacdddcabbb..", - "....bddcdddddcddb..", - "....bcdddddddddcb..", - ".....bdddddddddb...", - "....bcdddddddddcb..", - "....acbadddddabca..", - "....acddadddaddca..", - ".....aadddddddaa...", - "....beeaadddaaeeb..", - "....affbcaaacbffa..", - ".....aaccbfbccaa...", - "...bbdcaccbccacdbb.", - "...bdccaddaddaccdb.", - "....bbccaaaaaccbb..", - "......aa.....aa....", - "...................", - "..................." - ] - ], - "walk": [ - [ - "...................", - ".........bb........", - "........bccbba.....", - "........aabcddba...", - ".......addcbdddda..", - ".......accddddddda.", - "........accddddddb.", - ".......bcbdddddddda", - ".......acddddbadddb", - ".......adddddddaddb", - ".bb.....accdddddbb.", - "bcb....afaaaaaaa...", - "bccbbb.afffab......", - "bccfccbcaaacbbb....", - ".bffcccaffbacaaa...", - "..bbcccabbccacda...", - "....babbaccccdb....", - ".........aabccb....", - "............bb....." - ], - [ - ".........bb........", - "........bccbba.....", - "........aabcddba...", - ".......addcbdddda..", - ".......accddddddda.", - "........accddddddb.", - ".......bcbdddddddda", - "..bb...acddddbadddb", - ".bcb...adddddddaddb", - ".bcb....accdddddbb.", - ".bcfb..afaaaaaaa...", - ".affcbbafffab......", - "..bcccccaaacbbb....", - "..accccaffbacaaa...", - "...abccabbccacda...", - ".....abbaccccdb....", - ".........aabccb....", - "............bb.....", - "..................." - ] - ] - } - } -} diff --git a/ai-stuff/claude/mods/abra/hooks/register.js b/ai-stuff/claude/mods/abra/hooks/register.js deleted file mode 100644 index 54094f66..00000000 --- a/ai-stuff/claude/mods/abra/hooks/register.js +++ /dev/null @@ -1,116 +0,0 @@ -import { SPRITE } from './frames.js' - -// Abra lives at the right edge of the band above the prompt. It bobs in place -// while you're idle and paces back and forth while Claude works. - -const TICK_MS = 150 -const FRAME_TICKS = 2 -const STRIP_COLUMNS = 40 -const DEFAULT_COLOR = 0x01000000 -const UPPER_HALF = 0x2580 -const LOWER_HALF = 0x2584 -const SPACE = 0x20 - -let variant = 'default' -let working = false -let bandId = null -let columns = STRIP_COLUMNS -let tick = 0 -let x = STRIP_COLUMNS - SPRITE.width -let facing = 'left' - -const rows = Math.ceil(SPRITE.height / 2) - -// Palette letter to 0xRRGGBB, per variant -const COLORS = Object.fromEntries( - Object.entries(SPRITE.variants).map(([name, sheet]) => [ - name, - Object.fromEntries(sheet.palette.map((hex, i) => [String.fromCharCode(97 + i), parseInt(hex.slice(1), 16)])), - ]), -) - -// Look up a pixel's color in a frame, or null where it's transparent -function pixelsOf(frame, colors, flip) { - return (px, py) => { - const row = frame[py] - if (!row) return null - return colors[row[flip ? SPRITE.width - 1 - px : px]] ?? null - } -} - -// Pack the current frame into Raster cells, two pixels per cell with half blocks -function cellsNow() { - const sheet = SPRITE.variants[variant] - const anim = working ? sheet.walk : sheet.idle - const frame = anim[Math.floor(tick / FRAME_TICKS) % anim.length] - const pixel = pixelsOf(frame, COLORS[variant], working && facing === 'left') - const words = new Uint32Array(columns * rows * 3) - for (let cy = 0; cy < rows; cy++) { - for (let cx = 0; cx < columns; cx++) { - const px = cx - x - const top = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2) : null - const bottom = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2 + 1) : null - const i = (cy * columns + cx) * 3 - if (top !== null) words.set([UPPER_HALF, top, bottom ?? DEFAULT_COLOR], i) - else if (bottom !== null) words.set([LOWER_HALF, bottom, DEFAULT_COLOR], i) - else words.set([SPACE, DEFAULT_COLOR, DEFAULT_COLOR], i) - } - } - return new Uint8Array(words.buffer).toBase64() -} - -// Move one column per tick while working, turning at the strip's edges -function step() { - tick += 1 - if (!working) return - const maxX = columns - SPRITE.width - if (facing === 'left' && x <= 0) facing = 'right' - else if (facing === 'right' && x >= maxX) facing = 'left' - x = Math.max(0, Math.min(maxX, x + (facing === 'left' ? -1 : 1))) -} - -export function register(on) { - on('session.start', async ($, e, next) => { - await $.command.register({ - name: 'abra', - description: 'Switch Abra between default and shiny', - argumentHint: '[default|shiny]', - immediate: true, - }) - const saved = await $.store.get('variant') - if (saved in SPRITE.variants) variant = saved - $.clock.every(TICK_MS, () => { - step() - if (bandId !== null) $.ui.blit({ requestId: bandId, key: 'abra', columns, rows, cells: cellsNow() }) - }) - return next(e) - }) - - on('command.run', { command: 'abra' }, async ($, e) => { - const asked = e.args.trim() - variant = asked in SPRITE.variants ? asked : variant === 'shiny' ? 'default' : 'shiny' - await $.store.set('variant', variant) - $.ui.invalidate('ui.render') - return { text: 'Abra is now ' + variant + '.' } - }) - - on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { - if (e.surface !== 'terminal' || e.props.hasSurvey) { - bandId = null - return next(e) - } - const { Box, Raster } = $.ui.resolve(e) - working = e.props.isWorking - bandId = e.requestId - columns = Math.max(SPRITE.width, Math.min(STRIP_COLUMNS, e.props.bodyColumns)) - x = Math.max(0, Math.min(columns - SPRITE.width, x)) - - const abra = Raster({ key: 'abra', columns, rows, cells: cellsNow() }) - const theirs = await next(e) - return Box({ - flexDirection: 'row', - justifyContent: theirs ? 'space-between' : 'flex-end', - children: theirs ? [Box({ flexGrow: 1, children: [theirs] }), abra] : [abra], - }) - }) -} diff --git a/ai-stuff/claude/mods/abra/scripts/build-frames.mjs b/ai-stuff/claude/mods/abra/scripts/build-frames.mjs deleted file mode 100644 index 2cc26f57..00000000 --- a/ai-stuff/claude/mods/abra/scripts/build-frames.mjs +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node - -// Decodes sprites/*.gif with ffmpeg and writes hooks/frames.js. -// Each frame becomes rows of palette letters, cropped to the box every frame fits in. -// Run from anywhere: node scripts/build-frames.mjs - -import { execFileSync } from 'node:child_process' -import { writeFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') -const SIZE = 32 -const VARIANTS = ['default', 'shiny'] -const ANIMS = ['idle', 'walk'] -const LETTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - -function decode(file) { - const raw = execFileSync('ffmpeg', [ - '-v', 'error', '-i', file, '-fps_mode', 'passthrough', - '-f', 'rawvideo', '-pix_fmt', 'rgba', '-', - ]) - const frameBytes = SIZE * SIZE * 4 - const frames = [] - for (let off = 0; off < raw.length; off += frameBytes) { - const px = [] - for (let i = 0; i < SIZE * SIZE; i++) { - const p = off + i * 4 - px.push(raw[p + 3] > 127 ? (raw[p] << 16) | (raw[p + 1] << 8) | raw[p + 2] : null) - } - frames.push(px) - } - return frames -} - -const sheets = {} -for (const v of VARIANTS) { - sheets[v] = {} - for (const a of ANIMS) sheets[v][a] = decode(join(ROOT, 'sprites', `${v}_${a}_8fps.gif`)) -} - -let minX = SIZE, minY = SIZE, maxX = -1, maxY = -1 -for (const v of VARIANTS) for (const a of ANIMS) for (const px of sheets[v][a]) { - px.forEach((c, i) => { - if (c === null) return - const x = i % SIZE, y = Math.floor(i / SIZE) - minX = Math.min(minX, x); maxX = Math.max(maxX, x) - minY = Math.min(minY, y); maxY = Math.max(maxY, y) - }) -} - -const out = { width: maxX - minX + 1, height: maxY - minY + 1, variants: {} } -for (const v of VARIANTS) { - const palette = [] - const letter = (c) => { - if (c === null) return '.' - let i = palette.indexOf(c) - if (i < 0) i = palette.push(c) - 1 - return LETTERS[i] - } - const anims = {} - for (const a of ANIMS) { - anims[a] = sheets[v][a].map((px) => { - const rows = [] - for (let y = minY; y <= maxY; y++) { - let row = '' - for (let x = minX; x <= maxX; x++) row += letter(px[y * SIZE + x]) - rows.push(row) - } - return rows - }) - } - out.variants[v] = { palette: palette.map((c) => '#' + c.toString(16).padStart(6, '0')), ...anims } -} - -const body = `// Generated by scripts/build-frames.mjs from sprites/*.gif. Do not edit by hand. -// Each frame is rows of palette letters; '.' is transparent. - -export const SPRITE = ${JSON.stringify(out, null, 2)} -` -writeFileSync(join(ROOT, 'hooks', 'frames.js'), body) -console.log(`wrote hooks/frames.js: ${out.width}x${out.height}px, ${ANIMS.map((a) => `${a} ${out.variants.default[a].length}f`).join(', ')}`) diff --git a/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json b/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json new file mode 100644 index 00000000..c32b2c91 --- /dev/null +++ b/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "pokemon", + "version": "0.4.0", + "description": "A pixel Pokémon that lives at the right edge of the band above the prompt", + "author": { "name": "deniz gökçin" } +} diff --git a/ai-stuff/claude/mods/pokemon/README.md b/ai-stuff/claude/mods/pokemon/README.md new file mode 100644 index 00000000..c16135ab --- /dev/null +++ b/ai-stuff/claude/mods/pokemon/README.md @@ -0,0 +1,62 @@ +# pokemon + +A Claude Code mod that draws one pixel Pokémon at the right edge of the band +above the prompt. It bobs in place while you're idle and paces back and forth +while Claude works. + +## Requirements + +- Claude Code v2.1.287 or later +- The terminal app. The Desktop app has no `Raster`, so the mod draws nothing there. +- A truecolor terminal (iTerm2, Ghostty, kitty, WezTerm) + +## Usage + +| Command | Effect | +| --- | --- | +| `/pokemon` | Show which mon and variant are active, and the options | +| `/pokemon abra`, `/pokemon bulbasaur` | Pick a mon, saved across sessions | +| `/pokemon shiny`, `/pokemon default` | Pick a variant, saved across sessions | +| Ctrl+X Ctrl+A | Collapse or expand the band (Claude Code's own binding) | + +## Mons + +| Mon | Pixels | Band rows | Frames (default) | +| --- | --- | --- | --- | +| abra | 19x19 | 10 | 2 idle, 2 walk at 300 ms | +| bulbasaur | 20x17 | 9 | 6 idle, 6 walk at 100 ms | + +## Add a mon + +1. Copy the four GIFs from `media/gen//` in [jakobhoeg/vscode-pokemon](https://github.com/jakobhoeg/vscode-pokemon/tree/main/media) into `sprites//`. + The files are `default_idle_8fps.gif`, `default_walk_8fps.gif`, `shiny_idle_8fps.gif`, and `shiny_walk_8fps.gif`. +2. Run `node scripts/build-frames.mjs`. +3. Bump `version` in `.claude-plugin/plugin.json`, then run `claude plugin update pokemon@dotfiles-mods`. + +## Layout + +| Path | Contents | +| --- | --- | +| `hooks/register.js` | Band renderer, animation timer, `/pokemon` command | +| `hooks/frames.js` | Generated pixel frames. Don't edit by hand. | +| `sprites//*.gif` | Source GIFs, 32x32 | +| `scripts/build-frames.mjs` | Regenerates `frames.js` from the GIFs with ffmpeg | +| `tests/pokemon.test.ts` | `claude plugin test` suite | + +## Development + +```bash +node scripts/build-frames.mjs # after changing sprites/ +claude plugin validate . +claude plugin test +claude --plugin-dir . # live-reloading session +``` + +Each terminal cell holds two vertical pixels with `▀`/`▄` half blocks. The +sprite paces inside a 40 column strip. + +## Credits + +Sprites come from [jakobhoeg/vscode-pokemon](https://github.com/jakobhoeg/vscode-pokemon/tree/main/media). +Pokémon sprites are © The Pokémon Company / Nintendo / Game Freak, used here +for a personal, non-commercial fan project. diff --git a/ai-stuff/claude/mods/pokemon/hooks/frames.js b/ai-stuff/claude/mods/pokemon/hooks/frames.js new file mode 100644 index 00000000..9f1f03db --- /dev/null +++ b/ai-stuff/claude/mods/pokemon/hooks/frames.js @@ -0,0 +1,627 @@ +// Generated by scripts/build-frames.mjs from sprites//*.gif. Do not edit by hand. +// Each frame is its duration and rows of palette letters; '.' is transparent. + +export const SPRITES = { + "abra": { + "width": 19, + "height": 19, + "variants": { + "default": { + "palette": [ + "#000000", + "#483818", + "#b89828", + "#f8d860", + "#907860", + "#786038" + ], + "idle": [ + { + "ms": 300, + "rows": [ + "...................", + "...................", + "........aaaaa......", + "....bbbacdddcabbb..", + "....bddcdddddcddb..", + "....bcdddddddddcb..", + ".....bdddddddddb...", + "....bcdddddddddcb..", + "....acbadddddabca..", + "....acddadddaddca..", + ".....aadddddddaa...", + "....beeaadddaaeeb..", + "....affbcaaacbffa..", + ".....aaccbfbccaa...", + "...bbdcaccbccacdbb.", + "...bdccaddaddaccdb.", + "....bbccaaaaaccbb..", + "......aa.....aa....", + "..................." + ] + }, + { + "ms": 300, + "rows": [ + "...................", + "........aaaaa......", + "....bbbacdddcabbb..", + "....bddcdddddcddb..", + "....bcdddddddddcb..", + ".....bdddddddddb...", + "....bcdddddddddcb..", + "....acbadddddabca..", + "....acddadddaddca..", + ".....aadddddddaa...", + "....beeaadddaaeeb..", + "....affbcaaacbffa..", + ".....aaccbfbccaa...", + "...bbdcaccbccacdbb.", + "...bdccaddaddaccdb.", + "....bbccaaaaaccbb..", + "......aa.....aa....", + "...................", + "..................." + ] + } + ], + "walk": [ + { + "ms": 300, + "rows": [ + "...................", + ".........bb........", + "........bccbba.....", + "........aabcddba...", + ".......addcbdddda..", + ".......accddddddda.", + "........accddddddb.", + ".......bcbdddddddda", + ".......acddddbadddb", + ".......adddddddaddb", + ".bb.....accdddddbb.", + "bcb....afaaaaaaa...", + "bccbbb.afffab......", + "bccfccbcaaacbbb....", + ".bffcccaffbacaaa...", + "..bbcccabbccacda...", + "....babbaccccdb....", + ".........aabccb....", + "............bb....." + ] + }, + { + "ms": 300, + "rows": [ + ".........bb........", + "........bccbba.....", + "........aabcddba...", + ".......addcbdddda..", + ".......accddddddda.", + "........accddddddb.", + ".......bcbdddddddda", + "..bb...acddddbadddb", + ".bcb...adddddddaddb", + ".bcb....accdddddbb.", + ".bcfb..afaaaaaaa...", + ".affcbbafffab......", + "..bcccccaaacbbb....", + "..accccaffbacaaa...", + "...abccabbccacda...", + ".....abbaccccdb....", + ".........aabccb....", + "............bb.....", + "..................." + ] + } + ] + }, + "shiny": { + "palette": [ + "#000000", + "#604820", + "#d8b870", + "#f8e0a0", + "#a89080", + "#a08048" + ], + "idle": [ + { + "ms": 300, + "rows": [ + "...................", + "...................", + "........aaaaa......", + "....bbbacdddcabbb..", + "....bddcdddddcddb..", + "....bcdddddddddcb..", + ".....bdddddddddb...", + "....bcdddddddddcb..", + "....acbadddddabca..", + "....acddadddaddca..", + ".....aadddddddaa...", + "....beeaadddaaeeb..", + "....affbcaaacbffa..", + ".....aaccbfbccaa...", + "...bbdcaccbccacdbb.", + "...bdccaddaddaccdb.", + "....bbccaaaaaccbb..", + "......aa.....aa....", + "..................." + ] + }, + { + "ms": 300, + "rows": [ + "...................", + "........aaaaa......", + "....bbbacdddcabbb..", + "....bddcdddddcddb..", + "....bcdddddddddcb..", + ".....bdddddddddb...", + "....bcdddddddddcb..", + "....acbadddddabca..", + "....acddadddaddca..", + ".....aadddddddaa...", + "....beeaadddaaeeb..", + "....affbcaaacbffa..", + ".....aaccbfbccaa...", + "...bbdcaccbccacdbb.", + "...bdccaddaddaccdb.", + "....bbccaaaaaccbb..", + "......aa.....aa....", + "...................", + "..................." + ] + } + ], + "walk": [ + { + "ms": 300, + "rows": [ + "...................", + ".........bb........", + "........bccbba.....", + "........aabcddba...", + ".......addcbdddda..", + ".......accddddddda.", + "........accddddddb.", + ".......bcbdddddddda", + ".......acddddbadddb", + ".......adddddddaddb", + ".bb.....accdddddbb.", + "bcb....afaaaaaaa...", + "bccbbb.afffab......", + "bccfccbcaaacbbb....", + ".bffcccaffbacaaa...", + "..bbcccabbccacda...", + "....babbaccccdb....", + ".........aabccb....", + "............bb....." + ] + }, + { + "ms": 300, + "rows": [ + ".........bb........", + "........bccbba.....", + "........aabcddba...", + ".......addcbdddda..", + ".......accddddddda.", + "........accddddddb.", + ".......bcbdddddddda", + "..bb...acddddbadddb", + ".bcb...adddddddaddb", + ".bcb....accdddddbb.", + ".bcfb..afaaaaaaa...", + ".affcbbafffab......", + "..bcccccaaacbbb....", + "..accccaffbacaaa...", + "...abccabbccacda...", + ".....abbaccccdb....", + ".........aabccb....", + "............bb.....", + "..................." + ] + } + ] + } + } + }, + "bulbasaur": { + "width": 20, + "height": 17, + "variants": { + "default": { + "palette": [ + "#406030", + "#60a810", + "#000000", + "#88e020", + "#404040", + "#68d078", + "#409850", + "#e8e8f8", + "#681018", + "#c8c8c8", + "#b83018" + ], + "idle": [ + { + "ms": 100, + "rows": [ + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + "..ecfgeeeeegfcge....", + ".egcfffffgfffcge....", + ".egefffggffffea.....", + ".ecgfffggffffgc.....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..ecggfffffggca.....", + "..cgccggfggccga.....", + "...ccacccccaggc.....", + "............cc......", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + "..ecfgeeeeegfcge....", + ".egcfffffgfffcge....", + ".egefffggffffea.....", + ".ecgfffggffffgc.....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..ecggfffffggca.....", + "..cgccggfggccga.....", + "...ccacccccaggc.....", + "............cc......", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + "..ecfgeeeeegfcge....", + ".egcfffffgfffcge....", + ".egefffggffffea.....", + ".ecgfffggffffgc.....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..ecggfffffggca.....", + "..cgccggfggccga.....", + "...ccacccccaggc.....", + "............cc......", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + ".egcfgeeeeegfce.....", + ".egcfffffgfffcge....", + "..aefffggffffege....", + "..cgfffggffffgce....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..acggfffffggce.....", + "..agccggfggccgc.....", + "..cggacccccacc......", + "...cc..............." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + ".egcfgeeeeegfce.....", + ".egcfffffgfffcge....", + "..aefffggffffege....", + "..cgfffggffffgce....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..acggfffffggce.....", + "..agccggfggccgc.....", + "..cggacccccacc......", + "...cc..............." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + ".egcfgeeeeegfce.....", + ".egcfffffgfffcge....", + "..aefffggffffege....", + "..cgfffggffffgce....", + "..cghigfffgihgc.....", + "..cgjkfffffkjgc.....", + "..acggfffffggce.....", + "..agccggfggccgc.....", + "..cggacccccacc......", + "...cc..............." + ] + } + ], + "walk": [ + { + "ms": 100, + "rows": [ + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "....cggfffggaaccc...", + "...cggggccgggaa.....", + "...cggccaaacggc.....", + "....cc......cc......", + "....................", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "....cggfffggaaccc...", + "...cggggccgggaa.....", + "...cggccaaacggc.....", + "....cc......cc......", + "....................", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "....cggfffggaaccc...", + "...cggggccgggaa.....", + "...cggccaaacggc.....", + "....cc......cc......", + "....................", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "...cagggffggaaccc...", + "...cacggagggcga.....", + "....ccggccggca......", + "......cc..cc........", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "...cagggffggaaccc...", + "...cacggagggcga.....", + "....ccggccggca......", + "......cc..cc........", + "...................." + ] + }, + { + "ms": 100, + "rows": [ + "....................", + "......aaaa..........", + "...ccadddba.aaa.....", + "..cbbdddbbbccgfaa...", + "..cbbbbbbecgfffgga..", + "..abddbbeffffffggfa.", + ".abbddbbegffffffffa.", + ".abbbbbbeggffffffgfa", + ".abbbbbbcgfffffffffa", + "..abbbbcffgfffhifffa", + "...ccccfffafffjkgfga", + "....cgfffffafffffac.", + "...cagggffggaaccc...", + "...cacggagggcga.....", + "....ccggccggca......", + "......cc..cc........", + "...................." + ] + } + ] + }, + "shiny": { + "palette": [ + "#406030", + "#207840", + "#000000", + "#50a058", + "#404040", + "#709830", + "#a0d038", + "#e8e8f8", + "#681018", + "#d8d8f8", + "#b83018" + ], + "idle": [ + { + "ms": 300, + "rows": [ + "....................", + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + ".efcgfeeeeefgce.....", + ".efcgggggfgggcfe....", + "..aegggffggggefe....", + "..cfgggffggggfce....", + "..cfhifgggfihfc.....", + "..cfjkgggggkjfc.....", + "..acffgggggffce.....", + "..afccffgffccfc.....", + "..cffacccccacc......", + "...cc..............." + ] + }, + { + "ms": 300, + "rows": [ + ".......aaa..........", + "......ababa.........", + "....ccbdbbbcc.......", + "...cbddddbbdbc......", + "..cbbbdddbddbbc.....", + "..cbeabbbbbaebc.....", + "..ecgfeeeeefgcfe....", + ".efcgggggfgggcfe....", + ".efegggffggggea.....", + ".ecfgggffggggfc.....", + "..cfhifgggfihfc.....", + "..cfjkgggggkjfc.....", + "..ecffgggggffca.....", + "..cfccffgffccfa.....", + "...ccacccccaffc.....", + "............cc......", + "...................." + ] + } + ], + "walk": [ + { + "ms": 300, + "rows": [ + "....................", + ".....aaaa...........", + "..ccadddba.aaa......", + ".cbbdddbbbccfgaa....", + ".cbbbbbbecfgggffa...", + ".abddbbeggggggffga..", + "abbddbbefgggggggga..", + "abbbbbbeffggggggfga.", + "abbbbbbcfggggggggga.", + ".abbbbcggfggghiggga.", + "..ccccgggagggjkfgfa.", + "...cfgggggagggggac..", + "..cafffggffaaccc....", + "..cacffafffcfa......", + "...ccffccffca.......", + ".....cc..cc.........", + "...................." + ] + }, + { + "ms": 300, + "rows": [ + ".....aaaa...........", + "..ccadddba.aaa......", + ".cbbdddbbbccfgaa....", + ".cbbbbbbecfgggffa...", + ".abddbbeggggggffga..", + "abbddbbefgggggggga..", + "abbbbbbeffggggggfga.", + "abbbbbbcfggggggggga.", + ".abbbbcggfggghiggga.", + "..ccccgggagggjkfgfa.", + "...cfgggggagggggac..", + "...cffgggffaaccc....", + "..cffffccfffaa......", + "..cffccaaacffc......", + "...cc......cc.......", + "....................", + "...................." + ] + } + ] + } + } + } +} diff --git a/ai-stuff/claude/mods/abra/hooks/hooks.json b/ai-stuff/claude/mods/pokemon/hooks/hooks.json similarity index 100% rename from ai-stuff/claude/mods/abra/hooks/hooks.json rename to ai-stuff/claude/mods/pokemon/hooks/hooks.json diff --git a/ai-stuff/claude/mods/pokemon/hooks/register.js b/ai-stuff/claude/mods/pokemon/hooks/register.js new file mode 100644 index 00000000..b8d567a3 --- /dev/null +++ b/ai-stuff/claude/mods/pokemon/hooks/register.js @@ -0,0 +1,156 @@ +import { SPRITES } from './frames.js' + +// One pixel Pokémon lives at the right edge of the band above the prompt. It +// bobs in place while you're idle and paces back and forth while Claude works. + +const TICK_MS = 50 +const MOVE_TICKS = 3 +const STRIP_COLUMNS = 40 +const DEFAULT_COLOR = 0x01000000 +const UPPER_HALF = 0x2580 +const LOWER_HALF = 0x2584 +const SPACE = 0x20 +const MONS = Object.keys(SPRITES) +const VARIANTS = ['default', 'shiny'] + +let mon = MONS.includes('abra') ? 'abra' : MONS[0] +let variant = 'default' +let working = false +let bandId = null +let columns = STRIP_COLUMNS +let tick = 0 +let x = STRIP_COLUMNS +let facing = 'left' + +// Palette letter to 0xRRGGBB, per mon and variant +const COLORS = Object.fromEntries( + MONS.map((name) => [ + name, + Object.fromEntries( + VARIANTS.map((v) => [ + v, + Object.fromEntries( + SPRITES[name].variants[v].palette.map((hex, i) => [String.fromCharCode(97 + i), parseInt(hex.slice(1), 16)]), + ), + ]), + ), + ]), +) + +const rowsOf = (name) => Math.ceil(SPRITES[name].height / 2) + +// The frame showing after elapsed milliseconds, looping over each frame's own duration +function frameAt(anim, elapsed) { + const total = anim.reduce((sum, f) => sum + f.ms, 0) + let t = elapsed % total + for (const f of anim) { + if (t < f.ms) return f.rows + t -= f.ms + } + return anim[0].rows +} + +// Pack the current frame into Raster cells, two pixels per cell with half blocks +function cellsNow() { + const sprite = SPRITES[mon] + const sheet = sprite.variants[variant] + const frame = frameAt(working ? sheet.walk : sheet.idle, tick * TICK_MS) + const colors = COLORS[mon][variant] + const flip = working && facing === 'left' + const pixel = (px, py) => { + if (px < 0 || px >= sprite.width) return null + const row = frame[py] + if (!row) return null + return colors[row[flip ? sprite.width - 1 - px : px]] ?? null + } + + const rows = rowsOf(mon) + const words = new Uint32Array(columns * rows * 3) + for (let cy = 0; cy < rows; cy++) { + for (let cx = 0; cx < columns; cx++) { + const top = pixel(cx - x, cy * 2) + const bottom = pixel(cx - x, cy * 2 + 1) + const i = (cy * columns + cx) * 3 + if (top !== null) words.set([UPPER_HALF, top, bottom ?? DEFAULT_COLOR], i) + else if (bottom !== null) words.set([LOWER_HALF, bottom, DEFAULT_COLOR], i) + else words.set([SPACE, DEFAULT_COLOR, DEFAULT_COLOR], i) + } + } + return new Uint8Array(words.buffer).toBase64() +} + +// Keep the sprite inside the strip +function clampX() { + x = Math.max(0, Math.min(columns - SPRITES[mon].width, x)) +} + +// Advance the clock, and while working move one column every few ticks, turning at the edges +function step() { + tick += 1 + if (!working || tick % MOVE_TICKS !== 0) return + const maxX = columns - SPRITES[mon].width + if (facing === 'left' && x <= 0) facing = 'right' + else if (facing === 'right' && x >= maxX) facing = 'left' + x += facing === 'left' ? -1 : 1 + clampX() +} + +export function register(on) { + on('session.start', async ($, e, next) => { + await $.command.register({ + name: 'pokemon', + description: 'Pick the Pokémon above the prompt, or switch it to shiny', + argumentHint: '[' + [...MONS, ...VARIANTS].join('|') + ']', + immediate: true, + }) + const savedMon = await $.store.get('mon') + if (MONS.includes(savedMon)) mon = savedMon + const savedVariant = await $.store.get('variant') + if (VARIANTS.includes(savedVariant)) variant = savedVariant + $.clock.every(TICK_MS, () => { + step() + if (bandId !== null) { + $.ui.blit({ requestId: bandId, key: 'pokemon', columns, rows: rowsOf(mon), cells: cellsNow() }) + } + }) + return next(e) + }) + + on('command.run', { command: 'pokemon' }, async ($, e) => { + const asked = e.args.trim().toLowerCase() + if (MONS.includes(asked)) { + mon = asked + clampX() + await $.store.set('mon', mon) + } else if (VARIANTS.includes(asked)) { + variant = asked + await $.store.set('variant', variant) + } else if (asked) { + return { text: 'Unknown option "' + asked + '". Try one of: ' + [...MONS, ...VARIANTS].join(', ') + '.' } + } else { + return { text: 'Showing ' + variant + ' ' + mon + '. Options: ' + [...MONS, ...VARIANTS].join(', ') + '.' } + } + $.ui.invalidate('ui.render') + return { text: 'Now showing ' + variant + ' ' + mon + '.' } + }) + + on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { + if (e.surface !== 'terminal' || e.props.hasSurvey) { + bandId = null + return next(e) + } + const { Box, Raster } = $.ui.resolve(e) + working = e.props.isWorking + bandId = e.requestId + columns = Math.max(SPRITES[mon].width, Math.min(STRIP_COLUMNS, e.props.bodyColumns)) + clampX() + + const sprite = Raster({ key: 'pokemon', columns, rows: rowsOf(mon), cells: cellsNow() }) + const theirs = await next(e) + return Box({ + flexDirection: 'row', + justifyContent: theirs ? 'space-between' : 'flex-end', + children: theirs ? [Box({ flexGrow: 1, children: [theirs] }), sprite] : [sprite], + }) + }) +} diff --git a/ai-stuff/claude/mods/pokemon/scripts/build-frames.mjs b/ai-stuff/claude/mods/pokemon/scripts/build-frames.mjs new file mode 100644 index 00000000..5fdb2462 --- /dev/null +++ b/ai-stuff/claude/mods/pokemon/scripts/build-frames.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +// Decodes sprites//*.gif with ffmpeg and writes hooks/frames.js. +// Each frame becomes rows of palette letters plus its duration, cropped per mon +// to the box every one of its frames fits in. +// Add a mon by dropping its four GIFs from jakobhoeg/vscode-pokemon into sprites//. +// Run from anywhere: node scripts/build-frames.mjs + +import { execFileSync } from 'node:child_process' +import { readdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const SIZE = 32 +const VARIANTS = ['default', 'shiny'] +const ANIMS = ['idle', 'walk'] +const LETTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +function decode(file) { + const raw = execFileSync('ffmpeg', [ + '-v', 'error', '-i', file, '-fps_mode', 'passthrough', + '-f', 'rawvideo', '-pix_fmt', 'rgba', '-', + ]) + const durations = execFileSync('ffprobe', [ + '-v', 'error', '-show_entries', 'frame=duration_time', '-of', 'csv=p=0', file, + ]).toString().trim().split('\n').map((s) => Math.round(Number(s) * 1000) || 100) + + const frameBytes = SIZE * SIZE * 4 + const frames = [] + for (let off = 0, k = 0; off < raw.length; off += frameBytes, k++) { + const px = [] + for (let i = 0; i < SIZE * SIZE; i++) { + const p = off + i * 4 + px.push(raw[p + 3] > 127 ? (raw[p] << 16) | (raw[p + 1] << 8) | raw[p + 2] : null) + } + frames.push({ ms: durations[k] ?? 100, px }) + } + return frames +} + +function buildMon(mon) { + const sheets = {} + for (const v of VARIANTS) { + sheets[v] = {} + for (const a of ANIMS) sheets[v][a] = decode(join(ROOT, 'sprites', mon, `${v}_${a}_8fps.gif`)) + } + + let minX = SIZE, minY = SIZE, maxX = -1, maxY = -1 + for (const v of VARIANTS) for (const a of ANIMS) for (const { px } of sheets[v][a]) { + px.forEach((c, i) => { + if (c === null) return + const x = i % SIZE, y = Math.floor(i / SIZE) + minX = Math.min(minX, x); maxX = Math.max(maxX, x) + minY = Math.min(minY, y); maxY = Math.max(maxY, y) + }) + } + + const out = { width: maxX - minX + 1, height: maxY - minY + 1, variants: {} } + for (const v of VARIANTS) { + const palette = [] + const letter = (c) => { + if (c === null) return '.' + let i = palette.indexOf(c) + if (i < 0) i = palette.push(c) - 1 + return LETTERS[i] + } + const anims = {} + for (const a of ANIMS) { + anims[a] = sheets[v][a].map(({ ms, px }) => { + const rows = [] + for (let y = minY; y <= maxY; y++) { + let row = '' + for (let x = minX; x <= maxX; x++) row += letter(px[y * SIZE + x]) + rows.push(row) + } + return { ms, rows } + }) + } + out.variants[v] = { palette: palette.map((c) => '#' + c.toString(16).padStart(6, '0')), ...anims } + } + return out +} + +const mons = readdirSync(join(ROOT, 'sprites'), { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort() + +const sprites = Object.fromEntries(mons.map((mon) => [mon, buildMon(mon)])) + +const body = `// Generated by scripts/build-frames.mjs from sprites//*.gif. Do not edit by hand. +// Each frame is its duration and rows of palette letters; '.' is transparent. + +export const SPRITES = ${JSON.stringify(sprites, null, 2)} +` +writeFileSync(join(ROOT, 'hooks', 'frames.js'), body) +for (const [mon, s] of Object.entries(sprites)) { + const counts = ANIMS.map((a) => `${a} ${s.variants.default[a].length}f`).join(', ') + console.log(`${mon}: ${s.width}x${s.height}px, ${counts}`) +} diff --git a/ai-stuff/claude/mods/abra/sprites/default_idle_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/abra/default_idle_8fps.gif similarity index 100% rename from ai-stuff/claude/mods/abra/sprites/default_idle_8fps.gif rename to ai-stuff/claude/mods/pokemon/sprites/abra/default_idle_8fps.gif diff --git a/ai-stuff/claude/mods/abra/sprites/default_walk_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/abra/default_walk_8fps.gif similarity index 100% rename from ai-stuff/claude/mods/abra/sprites/default_walk_8fps.gif rename to ai-stuff/claude/mods/pokemon/sprites/abra/default_walk_8fps.gif diff --git a/ai-stuff/claude/mods/abra/sprites/shiny_idle_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/abra/shiny_idle_8fps.gif similarity index 100% rename from ai-stuff/claude/mods/abra/sprites/shiny_idle_8fps.gif rename to ai-stuff/claude/mods/pokemon/sprites/abra/shiny_idle_8fps.gif diff --git a/ai-stuff/claude/mods/abra/sprites/shiny_walk_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/abra/shiny_walk_8fps.gif similarity index 100% rename from ai-stuff/claude/mods/abra/sprites/shiny_walk_8fps.gif rename to ai-stuff/claude/mods/pokemon/sprites/abra/shiny_walk_8fps.gif diff --git a/ai-stuff/claude/mods/pokemon/sprites/bulbasaur/default_idle_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/bulbasaur/default_idle_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..440f81e02573db205f46bbb42ecbde10e6119689 GIT binary patch literal 793 zcmZ?wbh9u|RA5kGC}&_${Lk&@8WQa67~pE8XTZ$J02KPk!pa4tb$}SCNP(dl#9&~^ z5Rh<4FmP~i*kK@%utLCLMnJ}eijD^gCr+Gr@!|(W4TNN=7dYX$dauRnv%CK%X!NCI z&dXS}F6Z?&=QDh4wo4onRNUsz-qkW;yD^K$u6qUUHeDqwQR~eWB-~pV9yz=y^O2hz*-?m@%{&Q4hg}I!-?M{8CuzxN~BGLEU#&Q zKG|Al{!rC1s!z`P<-*N-ZL=RSYE&w&sLwJjjpkx?3sxwOVNPXc^vtOW?lo2N3u1Q- z@~{muHJa9^GDUQ;{xT^+On)>$T*oBW(!cWbTmHp!w%qF8d~eTh{R$$YGehw9uK8_;~9e(pPIWsGs znku(?j`lXrw{8ce4JR9zdQX%!{rK|PBGXR}TlNUtus1U+i{WB*4^b$IWlm#e>|_oO j2sUa|GWQReGe>ia%L6<$$yfND0VT29A{sj2to^8x|aF<`CA3Ik92k;dTLK zuQ?tY7ai@EFwVMjV&mfD{c^k=Pcj-DJeeg~ITQ*S8oZ~9h0D#jIZ2g~Q6zlVl9QX% z{5rL{rsx4VZ)+HV{ zTgp{(fQdDkT_a*cNm4iq=cXjzh6V4gugjXPXDWU1K+u7v&|kLN*BVY8@2kHxRmM;O z)AgW0At^{WRuJLaJ~pQ!6NiQ+jXg{(0u~MnQoXvh_)nzloYcV3r4@ZH#`E(sm#M5C z+j24&G@R{ZWnqyBSde*{k&(rqHzJ_{=-cUOQ==NRF0Wx;kjATJxbnu*^y$BPG)fzr zHZzOpY_JN{YGR4psOI3Xea)3E8LV^uWPZ(0SlL|9<#+Oi!;P&{@pUm00u0sw-ZZm) literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/pokemon/sprites/bulbasaur/shiny_walk_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/bulbasaur/shiny_walk_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..4b239408469c5298ff38b26bd16f710c20b8748f GIT binary patch literal 517 zcmZ?wbhEHbRA5kGXl4L`1s5z7DjXaV41nx{83q9hA{-nXb{I(9xbfq~iys*R5@1zO zLh(PhpKD04vtxj(k)8oFBT!iJCkv+>kk$by0U67{v6g|6L&jsnf`iQ*!dfvWHY_~c zE}-l+$7AE7qumn5S$9rsTztG=hP^{(!bFBfMz$a>1(_KWR3rUy_OD z1fvsejEtB*00j(5!9tXedzqd1PIxpNU|`l@?Gdq@*ucQ3!MjH!V3R|`1i{#IHI)qw zXBil|L?nI|F!;=5;$w)Fx}p}?=GOn|&qG2z=I|ud(|W!Q2c89VB+ukqVb$Q^w=S#wo37@?#IATLS<=b+#!0 literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/abra/tests/abra.test.ts b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts similarity index 60% rename from ai-stuff/claude/mods/abra/tests/abra.test.ts rename to ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts index d1fc7f4c..c8ca781b 100644 --- a/ai-stuff/claude/mods/abra/tests/abra.test.ts +++ b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts @@ -1,7 +1,7 @@ import { expect, mock, test } from 'claude-code/testing' const BAND = { - plugin: 'abra', + plugin: 'pokemon', component: 'AbovePrompt', requestId: 'band', viewport: { columns: 120, rows: 40 }, @@ -22,10 +22,10 @@ function codePoints(cells: string): number[] { return Array.from(words).filter((_, i) => i % 3 === 0) } -test('draws Abra as a half-block raster in the terminal band', async ($, on) => { +test('draws Abra by default as a half-block raster in the terminal band', async ($, on) => { on('ui.render', () => THEIRS) const ui = await $.ui.mount({ ...BAND, surface: 'terminal' }) - const raster = await ui.find({ key: 'abra' }) + const raster = await ui.find({ key: 'pokemon' }) expect(raster).toBeDefined() expect(raster.props.columns).toBe(40) expect(raster.props.rows).toBe(10) @@ -35,10 +35,10 @@ test('draws Abra as a half-block raster in the terminal band', async ($, on) => test('leaves the band alone on desktop and during a survey', async ($, on) => { on('ui.render', () => THEIRS) const desktop = await $.ui.mount({ ...BAND, surface: 'desktop' }) - expect(await desktop.find({ key: 'abra' })).toBeUndefined() + expect(await desktop.find({ key: 'pokemon' })).toBeUndefined() await desktop.unmount() const survey = await $.ui.mount({ ...BAND, surface: 'terminal', props: { ...BAND.props, hasSurvey: true } }) - expect(await survey.find({ key: 'abra' })).toBeUndefined() + expect(await survey.find({ key: 'pokemon' })).toBeUndefined() }) test('paces while Claude works', async ($, on) => { @@ -55,20 +55,31 @@ test('paces while Claude works', async ($, on) => { await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' }) await $.ui.mount({ ...BAND, surface: 'terminal', props: { ...BAND.props, isWorking: true } }) - await clock.advance(150 * 6) - expect(blits.length).toBe(6) - expect(new Set(blits).size).toBe(6) + await clock.advance(50 * 30) + expect(blits.length).toBe(30) + expect(new Set(blits).size).toBeGreaterThan(5) }) -test('/abra toggles shiny and saves it', async ($, on) => { +test('/pokemon switches the mon and the variant, and saves both', async ($, on) => { const saved = new Map() + on('ui.render', () => THEIRS) on('store.set', ($, e) => { saved.set(e.key, e.value) return { value: undefined } }) - const first = await $.command.run({ command: 'abra', args: '' }) - expect(first.text).toBe('Abra is now shiny.') - const second = await $.command.run({ command: 'abra', args: 'default' }) - expect(second.text).toBe('Abra is now default.') - expect(saved.get('variant')).toBe('default') + + const mon = await $.command.run({ command: 'pokemon', args: 'bulbasaur' }) + expect(mon.text).toBe('Now showing default bulbasaur.') + const shiny = await $.command.run({ command: 'pokemon', args: 'shiny' }) + expect(shiny.text).toBe('Now showing shiny bulbasaur.') + expect(saved.get('mon')).toBe('bulbasaur') + expect(saved.get('variant')).toBe('shiny') + + const ui = await $.ui.mount({ ...BAND, surface: 'terminal' }) + expect((await ui.find({ key: 'pokemon' })).props.rows).toBe(9) +}) + +test('/pokemon rejects an unknown name', async ($) => { + const answer = await $.command.run({ command: 'pokemon', args: 'mewtwo' }) + expect(answer.text).toMatch(/^Unknown option "mewtwo"/) }) diff --git a/ai-stuff/claude/settings.json b/ai-stuff/claude/settings.json index b31d00fd..592ff5e4 100644 --- a/ai-stuff/claude/settings.json +++ b/ai-stuff/claude/settings.json @@ -261,7 +261,6 @@ "command": "~/.claude/scripts/subagent-statusline.sh" }, "enabledPlugins": { - "abra@dotfiles-mods": true, "context7@claude-plugins-official": false, "datadog@treatwell": true, "feature-dev@claude-plugins-official": false, @@ -271,7 +270,8 @@ "pyright-lsp@claude-plugins-official": false, "ralph-loop@claude-plugins-official": false, "skill-creator@claude-plugins-official": false, - "typescript-lsp@claude-plugins-official": false + "typescript-lsp@claude-plugins-official": false, + "pokemon@dotfiles-mods": true }, "extraKnownMarketplaces": { "agent-plugins-for-aws": { From eca332c4d44d16b99f5a9359763d197fa954a81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:18:26 +0200 Subject: [PATCH 2/5] feat(pokemon): add charmander sprite - add default and shiny charmander sprites with idle and walk frames - add test for /pokemon charmander - document charmander in readme - bump plugin version to 0.5.0 --- .../mods/pokemon/.claude-plugin/plugin.json | 2 +- ai-stuff/claude/mods/pokemon/README.md | 3 +- ai-stuff/claude/mods/pokemon/hooks/frames.js | 216 ++++++++++++++++++ .../sprites/charmander/default_idle_8fps.gif | Bin 0 -> 455 bytes .../sprites/charmander/default_walk_8fps.gif | Bin 0 -> 493 bytes .../sprites/charmander/shiny_idle_8fps.gif | Bin 0 -> 455 bytes .../sprites/charmander/shiny_walk_8fps.gif | Bin 0 -> 493 bytes .../claude/mods/pokemon/tests/pokemon.test.ts | 9 + 8 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 ai-stuff/claude/mods/pokemon/sprites/charmander/default_idle_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/charmander/default_walk_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_idle_8fps.gif create mode 100644 ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_walk_8fps.gif diff --git a/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json b/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json index c32b2c91..92e83ff4 100644 --- a/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json +++ b/ai-stuff/claude/mods/pokemon/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pokemon", - "version": "0.4.0", + "version": "0.5.0", "description": "A pixel Pokémon that lives at the right edge of the band above the prompt", "author": { "name": "deniz gökçin" } } diff --git a/ai-stuff/claude/mods/pokemon/README.md b/ai-stuff/claude/mods/pokemon/README.md index c16135ab..306e28b1 100644 --- a/ai-stuff/claude/mods/pokemon/README.md +++ b/ai-stuff/claude/mods/pokemon/README.md @@ -15,7 +15,7 @@ while Claude works. | Command | Effect | | --- | --- | | `/pokemon` | Show which mon and variant are active, and the options | -| `/pokemon abra`, `/pokemon bulbasaur` | Pick a mon, saved across sessions | +| `/pokemon abra`, `/pokemon bulbasaur`, `/pokemon charmander` | Pick a mon, saved across sessions | | `/pokemon shiny`, `/pokemon default` | Pick a variant, saved across sessions | | Ctrl+X Ctrl+A | Collapse or expand the band (Claude Code's own binding) | @@ -25,6 +25,7 @@ while Claude works. | --- | --- | --- | --- | | abra | 19x19 | 10 | 2 idle, 2 walk at 300 ms | | bulbasaur | 20x17 | 9 | 6 idle, 6 walk at 100 ms | +| charmander | 19x17 | 9 | 2 idle, 2 walk at 300 ms | ## Add a mon diff --git a/ai-stuff/claude/mods/pokemon/hooks/frames.js b/ai-stuff/claude/mods/pokemon/hooks/frames.js index 9f1f03db..b5b9c73a 100644 --- a/ai-stuff/claude/mods/pokemon/hooks/frames.js +++ b/ai-stuff/claude/mods/pokemon/hooks/frames.js @@ -623,5 +623,221 @@ export const SPRITES = { ] } } + }, + "charmander": { + "width": 19, + "height": 17, + "variants": { + "default": { + "palette": [ + "#604020", + "#f88838", + "#b85820", + "#e8e8f8", + "#284858", + "#000000", + "#d8a830", + "#f0d070", + "#e02828" + ], + "idle": [ + { + "ms": 300, + "rows": [ + "...................", + ".....aaaaa.........", + "....abbbbba........", + "...acbbbbbca.......", + "...abbbbbbba.......", + "...abcbbbcba.......", + "..acdbbbbbdca......", + "..acebbbbbeca......", + "..fcebbbbbecf......", + "...fccbbbccf.......", + "...afcccccfa.......", + "...fcfffffcf.......", + "...fcaghgacf.......", + "..acffhhhffca......", + "..accghhhgcca......", + "..accfaaafff.......", + "...ff.............." + ] + }, + { + "ms": 300, + "rows": [ + ".....aaaaa.........", + "....abbbbba........", + "...acbbbbbca.......", + "...abbbbbbba.......", + "...abcbbbcba.......", + "..acdbbbbbdca......", + "..acebbbbbeca......", + "..fcebbbbbecf......", + "...fccbbbccf.......", + "...afcccccfa.......", + "...fcfffffcf.......", + "...fcaghgacf.......", + "..acffhhhffca......", + "..accghhhgcca......", + "...fffaaafcca......", + "..........ff.......", + "..................." + ] + } + ], + "walk": [ + { + "ms": 300, + "rows": [ + "...................", + "...........aaaa....", + ".i........abbbba...", + ".ii......abbbbbba..", + ".hi......abbbbbbc..", + "iiii....abbbbbbcba.", + "ihhi....abbbbbdcba.", + ".ihi....abbbbbebbba", + ".ii....abbbbbbebbbf", + ".fca...abbbcabbbcf.", + ".fccaaabbbcbcaffa..", + "..fccbbbbcabbca....", + "..fccccbbbcffff....", + "...ffcccbbccga.....", + ".....ffacccca......", + ".......fccaa.......", + "........fff........" + ] + }, + { + "ms": 300, + "rows": [ + "...........aaaa....", + "...i......abbbba...", + "...i.....abbbbbba..", + "..ii.....abbbbbbc..", + "..iii...abbbbbbcba.", + ".iihi...abbbbbdcba.", + ".ihhi...abbbbbebbba", + ".ahi...abbbbbbebbbf", + ".fca...abbbcabbbcf.", + ".fccaaabbbcbcaffa..", + "..fccbbbbbabbca....", + "..fccccbbbbffff....", + "...ffccccbbcga.....", + ".....ffaacccca.....", + "........afcca......", + "..........fff......", + "..................." + ] + } + ] + }, + "shiny": { + "palette": [ + "#604020", + "#f8c038", + "#f88838", + "#e8e8f8", + "#284858", + "#000000", + "#d8a830", + "#f0d070", + "#e02828" + ], + "idle": [ + { + "ms": 300, + "rows": [ + "...................", + ".....aaaaa.........", + "....abbbbba........", + "...acbbbbbca.......", + "...abbbbbbba.......", + "...abcbbbcba.......", + "..acdbbbbbdca......", + "..acebbbbbeca......", + "..fcebbbbbecf......", + "...fccbbbccf.......", + "...afcccccfa.......", + "...fcfffffcf.......", + "...fcaghgacf.......", + "..acffhhhffca......", + "..accghhhgcca......", + "..accfaaafff.......", + "...ff.............." + ] + }, + { + "ms": 300, + "rows": [ + ".....aaaaa.........", + "....abbbbba........", + "...acbbbbbca.......", + "...abbbbbbba.......", + "...abcbbbcba.......", + "..acdbbbbbdca......", + "..acebbbbbeca......", + "..fcebbbbbecf......", + "...fccbbbccf.......", + "...afcccccfa.......", + "...fcfffffcf.......", + "...fcaghgacf.......", + "..acffhhhffca......", + "..accghhhgcca......", + "...fffaaafcca......", + "..........ff.......", + "..................." + ] + } + ], + "walk": [ + { + "ms": 300, + "rows": [ + "...................", + "...........aaaa....", + ".i........abbbba...", + ".ii......abbbbbba..", + ".hi......abbbbbbc..", + "iiii....abbbbbbcba.", + "ihhi....abbbbbdcba.", + ".ihi....abbbbbebbba", + ".ii....abbbbbbebbbf", + ".fca...abbbcabbbcf.", + ".fccaaabbbcbcaffa..", + "..fccbbbbcabbca....", + "..fccccbbbcffff....", + "...ffcccbbccga.....", + ".....ffacccca......", + ".......fccaa.......", + "........fff........" + ] + }, + { + "ms": 300, + "rows": [ + "...........aaaa....", + "...i......abbbba...", + "...i.....abbbbbba..", + "..ii.....abbbbbbc..", + "..iii...abbbbbbcba.", + ".iihi...abbbbbdcba.", + ".ihhi...abbbbbebbba", + ".ahi...abbbbbbebbbf", + ".fca...abbbcabbbcf.", + ".fccaaabbbcbcaffa..", + "..fccbbbbbabbca....", + "..fccccbbbbffff....", + "...ffccccbbcga.....", + ".....ffaacccca.....", + "........afcca......", + "..........fff......", + "..................." + ] + } + ] + } + } } } diff --git a/ai-stuff/claude/mods/pokemon/sprites/charmander/default_idle_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/charmander/default_idle_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c21410a7f9ea26333c7f1edc93bf1b731551b0f GIT binary patch literal 455 zcmZ?wbhEHbRA5kGXl4L`9~~AuA`}uF6oBjx7YZ~yB5tfOc=6%~NDc!i{^#~{4GDI3 z3~)8lGhk)}Dp35%!YK!&bwG+i#xij9Gca<i1JUAgXz_%Y$7;qafkw_0BX-k%-e*HKJA~ zUzeLO%k+`g8pYjlPkv2Ia=>&W$m=9|oTHbppN=sqIM(qvOnSo5!^|>8Mf1Uuqm0_# zyVh_7tDa^sR$t~KAnfQpi;3rm$%n~GjSHMv1vXS%Nn>F2(&x+I3e7y%>CTyPRcdPH rX=YZwB{5tf*^QfH);sxVGQ7FT9B|d?j?|`=G0ReHU*$>&FjxZsTO*QF literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/pokemon/sprites/charmander/default_walk_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/charmander/default_walk_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff15b6d07f2d3f40d7ab6a80a32e55b149cee288 GIT binary patch literal 493 zcmZ?wbhEHbRA5kGXl4L`9~~A64hlOW6oBjp8X6xi6li!vym;~B#tH+FG%8U1&+X?L z671|4;A*62z|07gRs6}qDF>u=Kq^7TGH@(nVC0bT*s$PWGl#HN%!v&P54Q^_d(H9K zxaerNgmKoL6B`#F?`LN15K-93;Kaxn#HPTxvEgKsM(DDXmlqCrHyNh#RlK;Q>Niu9 zeN9Qkfrhj5jRZNCp4{-lm%%bvF5qOy!t)axn0*k(n&ulx za>-_Ge$hC^QZG;ZW{kHxvYg*dogLV4d%xKGx|lNt3Jlf&Nf)gx literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_idle_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_idle_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..3d924b064b78c33963a0feb370f7dffe31a396c4 GIT binary patch literal 455 zcmZ?wbhEHbRA5kGXl4L`9|tUcbXX)fC;-_XE)?8YVW8m=@#4h~kQ@e3{Lk&@8WQa6 z7~pE8XTZz|RG|2ig;Nel>wpx4jAh{HXJF)z@z}87U^9oXR?LYF3lFyoD0|KE*tqCu zw}f%lof8`uyZ13WwOz4LXz-lOY``Z_aA`@y6fy5@E|nWqyr(lJ1jTeFs5MO&4VRly z$)MrN#MHJ;$LoO7rP+)u92rudS6*3Z)$gZzKveT;mj}CqMnb~V>z!$wA`z#rYDBF} zzAiOkmgys}HHy3Ap8T4a qZ)R4WB{5tf*^QfH);sxVGQ5do4!G)cM{3i`m}M!puW}^>7_0#qy^_}e literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_walk_8fps.gif b/ai-stuff/claude/mods/pokemon/sprites/charmander/shiny_walk_8fps.gif new file mode 100644 index 0000000000000000000000000000000000000000..d3d38568b2565d406caef348d9893129bccf6f59 GIT binary patch literal 493 zcmZ?wbhEHbRA5kGXl4L`9|tTF929AXryP*h0VxC-%fPXSfssSTW5a@j%^bp7F()=GJlrmz>@~+@ z$^uT)tv(lwCmBvpQrW$igJm55TXqs;% z$t9b$`95}sdv77=OrzX7ipP|3ouf$ty kS=3gMtCQAti!t8r$Z~!+b#`FG?fqi!>tfCrC@@$908mq|%>V!Z literal 0 HcmV?d00001 diff --git a/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts index c8ca781b..d19c2be3 100644 --- a/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts +++ b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts @@ -83,3 +83,12 @@ test('/pokemon rejects an unknown name', async ($) => { const answer = await $.command.run({ command: 'pokemon', args: 'mewtwo' }) expect(answer.text).toMatch(/^Unknown option "mewtwo"/) }) + +test('/pokemon charmander draws Charmander', async ($, on) => { + on('ui.render', () => THEIRS) + on('store.set', () => ({ value: undefined })) + const answer = await $.command.run({ command: 'pokemon', args: 'charmander' }) + expect(answer.text).toBe('Now showing default charmander.') + const ui = await $.ui.mount({ ...BAND, surface: 'terminal' }) + expect((await ui.find({ key: 'pokemon' })).props.rows).toBe(9) +}) From 258af8788e10b3122ff6bfcc565e6a77f7c6a454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:21:21 +0200 Subject: [PATCH 3/5] feat(claude): add claude-mods make target - install every mod under mods/ from the dotfiles-mods marketplace - uninstall mods on claude-clean --- makefiles/claude.mk | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/makefiles/claude.mk b/makefiles/claude.mk index f5b4cce7..7d0fbe52 100644 --- a/makefiles/claude.mk +++ b/makefiles/claude.mk @@ -4,7 +4,7 @@ # configs/templates live at ~/.config/ai-shared (make ai-shared) — # agents reference them there, so nothing tool-specific holds content. # This file only handles what is Claude Code-specific: agents, hook scripts, -# settings, and keybindings. +# settings, keybindings, and mods. CLAUDE_HOME := ${HOME}/.claude @@ -12,6 +12,9 @@ CLAUDE_AGENTS := $(notdir $(wildcard $(DOTFILES)/ai-stuff/agents/*.md)) CLAUDE_OUTPUT_STYLES := $(notdir $(wildcard $(DOTFILES)/ai-stuff/output-styles/*.md)) +CLAUDE_MODS_DIR := $(DOTFILES)/ai-stuff/claude/mods +CLAUDE_MODS := $(patsubst $(CLAUDE_MODS_DIR)/%/.claude-plugin/plugin.json,%,$(wildcard $(CLAUDE_MODS_DIR)/*/.claude-plugin/plugin.json)) + # Pre-ai-shared layout installed by the old claude.mk — pruned on install. CLAUDE_LEGACY := ${CLAUDE_HOME}/personas ${CLAUDE_HOME}/config ${CLAUDE_HOME}/templates \ $(addprefix ${CLAUDE_HOME}/agents/,gitboi.md jiragirl.md mega-dev.md) @@ -28,7 +31,7 @@ claude-cli: ## Install Claude Code with Anthropic's recommended native installer echo "Claude Code already installed at $(HOME)/.local/bin/claude"; \ fi -claude-dotfiles: claude-dirs claude-agents claude-output-styles claude-scripts claude-settings claude-keybindings ai-shared ai-claude ## Install Claude Code configuration only +claude-dotfiles: claude-dirs claude-agents claude-output-styles claude-scripts claude-settings claude-keybindings claude-mods ai-shared ai-claude ## Install Claude Code configuration only $(call pretty_print, "Pruning legacy ~/.claude symlinks...") @rm -rf $(CLAUDE_LEGACY) @@ -67,8 +70,16 @@ claude-keybindings: claude-dirs ## Symlink Claude Code keybindings.json $(call pretty_print, "Installing Claude Code keybindings...") $(call symlink,ai-stuff/claude/keybindings.json,${CLAUDE_HOME}/keybindings.json) +# Mods load in place from the dotfiles-mods directory marketplace, so edits +# apply on /reload-plugins. This registers the marketplace and installs each mod. +claude-mods: claude-settings ## Install Claude Code mods from ai-stuff/claude/mods + $(call pretty_print, "Installing Claude Code mods...") + @claude plugin marketplace update dotfiles-mods >/dev/null 2>&1 || claude plugin marketplace add "$(CLAUDE_MODS_DIR)" + @for m in $(CLAUDE_MODS); do claude plugin install "$$m@dotfiles-mods"; done + claude-clean: ai-clean-claude ## Remove Claude Code symlinks $(call pretty_print, "Removing Claude Code symlinks...") + @for m in $(CLAUDE_MODS); do claude plugin uninstall "$$m@dotfiles-mods" >/dev/null 2>&1 || true; done @for a in $(CLAUDE_AGENTS); do rm -f "${CLAUDE_HOME}/agents/$$a"; done @for s in $(CLAUDE_OUTPUT_STYLES); do rm -f "${CLAUDE_HOME}/output-styles/$$s"; done $(call remove_file,${CLAUDE_HOME}/scripts) @@ -76,4 +87,4 @@ claude-clean: ai-clean-claude ## Remove Claude Code symlinks $(call remove_file,${CLAUDE_HOME}/keybindings.json) @rm -rf $(CLAUDE_LEGACY) -.PHONY: claude claude-cli claude-dotfiles claude-dirs claude-agents claude-output-styles claude-scripts claude-settings claude-keybindings claude-clean +.PHONY: claude claude-cli claude-dotfiles claude-dirs claude-agents claude-output-styles claude-scripts claude-settings claude-keybindings claude-mods claude-clean From 05b38f48be0842c0ce05a507aa74ba5b7359003d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:21:21 +0200 Subject: [PATCH 4/5] docs(claude): mods load in place, no version bump needed - document make claude-mods and /reload-plugins workflow - drop version bump step from pokemon sprite instructions --- ai-stuff/claude/README.md | 5 +++-- ai-stuff/claude/mods/pokemon/README.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ai-stuff/claude/README.md b/ai-stuff/claude/README.md index 06810380..88f31b40 100644 --- a/ai-stuff/claude/README.md +++ b/ai-stuff/claude/README.md @@ -29,8 +29,9 @@ ai-stuff/claude/ ``` `settings.json` declares `mods/` as the `dotfiles-mods` marketplace and enables -its plugins. Claude Code caches an installed plugin by version, so bump a mod's -`version` and run `claude plugin update @dotfiles-mods` after editing it. +its plugins, and `make claude-mods` installs every mod under `mods/`. A directory +marketplace loads each mod in place from the repo, so edits apply on +`/reload-plugins` without a version bump. `make claude` also installs (sources live elsewhere): diff --git a/ai-stuff/claude/mods/pokemon/README.md b/ai-stuff/claude/mods/pokemon/README.md index 306e28b1..9a3d6ed9 100644 --- a/ai-stuff/claude/mods/pokemon/README.md +++ b/ai-stuff/claude/mods/pokemon/README.md @@ -32,7 +32,7 @@ while Claude works. 1. Copy the four GIFs from `media/gen//` in [jakobhoeg/vscode-pokemon](https://github.com/jakobhoeg/vscode-pokemon/tree/main/media) into `sprites//`. The files are `default_idle_8fps.gif`, `default_walk_8fps.gif`, `shiny_idle_8fps.gif`, and `shiny_walk_8fps.gif`. 2. Run `node scripts/build-frames.mjs`. -3. Bump `version` in `.claude-plugin/plugin.json`, then run `claude plugin update pokemon@dotfiles-mods`. +3. Run `/reload-plugins`. The mod loads in place from the repo, so no version bump or reinstall is needed. ## Layout From 5d7ec6ae20378b5f37e6ebd1e1e9db43f6d93b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?deniz=20g=C3=B6k=C3=A7in?= <33603535+dgokcin@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:32:37 +0200 Subject: [PATCH 5/5] feat(pokemon): walk back to right edge after a turn - sprite keeps walking right when idle until it reaches home - keep sprite pinned to the right edge on resize and mon change - add test covering the walk home and idle at the edge - update readme and header comments --- ai-stuff/claude/mods/pokemon/README.md | 4 +- .../claude/mods/pokemon/hooks/register.js | 31 +++++++++++----- .../claude/mods/pokemon/tests/pokemon.test.ts | 37 +++++++++++++++++++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/ai-stuff/claude/mods/pokemon/README.md b/ai-stuff/claude/mods/pokemon/README.md index 9a3d6ed9..19a74340 100644 --- a/ai-stuff/claude/mods/pokemon/README.md +++ b/ai-stuff/claude/mods/pokemon/README.md @@ -1,8 +1,8 @@ # pokemon A Claude Code mod that draws one pixel Pokémon at the right edge of the band -above the prompt. It bobs in place while you're idle and paces back and forth -while Claude works. +above the prompt. It paces back and forth while Claude works, walks home to the +right edge when the turn ends, and bobs in place there while you're idle. ## Requirements diff --git a/ai-stuff/claude/mods/pokemon/hooks/register.js b/ai-stuff/claude/mods/pokemon/hooks/register.js index b8d567a3..b937ad0f 100644 --- a/ai-stuff/claude/mods/pokemon/hooks/register.js +++ b/ai-stuff/claude/mods/pokemon/hooks/register.js @@ -1,7 +1,8 @@ import { SPRITES } from './frames.js' // One pixel Pokémon lives at the right edge of the band above the prompt. It -// bobs in place while you're idle and paces back and forth while Claude works. +// paces back and forth while Claude works, walks home to the right edge when +// the turn ends, and bobs in place there while you're idle. const TICK_MS = 50 const MOVE_TICKS = 3 @@ -54,9 +55,10 @@ function frameAt(anim, elapsed) { function cellsNow() { const sprite = SPRITES[mon] const sheet = sprite.variants[variant] - const frame = frameAt(working ? sheet.walk : sheet.idle, tick * TICK_MS) + const walking = working || !isHome() + const frame = frameAt(walking ? sheet.walk : sheet.idle, tick * TICK_MS) const colors = COLORS[mon][variant] - const flip = working && facing === 'left' + const flip = walking && facing === 'left' const pixel = (px, py) => { if (px < 0 || px >= sprite.width) return null const row = frame[py] @@ -79,18 +81,25 @@ function cellsNow() { return new Uint8Array(words.buffer).toBase64() } +// The right edge of the strip, where the sprite idles +const homeX = () => columns - SPRITES[mon].width +const isHome = () => x >= homeX() + // Keep the sprite inside the strip function clampX() { - x = Math.max(0, Math.min(columns - SPRITES[mon].width, x)) + x = Math.max(0, Math.min(homeX(), x)) } -// Advance the clock, and while working move one column every few ticks, turning at the edges +// Advance the clock and move one column every few ticks: pacing while +// working, heading right while idle until the sprite is home function step() { tick += 1 - if (!working || tick % MOVE_TICKS !== 0) return - const maxX = columns - SPRITES[mon].width - if (facing === 'left' && x <= 0) facing = 'right' - else if (facing === 'right' && x >= maxX) facing = 'left' + if (tick % MOVE_TICKS !== 0) return + if (!working) { + if (isHome()) return + facing = 'right' + } else if (facing === 'left' && x <= 0) facing = 'right' + else if (facing === 'right' && isHome()) facing = 'left' x += facing === 'left' ? -1 : 1 clampX() } @@ -119,7 +128,9 @@ export function register(on) { on('command.run', { command: 'pokemon' }, async ($, e) => { const asked = e.args.trim().toLowerCase() if (MONS.includes(asked)) { + const wasHome = isHome() mon = asked + if (wasHome) x = homeX() clampX() await $.store.set('mon', mon) } else if (VARIANTS.includes(asked)) { @@ -140,9 +151,11 @@ export function register(on) { return next(e) } const { Box, Raster } = $.ui.resolve(e) + const wasHome = isHome() working = e.props.isWorking bandId = e.requestId columns = Math.max(SPRITES[mon].width, Math.min(STRIP_COLUMNS, e.props.bodyColumns)) + if (wasHome) x = homeX() clampX() const sprite = Raster({ key: 'pokemon', columns, rows: rowsOf(mon), cells: cellsNow() }) diff --git a/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts index d19c2be3..ecebe7a2 100644 --- a/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts +++ b/ai-stuff/claude/mods/pokemon/tests/pokemon.test.ts @@ -92,3 +92,40 @@ test('/pokemon charmander draws Charmander', async ($, on) => { const ui = await $.ui.mount({ ...BAND, surface: 'terminal' }) expect((await ui.find({ key: 'pokemon' })).props.rows).toBe(9) }) + +// The rightmost column that holds part of the sprite +function rightEdge(cells: string, columns: number): number { + const points = codePoints(cells) + let edge = -1 + points.forEach((cp, i) => { + if (cp !== 0x20) edge = Math.max(edge, i % columns) + }) + return edge +} + +test('walks back to the right edge after a turn and idles there', async ($, on) => { + const clock = mock.clock(on) + const blits: string[] = [] + on('ui.render', () => THEIRS) + on('ui.blit', ($, e) => { + blits.push(e.cells) + return { value: {} } + }) + on('session.start', () => ({ cwd: '/work' })) + on('command.register', () => ({ value: undefined })) + on('store.get', () => ({ value: undefined })) + await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' }) + + const idle = await $.ui.mount({ ...BAND, surface: 'terminal' }) + const home = rightEdge((await idle.find({ key: 'pokemon' })).props.cells, 40) + await idle.unmount() + + const busy = await $.ui.mount({ ...BAND, surface: 'terminal', props: { ...BAND.props, isWorking: true } }) + await clock.advance(150 * 10) + expect(rightEdge(blits[blits.length - 1], 40)).toBeLessThan(home) + await busy.unmount() + + await $.ui.mount({ ...BAND, surface: 'terminal' }) + await clock.advance(150 * 40) + expect(rightEdge(blits[blits.length - 1], 40)).toBe(home) +})