diff --git a/CLAUDE.md b/CLAUDE.md index 13a4270..c1173f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ skills/ design-game/SKILL.md # Visual design audit + improvements add-feature/SKILL.md # Add feature following patterns add-assets/SKILL.md # Replace shapes with pixel art sprites + retrodiffusion/SKILL.md # Retro Diffusion — true pixel art via paid API (sprites, tilesets, animations) game-3d-assets/SKILL.md # 3D model pipeline (GLB download, AssetLoader, animated characters) meshyai/SKILL.md # Meshy AI — generate 3D models from text/images, auto-rig, animate add-3d-assets/SKILL.md # Replace 3D primitives with real GLB models @@ -50,6 +51,7 @@ scripts/ example-actions.json # Example action payloads for iterate-client.js find-3d-asset.mjs # Search & download GLB models (Sketchfab, Poly Haven, Poly.pizza) meshy-generate.mjs # Generate 3D models with Meshy AI (text-to-3d, image-to-3d, rig, animate) + retrodiffusion-generate.mjs # Generate pixel art with Retro Diffusion (generate, img2img, animate, tileset, edit, balance) assets/ characters/ # 2D South Park-style spritesheets (photo-composite) manifest.json diff --git a/README.md b/README.md index 03cf803..c0c5c8b 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ src/ | `/design-game [path]` | Audit and improve visual polish | | `/add-feature [description]` | Add a gameplay feature following architecture patterns | | `/add-assets [path]` | Replace geometric shapes with pixel art sprites | +| `/retrodiffusion [prompt]` | Generate true pixel art sprites/tilesets/animations via Retro Diffusion API (paid) | | `/add-audio [path]` | Add Strudel.cc music and sound effects | | `/monetize-game [path]` | Register on Play.fun, add SDK, get monetized URL | | `/qa-game [path]` | Add Playwright QA tests | diff --git a/scripts/retrodiffusion-generate.mjs b/scripts/retrodiffusion-generate.mjs new file mode 100755 index 0000000..f672df6 --- /dev/null +++ b/scripts/retrodiffusion-generate.mjs @@ -0,0 +1,369 @@ +#!/usr/bin/env node +/** + * retrodiffusion-generate.mjs — Generate true pixel art with the Retro Diffusion API. + * + * Zero npm dependencies. Uses Node.js built-in fetch, fs, path. + * + * Modes: + * generate — Text-to-image pixel art (default) + * img2img — Image-to-image (modify an existing image) + * animate — Animation styles (returns base64 GIF or spritesheet) + * tileset — Generate a wang-style tileset or single tile + * edit — Edit an existing image via /v1/edit + * balance — Check remaining credits + * + * Usage: + * RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + * --mode generate --prompt "a cute green slime" \ + * --model RD_FAST --style retro --width 64 --height 64 \ + * --output public/assets/sprites/ --slug slime + * + * RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + * --mode img2img --image ./concept.png --prompt "an orange sports car" \ + * --strength 0.75 --output public/assets/sprites/ --slug car + * + * RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + * --mode animate --prompt "knight walking" --style walk-cycle \ + * --output public/assets/sprites/ --slug knight-walk + * + * RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs --mode balance + * + * Output: + * {slug}.png (or {slug}-{n}.png if num_images > 1) + * {slug}.meta.json — prompt, model, style, cost, balance, timestamps + * + * Environment: + * RETRODIFFUSION_API_KEY Required. Get one at https://www.retrodiffusion.ai → Account → API. + * + * See also: skills/retrodiffusion/SKILL.md + */ + +import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const API_BASE = 'https://api.retrodiffusion.ai/v1'; +const API_KEY = process.env.RETRODIFFUSION_API_KEY || process.env.RD_API_KEY; + +const args = process.argv.slice(2); +const getArg = (name, fallback = null) => { + const i = args.indexOf(`--${name}`); + return i === -1 || i + 1 >= args.length ? fallback : args[i + 1]; +}; +const hasFlag = (name) => args.includes(`--${name}`); + +const mode = getArg('mode', 'generate'); +const prompt = getArg('prompt'); +const negativePrompt = getArg('negative-prompt'); +const model = getArg('model', 'RD_FAST'); +const style = getArg('style', 'default'); +const width = parseInt(getArg('width', '64'), 10); +const height = parseInt(getArg('height', '64'), 10); +const numImages = parseInt(getArg('num-images', '1'), 10); +const seed = getArg('seed') ? parseInt(getArg('seed'), 10) : undefined; +const strength = parseFloat(getArg('strength', '0.75')); +const imagePath = getArg('image'); +const palettePath = getArg('palette'); +const outputDir = getArg('output', 'public/assets/sprites'); +const slug = getArg('slug'); +const removeBg = hasFlag('remove-bg'); +const tileX = hasFlag('tile-x'); +const tileY = hasFlag('tile-y'); +const checkCost = hasFlag('check-cost'); +const bypassExpansion = hasFlag('bypass-prompt-expansion'); + +function requireKey() { + if (!API_KEY) { + console.error( + 'RETRODIFFUSION_API_KEY environment variable is required.\n' + + 'Get an API key at: https://www.retrodiffusion.ai → Account → API' + ); + process.exit(1); + } +} + +function slugify(text) { + return (text || 'output').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 60); +} + +function imageToBase64(p) { + const buf = readFileSync(resolve(p)); + return buf.toString('base64'); +} + +async function apiCall(path, body, method = 'POST') { + const res = await fetch(`${API_BASE}${path}`, { + method, + headers: { + 'X-RD-Token': API_KEY, + 'Content-Type': 'application/json', + }, + body: method === 'GET' ? undefined : JSON.stringify(body), + }); + const text = await res.text(); + let data; + try { data = JSON.parse(text); } catch { data = { raw: text }; } + if (!res.ok) { + throw new Error(`${method} ${path} → HTTP ${res.status}: ${data.error || data.raw || text}`); + } + return data; +} + +function ensureOutputDir() { + if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true }); +} + +function writeImages(base64Images, baseSlug, ext = 'png') { + ensureOutputDir(); + const paths = []; + base64Images.forEach((b64, i) => { + const filename = base64Images.length === 1 ? `${baseSlug}.${ext}` : `${baseSlug}-${i + 1}.${ext}`; + const path = resolve(outputDir, filename); + writeFileSync(path, Buffer.from(b64, 'base64')); + paths.push(path); + console.log(` wrote ${path} (${(Buffer.byteLength(b64, 'base64') / 1024).toFixed(1)} KB)`); + }); + return paths; +} + +function writeMeta(baseSlug, meta) { + const path = resolve(outputDir, `${baseSlug}.meta.json`); + writeFileSync(path, JSON.stringify(meta, null, 2)); + console.log(` wrote ${path}`); +} + +// --------------------------------------------------------------------------- +// Modes +// --------------------------------------------------------------------------- + +async function runGenerate() { + if (!prompt) throw new Error('--prompt is required for generate mode'); + const baseSlug = slug || slugify(prompt); + const payload = { + prompt, + model, + prompt_style: style, + width, + height, + num_images: numImages, + }; + if (seed !== undefined) payload.seed = seed; + if (negativePrompt) payload.negative_prompt = negativePrompt; + if (removeBg) payload.remove_bg = true; + if (tileX) payload.tile_x = true; + if (tileY) payload.tile_y = true; + if (checkCost) payload.check_cost = true; + if (bypassExpansion) payload.bypass_prompt_expansion = true; + if (palettePath) payload.input_palette = imageToBase64(palettePath); + + console.log(`→ POST /inferences model=${model} style=${style} ${width}x${height} n=${numImages}`); + const data = await apiCall('/inferences', payload); + + if (checkCost) { + console.log(` estimated cost: $${data.balance_cost ?? '?'}`); + return; + } + console.log(` cost: $${data.balance_cost?.toFixed(3) ?? '?'} remaining: $${data.remaining_balance?.toFixed(2) ?? '?'}`); + writeImages(data.base64_images || [], baseSlug, 'png'); + writeMeta(baseSlug, { + mode: 'generate', + prompt, + model: data.model || model, + style, + width, + height, + seed, + cost: data.balance_cost, + remaining_balance: data.remaining_balance, + created_at: data.created_at, + }); +} + +async function runImg2Img() { + if (!prompt) throw new Error('--prompt is required for img2img mode'); + if (!imagePath) throw new Error('--image is required for img2img mode'); + const baseSlug = slug || slugify(prompt); + const payload = { + prompt, + model, + prompt_style: style, + width, + height, + num_images: numImages, + input_image: imageToBase64(imagePath), + strength, + }; + if (seed !== undefined) payload.seed = seed; + if (negativePrompt) payload.negative_prompt = negativePrompt; + if (removeBg) payload.remove_bg = true; + + console.log(`→ POST /inferences (img2img) strength=${strength}`); + const data = await apiCall('/inferences', payload); + console.log(` cost: $${data.balance_cost?.toFixed(3) ?? '?'} remaining: $${data.remaining_balance?.toFixed(2) ?? '?'}`); + writeImages(data.base64_images || [], baseSlug, 'png'); + writeMeta(baseSlug, { + mode: 'img2img', + prompt, + model: data.model || model, + style, + strength, + source_image: resolve(imagePath), + width, + height, + cost: data.balance_cost, + remaining_balance: data.remaining_balance, + created_at: data.created_at, + }); +} + +async function runAnimate() { + if (!prompt) throw new Error('--prompt is required for animate mode'); + const baseSlug = slug || slugify(prompt); + const payload = { + prompt, + model, + prompt_style: style, // e.g. "walk-cycle", "idle", "attack" + width, + height, + num_images: numImages, + }; + if (seed !== undefined) payload.seed = seed; + if (imagePath) payload.input_image = imageToBase64(imagePath); + + console.log(`→ POST /inferences (animation) style=${style}`); + const data = await apiCall('/inferences', payload); + console.log(` cost: $${data.balance_cost?.toFixed(3) ?? '?'} remaining: $${data.remaining_balance?.toFixed(2) ?? '?'}`); + // Animation styles return GIF or spritesheet PNG depending on the style. + // Sniff the base64 magic bytes — PNG → "iVBORw0K", GIF → "R0lGOD". + const ext = data.base64_images?.[0]?.startsWith('R0lGOD') ? 'gif' : 'png'; + writeImages(data.base64_images || [], baseSlug, ext); + writeMeta(baseSlug, { + mode: 'animate', + prompt, + style, + model: data.model || model, + width, + height, + cost: data.balance_cost, + remaining_balance: data.remaining_balance, + created_at: data.created_at, + }); +} + +async function runTileset() { + if (!prompt) throw new Error('--prompt is required for tileset mode'); + const baseSlug = slug || slugify(prompt); + const payload = { + prompt, + model, + prompt_style: style, // e.g. "wang-tile", "single-tile" + width, + height, + num_images: numImages, + }; + if (seed !== undefined) payload.seed = seed; + if (imagePath) payload.input_image = imageToBase64(imagePath); + + console.log(`→ POST /inferences (tileset) style=${style}`); + const data = await apiCall('/inferences', payload); + console.log(` cost: $${data.balance_cost?.toFixed(3) ?? '?'} remaining: $${data.remaining_balance?.toFixed(2) ?? '?'}`); + writeImages(data.base64_images || [], baseSlug, 'png'); + writeMeta(baseSlug, { + mode: 'tileset', + prompt, + style, + model: data.model || model, + width, + height, + cost: data.balance_cost, + remaining_balance: data.remaining_balance, + created_at: data.created_at, + }); +} + +async function runEdit() { + if (!imagePath) throw new Error('--image is required for edit mode'); + if (!prompt) throw new Error('--prompt is required for edit mode (describes the edit)'); + const baseSlug = slug || `${slugify(prompt)}-edit`; + const payload = { + prompt, + input_image: imageToBase64(imagePath), + }; + console.log(`→ POST /edit`); + const data = await apiCall('/edit', payload); + console.log(` cost: $${data.balance_cost?.toFixed(3) ?? '?'} remaining: $${data.remaining_balance?.toFixed(2) ?? '?'}`); + writeImages(data.base64_images || [], baseSlug, 'png'); + writeMeta(baseSlug, { + mode: 'edit', + prompt, + source_image: resolve(imagePath), + cost: data.balance_cost, + remaining_balance: data.remaining_balance, + created_at: data.created_at, + }); +} + +async function runBalance() { + const data = await apiCall('/inferences/credits', null, 'GET'); + console.log(`Remaining balance: $${data.credits ?? data.remaining_balance ?? '?'}`); + if (data.tier) console.log(`Tier: ${data.tier}`); +} + +// --------------------------------------------------------------------------- +// Entry +// --------------------------------------------------------------------------- + +function usage() { + console.error(`Usage: RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs --mode [options] + +Modes: + generate Text-to-image pixel art (default) + img2img Image-to-image transformation + animate Animation styles (walk-cycle, idle, attack, etc.) + tileset Wang-style tileset or single tile + edit Edit an existing image (/v1/edit, flat $0.06) + balance Check remaining credits + +Options: + --prompt Text prompt + --negative-prompt Negative prompt + --model RD_PRO | RD_FAST | RD_PLUS | RD_MINI (default: RD_FAST) + --style prompt_style (default: "default" — see api-reference.md) + --width Output width in pixels (default: 64) + --height Output height in pixels (default: 64) + --num-images Number of images to generate (default: 1) + --seed Seed for reproducibility + --image Input image (img2img / animate input / tileset / edit) + --palette Reference palette image (input_palette) + --strength <0..1> img2img modification strength (default: 0.75) + --remove-bg Transparent background + --tile-x Seamless horizontal tiling + --tile-y Seamless vertical tiling + --bypass-prompt-expansion Disable automatic prompt enhancement + --check-cost Estimate cost without generating + --output Output directory (default: public/assets/sprites) + --slug Output filename slug + +Environment: + RETRODIFFUSION_API_KEY API key from https://www.retrodiffusion.ai → Account → API`); +} + +(async () => { + if (mode === 'balance') { + requireKey(); + await runBalance(); + return; + } + if (!mode || hasFlag('help')) { usage(); process.exit(mode ? 0 : 1); } + requireKey(); + try { + if (mode === 'generate') await runGenerate(); + else if (mode === 'img2img') await runImg2Img(); + else if (mode === 'animate') await runAnimate(); + else if (mode === 'tileset') await runTileset(); + else if (mode === 'edit') await runEdit(); + else { console.error(`Unknown mode: ${mode}`); usage(); process.exit(1); } + } catch (err) { + console.error(`✖ ${err.message}`); + process.exit(1); + } +})(); diff --git a/skills/retrodiffusion/SKILL.md b/skills/retrodiffusion/SKILL.md new file mode 100644 index 0000000..c2eb375 --- /dev/null +++ b/skills/retrodiffusion/SKILL.md @@ -0,0 +1,294 @@ +--- +name: retrodiffusion +description: Generate true pixel art sprites, tilesets, and animations with the Retro Diffusion API — the best dedicated pixel art model. Use when the user says "generate pixel art", "make a sprite with AI", "use Retro Diffusion", "rdpk", or wants high-quality pixel art faster than hand-coded sprite arrays. Requires a Retro Diffusion account and RETRODIFFUSION_API_KEY (paid credits). Great for quick prototyping of 2D Phaser games. +argument-hint: "[prompt]" +license: MIT +compatibility: Requires a Retro Diffusion account, RETRODIFFUSION_API_KEY environment variable, and internet access for API calls. Each generation consumes paid credits from the user's account. +metadata: + author: OpusGameLabs + version: 1.0.0 + tags: [game, 2d, pixel-art, retro-diffusion, ai, image-generation, sprites, tilesets, animation] +--- + +# Retro Diffusion — True Pixel Art Generation + +Generate authentic pixel art sprites, tilesets, and animations from text prompts or reference images using the [Retro Diffusion](https://www.retrodiffusion.ai) API. Output is true pixel art (clean integer-pixel grids, limited palettes), not blurry diffusion-model output downscaled into pixels — making it the best AI option for shippable 2D game art and fast prototyping. + +> **Paid service.** Retro Diffusion requires an account at https://www.retrodiffusion.ai and an API key with credits. **Always tell the user this requires a paid Retro Diffusion account before generating anything**, and confirm they understand each call deducts from their balance. + +## Reference Files + +| File | Description | +|------|-------------| +| [api-reference.md](./api-reference.md) | Full API endpoints, models, prompt_style values, response shape, pricing, and error codes | + +## When to Use + +- **Quick prototypes** — generate dozens of unique sprites in a minute instead of hand-coding 16×16 arrays +- **Polish pass on a working game** — replace placeholder shapes with cohesive pixel art that already matches a chosen style +- **Tilesets** — wang-style ground/wall sets without manually drawing every transition tile +- **Sprite animations** — walk cycles, idle bobs, attacks delivered as transparent GIFs or spritesheets + +**Choose this over `add-assets` (code-only pixel art) when:** the user wants AI-generated art, has a Retro Diffusion key, or the game needs more visual variety than hand-coded matrices can deliver in a reasonable time. The two skills compose well — Retro Diffusion for hero sprites/tilesets, code-only matrices as a free fallback for quick filler. + +**Do NOT use this for:** 3D models (use `meshyai`), 3D worlds (use `worldlabs`), audio (use `game-audio`), or when the user explicitly wants the all-code procedural pixel art approach (use `add-assets`). + +## Authentication + +Before prompting the user, check if the key already exists: +```bash +test -f .env && grep -q '^RETRODIFFUSION_API_KEY=.' .env && echo "found" +``` +If found, export it with `set -a; . .env; set +a` and skip the prompt. + +If the key is **not** set, **ask the user immediately and explain the cost model**: + +> I'll generate true pixel art with Retro Diffusion — the best dedicated pixel art model for shippable game sprites. +> +> **Heads up: this is a paid service.** You'll need a Retro Diffusion account with credits. Each image typically costs between $0.015 (RD_FAST) and $0.18 (RD_PRO). Get a key in 60 seconds: +> +> 1. Sign up at https://www.retrodiffusion.ai +> 2. Buy credits (smallest pack is fine for prototyping) +> 3. Account → API → Generate API key +> +> Paste your key like: `RETRODIFFUSION_API_KEY=rdpk-...` +> (It will be saved to `.env` and redacted from this conversation.) +> +> Or type "skip" to fall back to free hand-coded pixel art via `/add-assets`. + +If the user provides a key, save it to `.env` and use it via `set -a; . .env; set +a && node scripts/retrodiffusion-generate.mjs ...`. + +If the user skips, **stop and hand off to `/add-assets`** — do not silently degrade to a different art style without telling them. + +## CLI Script — `scripts/retrodiffusion-generate.mjs` + +Zero-dependency Node.js script. Handles all six modes: generate, img2img, animate, tileset, edit, and balance. + +### Generate (text-to-pixel-art) + +```bash +# Single 64×64 sprite, RD_FAST (cheapest) +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode generate \ + --prompt "a cute green slime with big eyes" \ + --model RD_FAST --style retro \ + --width 64 --height 64 \ + --output public/assets/sprites/ --slug slime + +# Higher quality with RD_PRO (flat $0.18, supports up to 256×256) +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode generate \ + --prompt "a heroic knight, full body, side view" \ + --model RD_PRO --style fantasy \ + --width 128 --height 128 \ + --output public/assets/sprites/ --slug knight + +# Transparent background for game sprites +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode generate --prompt "a treasure chest, closed" \ + --model RD_FAST --style game-asset \ + --width 64 --height 64 --remove-bg \ + --output public/assets/sprites/ --slug chest + +# Seamless tiling texture (e.g., grass, water) +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode generate --prompt "lush grass texture, top down" \ + --model RD_PLUS --style top-down \ + --width 64 --height 64 --tile-x --tile-y \ + --output public/assets/tiles/ --slug grass + +# Estimate cost before paying for it +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode generate --prompt "..." --check-cost +``` + +### Image-to-Image (refine a reference) + +```bash +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode img2img \ + --image ./concept-art/hero-sketch.png \ + --prompt "a heroic knight in shining armor" \ + --model RD_FAST --style retro \ + --width 64 --height 64 --strength 0.75 \ + --output public/assets/sprites/ --slug hero +``` + +`--strength` controls how much the AI deviates from the input (0 = identical, 1 = ignore input). 0.5–0.8 is the useful range. + +### Animations (walk cycles, idles, attacks) + +```bash +# Walk cycle — returns spritesheet PNG or transparent GIF (style-dependent) +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode animate \ + --prompt "a knight walking" \ + --model RD_PRO --style walk-cycle \ + --width 64 --height 64 \ + --output public/assets/sprites/ --slug knight-walk + +# Idle bob from an existing character +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode animate --image public/assets/sprites/knight.png \ + --prompt "knight idle breathing" --style idle \ + --output public/assets/sprites/ --slug knight-idle +``` + +Animation styles cost $0.07–$0.25 depending on type. See [api-reference.md](./api-reference.md) for the full style list. + +### Tilesets + +```bash +# Wang-style tileset (auto-tiling ground/wall sets) +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode tileset \ + --prompt "stone dungeon floor, mossy" \ + --model RD_PLUS --style wang-tile \ + --width 96 --height 96 \ + --output public/assets/tiles/ --slug dungeon-floor + +# Single detailed tile +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode tileset --prompt "wooden bridge plank" --style single-tile \ + --width 64 --height 64 --output public/assets/tiles/ --slug bridge +``` + +### Edit (progressive refinement) + +Flat $0.06 per edit. Use to tweak a generated sprite without re-rolling from scratch. + +```bash +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs \ + --mode edit --image public/assets/sprites/knight.png \ + --prompt "give the knight a red cape" \ + --output public/assets/sprites/ --slug knight-red +``` + +### Balance check + +```bash +RETRODIFFUSION_API_KEY= node scripts/retrodiffusion-generate.mjs --mode balance +``` + +## Model Selection Guide + +| Model | Cost | Sizes | Best for | +|-------|------|-------|----------| +| **RD_FAST** | $0.015–0.04 | 64×64 to 384×384 | Bulk prototyping, simple sprites, items | +| **RD_PLUS** | $0.025–0.08 | 16×16 to 192×192 | Tilesets, top-down, watercolor, Minecraft-style | +| **RD_PRO** | $0.18 flat | 64×64 to 256×256 | Hero sprites, isometric scenes, polish pass (17+ styles, supports up to 9 reference images) | +| **RD_MINI** | varies | very small | Routes to PLUS/FAST for tiny pickups | + +Default to **RD_FAST** for prototyping. Upgrade individual hero sprites to **RD_PRO** when the prototype is shipping. + +See [api-reference.md](./api-reference.md) for the complete per-model `prompt_style` list. + +## Prompt Engineering for Pixel Art + +Good prompts are short, specific about silhouette and palette, and call out the perspective: + +| Goal | Prompt | Why | +|------|--------|-----| +| Game character | "a cute green slime, big eyes, side view" | Single subject, perspective named | +| Item | "a glowing red potion bottle" | Specific shape + color | +| Tile | "lush grass texture, top down, seamless" | Perspective + tiling intent | +| Enemy | "a skeleton warrior, hunched, side view" | Silhouette cue ("hunched") | + +**Avoid:** +- Multiple subjects in one prompt — generate separately and composite in-game +- Abstract concepts ("the feeling of dread") — describe the visual instead +- Mismatched perspective ("top-down knight, side view") — pick one + +## Integration with Phaser Games + +Retro Diffusion outputs are normal PNGs — they slot directly into Phaser's loader. + +```js +// In a preload scene: +this.load.image('slime', 'assets/sprites/slime.png'); +this.load.spritesheet('knight-walk', 'assets/sprites/knight-walk.png', { + frameWidth: 64, + frameHeight: 64, +}); + +// In create(): +const slime = this.physics.add.sprite(100, 100, 'slime'); +this.anims.create({ + key: 'knight-walk', + frames: this.anims.generateFrameNumbers('knight-walk', { start: 0, end: 7 }), + frameRate: 12, + repeat: -1, +}); +``` + +**Pixel-perfect rendering** — when using AI pixel art, configure the Phaser game with crisp scaling so sprites stay sharp: + +```js +// In main.js, Phaser config: +const config = { + type: Phaser.AUTO, + pixelArt: true, // disables antialiasing, preserves hard pixel edges + roundPixels: true, // snaps sprite positions to integers + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, + // ... +}; +``` + +Without `pixelArt: true`, the browser will smooth-scale the PNGs and the pixel-art look is lost. + +## Spritesheet Slicing + +Animation styles return either a transparent GIF or a horizontal spritesheet (style-dependent — check the output PNG dimensions). For spritesheets, infer `frameWidth = imageWidth / frameCount` and load with `this.load.spritesheet()`. The `.meta.json` file written next to each output records the requested width/height so frame count is recoverable. + +## Cost Discipline + +Every call costs real money from the user's balance. Apply these defaults to avoid waste: + +1. **Use `--check-cost` first** when generating at unfamiliar sizes or with RD_PRO. +2. **Default to RD_FAST + 64×64** for prototypes. Only upgrade specific hero sprites. +3. **Use `--seed`** when iterating on a prompt — same seed + same prompt = same image, so you can A/B test prompt edits without re-rolling random variations. +4. **Cache outputs** — generated PNGs are committed to `public/assets/sprites/`. Never regenerate an asset that already exists unless the user asked for a new variant. +5. **Run `--mode balance`** before a big batch so the user sees their remaining credits. + +## Output Convention + +``` +public/assets/sprites/ + slime.png # generated image + slime.meta.json # prompt, model, style, cost, balance, timestamps + knight-walk.png # animation spritesheet or GIF + knight-walk.meta.json +public/assets/tiles/ + grass.png # tiling texture + grass.meta.json +``` + +Always write a `.meta.json` next to each PNG so the prompt and seed are recoverable for later regeneration. + +## Troubleshooting + +| Problem | Cause | Fix | +|---------|-------|-----| +| `RETRODIFFUSION_API_KEY` not set | Key missing | Ask the user for their key (https://www.retrodiffusion.ai → Account → API). Save to `.env`. | +| HTTP 401 Unauthorized | Wrong header or invalid key | Header must be `X-RD-Token: `, NOT `Authorization: Bearer`. Verify the key on the dashboard. | +| HTTP 400 "insufficient credits" | Account balance is empty | Tell the user to top up at the dashboard before retrying. Run `--mode balance` to confirm. | +| Sprite looks blurry in Phaser | Browser is smooth-scaling the PNG | Set `pixelArt: true` and `roundPixels: true` in the Phaser config. | +| Output ignores prompt details | Prompt is being auto-expanded into something different | Try `--bypass-prompt-expansion` to send the prompt verbatim. | +| Tileset edges don't match | Style isn't a wang/tile style | Use `--style wang-tile` for auto-tiling sets, or `--tile-x --tile-y` for seamless single textures. | +| img2img output ignores reference | Strength too high | Lower `--strength` to 0.4–0.6. Strength 0 keeps the input; strength 1 ignores it. | +| Inconsistent style across batch | Different seeds and slight prompt variation | Pin `--seed` and reuse the exact same prompt structure for sibling sprites (same character family). | + +## Checklist + +- [ ] **User informed this is a paid service** — Retro Diffusion account + credits required +- [ ] `RETRODIFFUSION_API_KEY` checked in `.env` or env, prompted if missing +- [ ] Model + style chosen for the budget (RD_FAST for prototyping, RD_PRO for hero sprites) +- [ ] `--check-cost` used for the first call at any new size +- [ ] Prompt is specific about subject, perspective, and silhouette +- [ ] `--seed` pinned when generating siblings (e.g., walk + idle of the same character) +- [ ] `--remove-bg` used for game sprites (transparent PNG) +- [ ] `--tile-x`/`--tile-y` used for textures, `--style wang-tile` for auto-tiling +- [ ] PNG written to `public/assets/sprites/` (or `tiles/`) with `.meta.json` alongside +- [ ] Phaser config has `pixelArt: true` and `roundPixels: true` +- [ ] Spritesheet `frameWidth`/`frameHeight` matches the requested width/height +- [ ] User shown remaining balance after the batch diff --git a/skills/retrodiffusion/api-reference.md b/skills/retrodiffusion/api-reference.md new file mode 100644 index 0000000..c0ee53f --- /dev/null +++ b/skills/retrodiffusion/api-reference.md @@ -0,0 +1,214 @@ +# Retro Diffusion API Reference + +Companion file for [SKILL.md](./SKILL.md). Full API endpoint specifications based on the official examples repo: https://github.com/Retro-Diffusion/api-examples. + +## Base URL + +``` +https://api.retrodiffusion.ai/v1 +``` + +## Authentication + +All requests require this header: + +``` +X-RD-Token: +``` + +Keys come from the Retro Diffusion dashboard (https://www.retrodiffusion.ai → Account → API). Keys typically start with `rdpk-`. + +> **Wrong format:** the API does **not** use `Authorization: Bearer `. Using the wrong header returns HTTP 401. + +## Endpoints + +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/inferences` | Main image generation (text-to-image, img2img, animation, tileset) | +| `POST` | `/edit` | Progressive image edit (flat $0.06) | +| `GET` | `/inferences/credits` | Check remaining account balance | +| `POST` | `/styles` | Create a custom RD_PRO style | +| `GET` | `/styles` | List custom styles | +| `PATCH`| `/styles/{id}` | Update a custom style | +| `DELETE`| `/styles/{id}` | Delete a custom style | + +## POST /inferences — Generate Image + +### Request body + +```json +{ + "prompt": "a cute green slime", + "negative_prompt": "blurry, photorealistic", + "model": "RD_FAST", + "prompt_style": "retro", + "width": 64, + "height": 64, + "num_images": 1, + "seed": 12345, + "input_image": "", + "input_palette": "", + "strength": 0.75, + "remove_bg": false, + "tile_x": false, + "tile_y": false, + "bypass_prompt_expansion": false, + "check_cost": false, + "include_downloadable_data": false +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `prompt` | string | yes | Short, specific. Include perspective ("side view", "top down") and silhouette cues. | +| `negative_prompt` | string | no | What to avoid (e.g. "blurry, antialiased, photorealistic"). | +| `model` | string | yes | One of `RD_PRO`, `RD_FAST`, `RD_PLUS`, `RD_MINI`. | +| `prompt_style` | string | yes | Style key — see [Style Catalog](#style-catalog) below. | +| `width`, `height` | int | yes | Output dimensions. Range depends on model — see [Model Sizes](#model-sizes). | +| `num_images` | int | no | 1–4 typical. Each image bills separately. | +| `seed` | int | no | Reproducible generation. Same prompt + style + seed = same output. | +| `input_image` | string (base64) | conditional | Required for img2img and most animation styles. PNG, RGB, max ~4MB. | +| `input_palette` | string (base64) | no | Reference palette image — output uses these colors. | +| `strength` | float (0–1) | img2img only | 0 = identical to input, 1 = ignore input. 0.5–0.8 is the practical range. | +| `remove_bg` | bool | no | Returns transparent PNG instead of solid background. Best for sprites. | +| `tile_x`, `tile_y` | bool | no | Seamless tiling on that axis (textures, not wang sets). | +| `bypass_prompt_expansion` | bool | no | Skips the model's auto-expansion of short prompts. | +| `check_cost` | bool | no | Returns `balance_cost` only, no images, no charge. | +| `include_downloadable_data` | bool | no | For inventory/spritesheet styles, returns structured JSON metadata. | + +### Response (success) + +```json +{ + "created_at": 1733425519, + "model": "rd_fast", + "balance_cost": 0.025, + "remaining_balance": 100.75, + "base64_images": ["iVBORw0KGgo..."], + "downloadable_data": null +} +``` + +| Field | Notes | +|-------|-------| +| `created_at` | Unix timestamp. | +| `model` | Resolved model (RD_MINI may resolve to PLUS or FAST). | +| `balance_cost` | USD charged for this call. | +| `remaining_balance` | USD remaining in the account after this call. | +| `base64_images` | Array of base64-encoded PNGs (or GIFs for animation styles). Length = `num_images`. | +| `downloadable_data` | Structured JSON, only present when `include_downloadable_data: true`. | + +### Response (error) + +```json +{ "error": "Insufficient credits" } +``` + +| HTTP | Meaning | Common cause | +|------|---------|--------------| +| 400 | Bad request | Invalid model/style combo, dimension out of range, insufficient credits | +| 401 | Unauthorized | Missing or invalid `X-RD-Token`, or used `Authorization: Bearer` | +| 403 | Forbidden | Style not available on the user's plan | +| 429 | Rate limited | Slow down — exponential backoff | +| 500 | Server error | Transient — retry with backoff | + +## POST /edit — Edit an Image + +Flat **$0.06** per call. Lighter-weight than re-rolling a full inference. + +### Request body + +```json +{ + "prompt": "give the knight a red cape", + "input_image": "" +} +``` + +### Response + +Same shape as `/inferences` — `base64_images[0]` contains the edited PNG. + +## GET /inferences/credits — Check Balance + +No body. Response: + +```json +{ "credits": 24.50 } +``` + +(Field name may also appear as `remaining_balance` depending on plan tier.) + +## Model Sizes + +| Model | Min size | Max size | Notes | +|-------|----------|----------|-------| +| **RD_PRO** | 64×64 | 256×256 | 17+ styles. Supports up to 9 reference images. Flat $0.18 per image. | +| **RD_FAST** | 64×64 | 384×384 | 15 styles. Cheapest tier ($0.015–0.04). | +| **RD_PLUS** | 16×16 | 192×192 | 18+ styles. Good for tilesets and small assets ($0.025–0.08). | +| **RD_MINI** | varies | varies | Routes to PLUS or FAST. Optimized for tiny dimensions. | + +Out-of-range dimensions return HTTP 400. Always check the per-model limits before sending. + +## Style Catalog + +Each model exposes its own `prompt_style` keys. Common values seen in the official examples: + +### RD_PRO (17+ styles) +`default`, `painterly`, `fantasy`, `ui-panel`, `horror`, `sci-fi`, `isometric`, `platformer`, `dungeon-map`, `character-turnaround`, `walk-cycle`, `idle`, `attack`, `inventory-sheet`, `portrait`, `concept`, `creature` + +### RD_FAST (15 styles) +`default`, `retro`, `arcade`, `simple`, `detailed`, `anime`, `texture`, `ui`, `game-asset`, `character`, `portrait`, `prop`, `effect`, `weapon`, `enemy` + +### RD_PLUS (18+ styles) +`default`, `watercolor`, `top-down`, `cartoon`, `isometric-asset`, `minecraft`, `low-res-16`, `low-res-32`, `low-res-64`, `wang-tile`, `single-tile`, `terrain`, `building`, `plant`, `creature`, `weapon`, `effect`, `ui` + +### RD_MINI +Routes — pass any RD_PLUS or RD_FAST style key. + +> The exact set of keys is updated by Retro Diffusion server-side. If a style returns 400 ("invalid style"), check the dashboard or the official examples repo for the current list. The names above are stable enough for prototyping. + +## Pricing (as of 2026) + +| Operation | Cost | +|-----------|------| +| RD_FAST inference | $0.015–$0.04 (size-dependent) | +| RD_PLUS inference | $0.025–$0.08 (size-dependent) | +| RD_PRO inference | $0.18 flat | +| Animation style | $0.07–$0.25 (style-dependent) | +| Tileset (wang) | $0.10 flat | +| Image edit | $0.06 flat | + +Multiply by `num_images` for batch calls. Use `check_cost: true` on `/inferences` to preview before paying. + +## Custom Styles (POST /styles) + +Custom styles are RD_PRO-based. Useful for locking a consistent look across a whole game. + +### Request body + +```json +{ + "name": "my-game-style", + "description": "Muted NES palette, side-view characters", + "reference_images": ["", "..."], + "llm_instructions": "Always render in NES-style limited palette, side-view, ...", + "forced_dimensions": { "width": 96, "height": 96 } +} +``` + +`forced_dimensions` is optional and accepts widths/heights between 96 and 256. + +### Response + +```json +{ "id": "style-abc123", "name": "my-game-style", "created_at": 1733425519 } +``` + +Use the returned `id` as `prompt_style` on subsequent `/inferences` calls. + +## Source + +Official examples repo (recommended reading before integrating): +- https://github.com/Retro-Diffusion/api-examples +- `img2img.py` — minimal Python example with full request/response handling