These days, AI agents do the building. The specification below is a complete, tested operating manual for UnitForge — every selector, part, animation and export format. Copy it and paste it to your AI (or give it the URL on the right), then ask in plain language, for example:
# UnitForge — AI Agent Guide
Spec-Version: 2026-08-22.3
UnitForge (https://unitforge.net) is a web-based pixel-art character maker. It assembles a 21-pixel chibi character from interchangeable parts (body, hair, helmet, armor, boots, weapon, shield), plays skeleton-driven animations, and exports game-ready PNG stills and sprite sheets. This document is a complete operating manual for an AI agent driving the site via browser automation, or for instructing a human step by step. Every selector and behavior below is verified against the live site.
This file is always available at: https://unitforge.net/ai-guide.md
## 1. Core concepts
- Character: 21 px tall retro pixel chibi, 3/4 view. **Faces LEFT by default**; use the Flip button for right-facing.
- Skeleton-driven: parts attach to bones (head, body, two arms, two feet). Animations are bone transform curves only — every equipped part follows automatically. No per-part animation work is ever needed.
- Rendering is nearest-neighbor (crisp pixels). The stage shows a 10× preview; exports are native resolution.
- The **red dashed box** on the stage is the export frame (48×48 native px). Pixels outside it are clipped in exports.
- The UI is bilingual (English default / Korean). Control selectors below are language-independent.
## 2. UI map — stable selectors
Each screen has its own URL, so an agent can navigate straight to it instead of
clicking through tabs. English lives at the root, Korean under `/ko/`.
| Screen | URL | Korean |
|---|---|---|
| Character | `/` | `/ko/` |
| Monsters | `/monsters` | `/ko/monsters` |
| Character Editor | `/editor` | `/ko/editor` |
| Guide | `/guide` | `/ko/guide` |
| Guide for AI (this document) | `/ai-guide` | `/ko/ai-guide` |
The raw markdown of this document is also served at `/ai-guide.md`.
Tabs are real links (`a.tab-btn[data-tab="…"]`); clicking one swaps the view via
pushState without a full page load.
All character-assembly controls live on the **Character** tab (the default tab; other tabs are Monsters, Character Editor, Guide, Guide for AI and are not needed for assembly). Dispatch a `change` event after setting a `<select>`, and an `input` event after setting a number/color input, or interact by real clicks/keystrokes.
| Control | Selector | Behavior |
|---|---|---|
| Part select (per category) | `select[data-cat="<Category>"]` | Option value = part index as string; `"-1"` = none/unequipped. Categories: `Body`, `Hair`, `Helmet`, `Armor`, `Boots`, `Weapon`, `Shield` |
| Position adjust | `input[data-cat="<Category>"][data-adj="x"]` / `[data-adj="y"]` | Number, −20…20 px, step 1. +X = toward facing direction (forward), +Y = down |
| Tilt adjust (Weapon/Shield only) | `input[data-cat="<Category>"][data-adj="r"]` | Number, −180…180°, step 15. Weapon pivots at its grip, shield at its center |
| Hair tint | `input[data-tint="Hair"]` | Color input; recolors the hair |
| Helmet mode | `select#helm-mode` | `sealed` / `eyes` / `open` — affects closed-type helmets only (see §4) |
| Animation | `select#anim-select` | Values listed in §5. Conditional attack entries appear only when a matching weapon type is equipped |
| Flip left/right | `button#btn-flip` | Toggles facing. Current direction is readable from its `data-facing` attribute: `"left"` (default) or `"right"` |
| Play/Pause | `button#btn-play` | Toggles animation playback |
| Export PNG | `button#btn-png` | Downloads `unitforge_character.png` (see §6) |
| Sheet target | `select#export-anim` | Set by option **value**: `ALL` or an animation name. (Visible labels are localized — e.g. the ALL option displays "All"/"전체" — so match by value, not text) |
| Export sheet | `button#btn-sheet` | Downloads the sprite sheet for the sheet target (see §6) |
| Export for Godot | `button#btn-godot` | Opens a dialog; exports `.png` + `.tres` (+ optional `.tscn`) for Godot 4 (see §6) |
| Preset: default knight | `button#preset-default` | Sword + shield + full knight set |
| Preset: all off | `button#preset-alloff` | Bare base body |
| Preset: random | `button#preset-random` | Random part per category |
| Upload custom part | `button[data-upload="<Category>"]` | Opens the upload modal for that category (see §7) |
| Delete custom part | `button[data-del="<Category>"]` | Visible only while a custom part is selected |
| Export backup | `button#btn-backup` | Downloads every custom part as a single `unitforge-parts.json` |
| Import backup | `button#btn-restore` | Opens a file picker; merges a backup `.json` back in (duplicates are skipped) |
Upload modal fields: `#upload-name` (text), `#upload-file` (file input, PNG only, multi-file allowed), `#upload-form` (form/type dropdown — shown for Helmet and Weapon), `#upload-sample` (downloads a correctly-formatted sample PNG), `#upload-confirm` / `#upload-cancel`.
## 3. Built-in parts catalog
<!--PARTS_START-->
| Category | Built-in parts | Controls |
|---|---|---|
| Body | Base 01, Orc, Goblin, Skeleton, Zombie | always on (not removable), tintable (skin tone via `input[data-tint="Body"]`, multiply — darker tones only), custom bases can be uploaded/drawn (6-cell) |
| Hair | Brown, Blonde, Black, Long, Ponytail, Spiky, Bob, Mohawk | removable, tintable, adjust X/Y |
| Helmet | Knight 01 (Closed), Open 01, Viking, Kettle, Crested, Leather Cap, Gold Helm (Closed), Dark Helm (Closed), Ranger Hood, Mage Hat, Paladin Wing (Closed) | removable, adjust X/Y |
| Armor | Knight 01, Leather, Gold, Dark, Ranger, Mage Robe, Crimson, Paladin, Bronze, Frost | removable, adjust X/Y |
| Boots | Knight 01, Leather, Gold, Dark, Ranger, Mage, Crimson, Paladin, Bronze, Frost | removable, adjust X/Y |
| Weapon | Sword 01, Axe, Hammer, Dagger *(chop)*, Bow *(bow)*, Dagger Red *(chop)*, Staff, Mace, Sword Silver, Sword Bronze, War Pick, Pistol *(handgun)*, Rifle *(long gun)*, Musket *(long gun)*, Revolver *(handgun)*, Spear *(two-hand thrust)*, Trident *(two-hand thrust)* | removable, adjust X/Y/R |
| Shield | Shield 01, Round Wood, Tower, Round Red, Kite Blue, Gold, Green, Crystal, Dark Gem, Kite Red | removable, adjust X/Y/R |
<!--PARTS_END-->
Weapon behavior column meanings — see §4. Custom uploads are appended to the same selects, prefixed `★`.
## 4. Weapon types and conditional animations
Each weapon has a behavior type. Equipping it adds its attack animation to `#anim-select` and `#export-anim` (and removes others'):
| Type | Attack animation | Behavior |
|---|---|---|
| melee | `ATTACK` (generic) | Overhead swing. Held behind the body, blade up |
| chop (dagger) | `CHOP_ATTACK` | Arm raises to 120°, slams down to 50°. Weapon and arm render in front during the attack |
| two-hand thrust (spear) | `SPEAR_ATTACK` | Outside combat: carried upright like a normal weapon. During attack: rotated 90° at the grip and held level like a rifle, both hands on the shaft (front hand on top), backswing then forward lunge |
| bow | `BOW_ATTACK` | Front arm extends to the bow, draw, release snap |
| handgun | `PISTOL_ATTACK` | One-hand aim; the front arm stays down and only recoils |
| long gun | `RIFLE_ATTACK` | Two-hand aim (front hand supports the stock, rendered above the gun). Outside combat the gun is hidden behind the body |
Helmet modes (`#helm-mode`) apply to helmets marked "(Closed)": `sealed` = fully closed, `eyes` = eye slit opened, `open` = full face visible with helmet interior behind the head. Other helmets ignore the mode. Equipping any helmet hides hair.
## 5. Animations
| Name | Duration (s) | Loop | Notes |
|---|---|---|---|
| IDLE | 2.0 | yes | Breathing bounce |
| MOVE | 0.6 | yes | Walk cycle, feet keep ground contact |
| ATTACK | 0.5 | yes | Generic melee swing |
| CHOP_ATTACK | 0.5 | yes | Dagger only (conditional) |
| SPEAR_ATTACK | 0.6 | yes | Spears only (conditional) |
| BOW_ATTACK | 0.9 | yes | Bow only (conditional) |
| PISTOL_ATTACK | 0.6 | yes | Handguns only (conditional) |
| RIFLE_ATTACK | 0.6 | yes | Long guns only (conditional) |
| DAMAGED | 0.4 | yes | Knockback flinch |
| DEATH | 0.8 | no | Falls backward, ends lying down |
## 6. Exports
- **PNG** (`#btn-png`): one still IDLE pose, 192×192 px (48 native × 4). Always IDLE regardless of the selected animation.
- **Sheet** (`#export-anim` + `#btn-sheet`): 8 frames per row, **48×48 native px per frame**. One animation = 384×48. `ALL` = one row per available animation, top to bottom in the `#export-anim` listed order (e.g. 6 rows = 384×288 when a conditional attack is present).
- Frame times: looping animations sample t = i·(duration/8) for i = 0…7 (no duplicated end frame); DEATH samples t = i·(duration/7) so the final lying pose is included.
- Part adjustments, tilt, facing (flip), helmet mode, and tint are all reflected in exports.
- **Godot 4** (`#btn-godot` → dialog): exports three files — the sheet `.png`, a `SpriteFrames` `.tres` (one animation per sheet row, per-animation fps = 8/duration, `loop=false` only for DEATH) and, when `#godot-autoplay` is checked, a ready-to-run `.tscn` (AnimatedSprite2D, `texture_filter = 1` nearest, autoplay on the first animation). Dialog fields: `#godot-name` (file base name), `#godot-scale` (1/2/4 → 48/96/192 px frames), `#godot-autoplay`; confirm with `#godot-go`. Put all files in the same Godot project folder.
- Engine import settings: grid 48×48, pivot at bottom-center (ground line is 6 px above the frame's bottom edge), filter **Point/Nearest**, no compression, no mipmaps. Suggested PPU: 32.
## 7. Custom part upload
Per category, click `button[data-upload="<Category>"]`. Rules (violations are rejected or auto-corrected):
- **PNG only**, validated by file signature (renaming other formats does not bypass it).
- Canvas: **32×32 px per cell**, drawn at assembly position (the sample from `#upload-sample` shows the exact expected layout — download it and draw over it). Integer upscales (64×64, 96×96 … = N× grid) are auto-detected and downscaled.
- Multi-cell categories use one horizontal strip: **Body** = 192×32 (head | torso | left arm | right arm | left foot | right foot), **Armor** = 96×32 (torso | left shoulder | right shoulder), **Boots** = 64×32 (left | right). Others are single 32×32.
- Weapon blade / boot toe must point **+x (right)** in the art. Weapon grip is at (23,23), shield center at (10,21).
- Helmet/Hair: pixels inside the face window (columns 11–21, rows 12–16) are auto-removed so the face never gets painted over. For a **closed** helmet, draw the eye slit in **pure #000000** inside the face window — the three view modes (§4) are derived from it automatically. Choose form (cap / closed) in `#upload-form`.
- Weapon: choose the behavior type (§4) in `#upload-form`: melee / chop / two-hand thrust / bow / handgun / long gun.
- Storage: parts persist in this browser only (localStorage) — there is no account and nothing is uploaded. Use `button#btn-backup` / `button#btn-restore` to move them between browsers or machines as a `.json` file. Delete the selected custom part with `button[data-del="<Category>"]`.
## 8. Verified recipes
**R1 — Dark knight with a rifle, facing right, full sheet export:**
1. Click `#preset-default`.
2. `select[data-cat="Helmet"]` → option with text `Dark Helm (Closed)`; `select[data-cat="Armor"]` → `Dark`; `select[data-cat="Boots"]` → `Dark`.
3. `select[data-cat="Weapon"]` → `Rifle` (adds RIFLE_ATTACK), `select[data-cat="Shield"]` → `-1` (None).
4. `#anim-select` → `RIFLE_ATTACK` to preview; click `#btn-flip` for right-facing.
5. `#export-anim` → `ALL`; click `#btn-sheet`. Result: 384×288 sheet (6 rows: IDLE, MOVE, ATTACK, RIFLE_ATTACK, DAMAGED, DEATH).
**R2 — Red-haired ranger with a bow, PNG portrait:**
1. `#preset-alloff`, then `select[data-cat="Hair"]` → `Brown`, `input[data-tint="Hair"]` → `#c03030`.
2. `select[data-cat="Armor"]` → `Ranger`, `Boots` → `Ranger`, `Helmet` → `-1`, `Weapon` → `Bow`.
3. Click `#btn-png`. Result: 192×192 still IDLE PNG.
**R3 — Spearman with an open-face gold helm:**
1. `#preset-default`; `select[data-cat="Weapon"]` → `Spear`; `Shield` → `Tower`.
2. `select[data-cat="Helmet"]` → `Gold Helm (Closed)`; `#helm-mode` → `eyes`.
3. `#anim-select` → `SPEAR_ATTACK` to preview the two-hand thrust; export as needed.
## 9. Errors, limits & compatibility (the failure ledger)
- **Browser support**: evergreen browsers (Chromium, Firefox, Safari — ES modules + Canvas 2D required). Developed and CI-verified on Chromium. A responsive mobile layout exists (≤820px width); the desktop layout and all selectors are unchanged by it.
- **Selector stability contract**: element `id`s and `data-*` attributes documented in §2 are the stable automation API — they are kept backward-compatible. Option **indexes are NOT stable** (parts get added); always match options by visible text (§3 catalog) or by value, never by position. The parts table in this document is regenerated on every deploy, so a re-fetch of this file is the source of truth.
- **Upload rejection receipts** (shown in the modal status line; exact English texts):
- wrong extension → `Not a PNG file (.{ext})`
- content is not a real PNG (magic-byte check) → `Not a PNG file (content check failed)`
- file over 2 MB → `File too large (max 2MB)`
- wrong canvas geometry → `Expected {w}×{h} (or an integer multiple), got {gw}×{gh}` (multiples up to 16× are accepted and downscaled)
- helmet/hair pixels inside the face window → auto-removed with a warning, not rejected
- browser storage full → the part is added for this session but warns that it will not survive a refresh
- **No server-side state**: there is no API, no account and no upload. Everything runs in the browser, so the only cap is the browser's own localStorage quota.
- **Known 1-px cases**: none currently — every built-in weapon × animation × frame fits the 48×48 export box (checked by an automated out-of-bounds pixel scan).
## 10. License
- Characters you assemble from **built-in parts** and export (PNG or sprite sheet) are **free to use, including in commercial projects. No attribution required.**
- Do **not** redistribute or resell the raw built-in part assets themselves (the .png part files) outside of exported characters.
- **Custom parts you upload** remain your responsibility: you must hold the rights to the images you upload, and the service disclaims liability for user-uploaded content (see the site notice).
## 11. Blind-run record
Latest verification run of this spec against the live site — one row, five receipts. Updated whenever the spec or UI changes. If the newest row is older than the Spec-Version above, treat the spec as unverified.
| Date | Spec-Version | Browser build | Recipe | Export pixel-hash (SHA-256) | First selector failure |
|---|---|---|---|---|---|
| 2026-08-22 | 2026-08-22.3 | Chrome/148.0.7778.280 | R1 (§8) | `76e03984def5fd9258475c7868f708c298b389221801a03d5cc21dfb021a31a5` | none |
| 2026-08-22 | 2026-08-22.2 | Chrome/148.0.7778.280 | R1 (§8) | `76e03984def5fd9258475c7868f708c298b389221801a03d5cc21dfb021a31a5` | none |
| 2026-08-22 | 2026-08-22.1 | Chrome/148.0.7778.280 | R1 (§8) | `76e03984def5fd9258475c7868f708c298b389221801a03d5cc21dfb021a31a5` | none |
Reproduction method: execute Recipe R1 exactly as written (§8) via the documented selectors, then hash the resulting ALL sheet's **raw RGBA pixel buffer** (`getImageData` over the full 384×288 sheet canvas, SHA-256 over `data.buffer`). Pixel bytes are hashed instead of the PNG file because PNG encoders differ across browsers; nearest-neighbor integer rendering makes the pixel buffer deterministic for a given asset set. The hash changes legitimately whenever built-in assets or animations are updated — compare against the row above, not across versions.
## 12. Misc
- Presets/`#preset-random` are instant; there is no undo — re-select parts to revert.
- There is no login and no account. Custom parts never leave the browser; the only way to move them is the backup `.json`.
- Uploaded content must respect copyright; the service disclaims responsibility for user uploads (see the site notice).
- Feedback can be submitted from the page footer.
## 13. Monsters tab
Open `/monsters` directly, or click `a.tab-btn[data-tab="monsters"]`. A standalone bestiary of 17 single-piece creatures (no equipment). Assets lazy-load on first open — wait until `select#mon-select` has options.
| Control | Selector | Behavior |
|---|---|---|
| Monster | `select#mon-select` | Option value = roster index (string). 17 creatures across rig types |
| Color presets | `#mon-presets button.swatch` | Click a swatch to tint (multiply); `data-color` holds its hex |
| Color (free) | `input#mon-color` | Any color; tint is applied to exports too |
| Animation | `select#mon-anim` | `IDLE`/`MOVE`/`ATTACK`/`DAMAGED`/`DEATH` (DEATH is one-shot) |
| Flip | `button#mon-flip` | Toggles facing (`data-facing`) |
| Play/Pause | `button#mon-play` | |
| Export PNG / Sheet | `button#mon-png` / `button#mon-sheet` | Same format as character exports (48×48 frames; sheet = 5 rows) |
## 14. Character Editor tab
Open `/editor` directly, or click `a.tab-btn[data-tab="editor"]`. Draw a custom part pixel-by-pixel or load an existing one and edit it. **Saving uses the same pipeline as upload** — ground-rules (face-window clear, closed-helmet 3-mode derivation, weapon `animType`) apply automatically, and the part lands in the Character tab's `select[data-cat=…]` list.
| Control | Selector | Behavior |
|---|---|---|
| Category | `select#ed-cat` | `Body`/`Hair`/`Helmet`/`Armor`/`Boots`/`Weapon`/`Shield` |
| Load | `select#ed-load` | `-1` = new blank; ≥0 = load that built-in/custom part to edit (Body auto-loads Base 01) |
| Cell (multi-cell parts) | `#ed-cells button` | Body/Armor/Boots have multiple cells; click to switch which cell you paint |
| Tools | `button#ed-pen` / `#ed-eraser` / `#ed-fill` / `#ed-pick` | Right-click always erases |
| Color | `input#ed-color` + `#ed-palette button.swatch` | `#000000` swatch = closed-helmet eye slit |
| Undo/Redo/Clear | `button#ed-undo` / `#ed-redo` / `#ed-clear` | Also Ctrl+Z / Ctrl+Y |
| Animation | `select#ed-anim` | Previews the part on an animated body (facing left, like the Character tab) |
| Form (Helmet/Weapon) | `select#ed-form` | Same options as upload (§7); sets closed/cap or weapon type |
| Adjust | `input#ed-dx` / `#ed-dy` / `#ed-rot` | Reflected in both the edit canvas and the preview (R only for Weapon/Shield) |
| Save | `input#ed-name` + `button#ed-save` | Adds a new custom part (never overwrites) — appears in the Character tab list |
- The edit canvas is shown left-facing to match the Character tab; you draw in the art's native right-facing coordinates and clicks are auto-corrected (a weapon blade drawn toward the front/left is saved pointing +x, per §7).