Add retrodiffusion as a skill (#25)

* Add retrodiffusion

* Detect animation GIF output by magic bytes; add /retrodiffusion to README

The previous animate-mode extension check looked for "gif" in the style
name, but documented animation styles (walk-cycle, idle, attack) never
contain that substring, so GIF responses were always saved as .png and
wouldn't load. Sniff the base64 prefix instead.

Also drop unused `extname` import and add a `/retrodiffusion` row to the
README slash-commands table to match the new user-invocable skill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: rshtirmer <rshtirmer@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max Bridgland
2026-05-11 14:52:13 -04:00
committed by GitHub
parent 6fbd393380
commit 1132c5c90f
5 changed files with 880 additions and 0 deletions
+2
View File
@@ -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
+1
View File
@@ -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 |
+369
View File
@@ -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=<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=<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=<key> node scripts/retrodiffusion-generate.mjs \
* --mode animate --prompt "knight walking" --style walk-cycle \
* --output public/assets/sprites/ --slug knight-walk
*
* RETRODIFFUSION_API_KEY=<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=<key> node scripts/retrodiffusion-generate.mjs --mode <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> Text prompt
--negative-prompt <text> Negative prompt
--model <name> RD_PRO | RD_FAST | RD_PLUS | RD_MINI (default: RD_FAST)
--style <name> prompt_style (default: "default" — see api-reference.md)
--width <n> Output width in pixels (default: 64)
--height <n> Output height in pixels (default: 64)
--num-images <n> Number of images to generate (default: 1)
--seed <n> Seed for reproducibility
--image <path> Input image (img2img / animate input / tileset / edit)
--palette <path> 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 <dir> Output directory (default: public/assets/sprites)
--slug <name> 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);
}
})();
+294
View File
@@ -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=<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=<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=<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=<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=<key> node scripts/retrodiffusion-generate.mjs \
--mode generate --prompt "..." --check-cost
```
### Image-to-Image (refine a reference)
```bash
RETRODIFFUSION_API_KEY=<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.50.8 is the useful range.
### Animations (walk cycles, idles, attacks)
```bash
# Walk cycle — returns spritesheet PNG or transparent GIF (style-dependent)
RETRODIFFUSION_API_KEY=<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=<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=<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=<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=<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=<key> node scripts/retrodiffusion-generate.mjs --mode balance
```
## Model Selection Guide
| Model | Cost | Sizes | Best for |
|-------|------|-------|----------|
| **RD_FAST** | $0.0150.04 | 64×64 to 384×384 | Bulk prototyping, simple sprites, items |
| **RD_PLUS** | $0.0250.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: <key>`, 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.40.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
+214
View File
@@ -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: <your-api-key>
```
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 <key>`. 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": "<base64 PNG, optional>",
"input_palette": "<base64 PNG, optional>",
"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 | 14 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 (01) | img2img only | 0 = identical to input, 1 = ignore input. 0.50.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": "<base64 PNG>"
}
```
### 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.0150.04). |
| **RD_PLUS** | 16×16 | 192×192 | 18+ styles. Good for tilesets and small assets ($0.0250.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": ["<base64 PNG>", "..."],
"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