brawl-creator

Officialneta-search/brawl-creator
Use it in Cohub

Paste this into your agent's chat — it fetches the skill and follows it.

Use the skill: brawl-creator. Run `npx -y @neta-art/neta-search-cli@latest skills use neta-search/brawl-creator --via site` and follow what it prints. Read its complete output, redirecting it to a temporary file first if necessary, and resolve relative paths from the skill directory it prints.
Use in Neta Studio Open it in Neta Studio and see what people made with it.
Install from a terminal
npx -y @neta-art/neta-search-cli@latest skills use neta-search/brawl-creator
Summary

Turn any character you make into a playable fighter in a full crossover brawler game.

  • Add your character to a working fighting game, with their own moves, animations and fighting style on screen.
  • You pick: original character or an existing one, human / beast / machine / mystery creature, speed-fighter or power-fighter.
  • Comes as a playable browser game with a fighter select screen your character appears on alongside the others.
  • A character with a clear, distinct look (and a reference picture if you have one) makes for the strongest fighter art.
  • You get a shareable link to the finished game, ready to play and show off.
SKILL.md
# Brawl Creator — runtime + fighter-production skill

This skill ships a complete, playable 2D fighting game as a bundled runtime,
plus the workshop flow that grows its roster. It is self-contained: engine,
sprite pipeline, QA gates, and onboarding all live in this package. The
runtime ships **code only** — every media asset (fighter sheets, FX, stages,
voice, BGM, fonts) loads from Cohub's public asset CDN through the runtime's
`ASSET_CDN` table. `tools/upload_cdn.py` + `tools/sync_cdn_map.py` turn any
provided file into a CDN link and wire it in, in one command.

```text
One runtime.  Two modes.  Three hard gates.  One promise.
```

**The promise (definition of done):** when this skill hands the game over, the
fighter the user asked for is on the select screen of the *published* Work.
A brawl whose roster is only the bundled NAIWA is the starting point, never a
deliverable — not on a fresh install, not "while the card waits". Done is
proved by `tools/verify-roster.mjs <work-dir> <cid> --live` printing
`ROSTER_OK` (it reads the `ROSTER_IDS` the published page actually loads).

- **Runtime** — `runtime/brawl-v1/` is a full snapshot of the game (vanilla JS
  + Canvas, no build step, data-driven roster; no local media — assets stream
  from Cohub's CDN, URL map in `assets/cdn-map.json` + `js/sprites.js`).
  Install = copy. Publish = the host's web-work flow.
- **Two modes** — WORKSHOP (taste round → spec card with an accepted cover) and
  BUILD (sprite production → registration → QA → publish). WORKSHOP is the
  default entry; BUILD never starts without WORKSHOP's accepted card, and
  follows it in the same turn.
- **Three hard gates** — (1) no sprite generation without a spec card whose
  cover passed acceptance (white bg, full body, zero text); (2) no compositing
  with colorkey cutouts; (3) no publish without passing the strict QA audit +
  in-game screenshot check — and no handover without the roster gate
  (`verify-roster.mjs --live` → `ROSTER_OK`).

## Path rule

All paths in this skill are relative to this SKILL.md's directory. Resolve
externals through `tools/resolve-paths.mjs` (variables `$NETA` CLI, `$SPRITE`
sprite subskill, `$BRAWL` this skill). Unresolved means blocked: report the gap
and stop — never guess a location or improvise a substitute.

## Install (first use in a world)

**Before installing, always check for an EXISTING brawl work in this world**
(this rule was added after a second install briefly forked the roster in
practice — never create a second one):

```bash
node $NETA work list                 # look for an already-installed/published brawl web work
ls works/webs/*/js/data.js           # its data.js is the roster source of truth
```

If one exists: do NOT copy the runtime again. Register the new fighter into
the existing work (BUILD mode steps 4-6) and re-publish it — a second copy
would fork the roster and split the published link.

Only when no brawl work exists in this world, install from scratch:

```bash
# 1. copy the runtime into the world's works dir (skip if already installed)
cp -a runtime/brawl-v1 works/webs/<game-slug>

# 2. boot-verify: serve + headless screenshot, zero console errors
cd works/webs/<game-slug> && python3 -m http.server 8901 &
google-chrome --headless --disable-gpu --no-sandbox --window-size=1200,700 \
  --screenshot=/tmp/boot.png --virtual-time-budget=15000 \
  "http://localhost:8901/index.html"
```

The runtime boots as a **single-fighter** game — NAIWA (奶蛙) only, matches are
mirror duels. That bare roster is scaffolding: the user's fighter is what they
came for, so the first published version they are handed must already contain
it (see "The promise"). Each new fighter's assets are registered to the CDN and
the engine in BUILD mode (step 4).

Install is not the opening move when the taste round is still open: if the
opening leaves any required Beat 1 item unanswered, ask Beat 1 first and stop —
do not copy, publish or hand over the bare runtime in that turn.

Once the taste round is settled (given in the opening or answered), install +
boot-verify, then run Beat 2 → BUILD **in the same turn without stopping**.
Never hand over (link card, URL, "already playable") a NAIWA-only brawl as the
result of a request for a fighter — that is exactly the "I only see 奶蛙"
failure. An early `work publish` of the bare runtime is allowed only as an
internal smoke test; it is not mentioned or linked to the user. BUILD's
publish (step 6) is the one the user receives.

## Mode A — WORKSHOP (asking contract; owns how it asks)

**Do not run the questionnaire skill.** The workshop's three-beat flow IS this
skill's asking contract. Only Beat 1 may stop the turn (and only when a
required item is open); Beats 2 → 3 → BUILD run through in one turn.

The workshop script is voice-neutral on purpose (see `references/onboarding.md`):
questions are asked in the host world's own voice; this skill supplies the
questions and the gates, never the persona.

**Reply language follows the user, in every mode.** This skill is written in
English; that is not the reply language. Every user-facing line — Beat 1
questions, the spec card presentation, BUILD status lines, the handover and
its closing options — is written in the language of the user's opening
message (or their latest message if they switched), from the first turn
through BUILD to the final handover. Do not drift into English once BUILD or
the English cards (`BUILD-CARD.md`, `WORKSHOP-CARD.md`) are read. In-game
titles, fighter names and move names keep the user's own wording.

### Beat 1 — taste round (only when a required item is open; then stop)

Beat 1 is the only beat that stops the turn. Ask, in the host's voice: original or bring-someone-in? Human / beast / machine
/ mystery-thing (offer directions)? SPEED or POWER? One signature-move imagining
(optional)? If bringing someone in: collect a text description and (if any) a
reference image — save it now; production uses it as `--ref-file`.

When to ask — check each item against the opening first:

- Required: original vs bring-in, kind, SPEED/POWER. A named existing
  character settles both original/bring-in and kind. Signature move and
  reference image are optional — never a reason to ask on their own.
- Any required item open → ask only the open items, in one turn, before any
  install, publish or generation; then stop and wait.
- Everything required already given → don't re-ask; go straight to Beat 2.

### Beat 2 — spec card (hard gate #1, no stop)

Land the fighter as a **character material on the world canvas** before any
sprite generation — it is the identity source for every prompt downstream:

1. Create the card with the host's atom tooling, **`--no-cover`** — the
   platform's auto card-face template stamps text onto artwork and fights the
   art contract (verified 2026-08-28/29). The card exists bare first.
   Do not touch world settings for this: no `neta config set`, no manifest /
   worldConfig edits (visualStyle, portal refs, …) — the game's look is
   decoupled from them and a `--no-cover` card does not need visualStyle. If
   the CLI still refuses without one, ask the creator before setting it. Never
   delete files that existed before this run.
2. Produce the confirmation cover on the guaranteed path (identity ref + pinned
   style word + white-bg full-body + zero-text prohibitions — exact prompt in
   `references/art-contract.md`), then bind it with `atom cover set`.
3. Acceptance: pure-white background, full-body, zero visible text. Rerender on
   the same work id until it passes. Never present a non-conforming cover, never
   fall back to the auto-cover or `atom cover retry`.
4. Show the card in one short status line (name, type, signature move) and
   **continue straight into BUILD in the same turn — do not stop for a nod**.
   The user asked for a playable fighter; the acceptance check in step 3 is
   the gate, not a confirmation round. Stop here only if the user explicitly
   asked to review the card first ("先给我看卡", "show me the card before you
   build") or asked for a card only. Identity changes the user asks for later
   are a follow-up round (back to Beat 2 for that fighter, then re-BUILD).

### Beat 3 — handoff

State the real cost in one line (sprite video rounds, cutout, QA, publish —
takes real time and real generation credits) and keep going — it is a heads-up,
not a question. BUILD mode begins in the same turn.

## Mode B — BUILD (production line)

Default round = one fighter, end to end, in the same turn as Beat 2. Read
`BUILD-CARD.md` before starting.

```text
0. opening announcement (one line, host voice) — required before any tool call
1. preflight  — tools/resolve-paths.mjs + /workspace/.pylibs deps
2. sprite production — subskills/sprite-sheet-builder/ (motion-grid engine,
   2 rounds of 3x3, BiRefNet cutout, colorkey = hard fail)
3. compose    — tools/build_game_sheets.py (per-frame 330/360/380 normalize,
   colorkey rejection, anchors.json)
4. install    — drop strips under assets/img/<cid>/, then
   `python3 $BRAWL/runtime/brawl-v1/tools/sync_cdn_map.py
   <runtime>/assets/img/<cid>` (uploads to the CDN in parallel, rewrites the
   ASSET_CDN table); write the DATA entry
   (references/fighter-data-schema.md; two tiers: crossoverMoves fast path or
   full custom moveset)
5. QA         — tools/qa_fighter_assets.py <cid> strict gate, then headless
   screenshots (fight + select + ?pose), fix and re-run until green
6. publish    — host web-work flow (neta work publish web), verify 200s,
   then the roster gate: `node $BRAWL/tools/verify-roster.mjs
   works/webs/<game> <cid> --live` must print ROSTER_OK
7. handover   — the playable link + what this round covered + what's open
```

**If BUILD cannot finish** (credits run out, a provider is down, QA stays red):
do not dress the NAIWA-only game up as the delivery. Say in one line that the
fighter is not in the game yet and why (blocked / needs another pass), keep the
card on the canvas, and offer the resume. A link card to the game in that turn
must carry a subtitle that says the fighter is still in production.

Handover card: hand the game over through the **interaction-components**
skill as a link card whose target is the published brawl Work itself —
`{"type":"work","workId":…}`, the id read from `preview.workId` in the work
dir's `meta.json` that step 6's publish wrote. Not a material card for the
fighter, not an `external-url`, not a `world-file` to `index.html`. The same
applies whenever a turn ends with a published (even base-roster) brawl.
Write the card in the same turn as the publish: verify with bounded `curl`
(`-m 20`) on the new asset URLs and the bounded `verify-roster.mjs --live`
(never a headless browser of the live page). `ROSTER_FAIL` is not "pending":
fix the DATA entry / re-publish and re-run it before handing over. Only a
network timeout of the check itself (exit 2) may hand over with a status line —
the card is never skipped, and no tool call follows it.

## Subskill index

| Subskill | Path | When |
| --- | --- | --- |
| sprite-sheet-builder | `subskills/sprite-sheet-builder/` | sprite production (mode B step 2); reads its own SKILL.md card |

The parent never duplicates the subskill's content: motion-grid prompting,
cutout policy, and extraction commands live in the subskill and its references;
DATA schema, art contract, and onboarding live here.

## Art contract (decoupled from world visualStyle)

The game owns its look, independent of the host world's `visualStyle`:

- 16-bit pixel-art roster, 380px frames, ~330px stand height, transparent sheets.
- New fighters follow the ROSTER's density, not the world's style — if the world
  is oil-painted, fighters are still pixel art. Full contract:
  `references/art-contract.md`.
- The style word is pinned in the confirmation-cover prompt and must match the
  roster, never the world style.

## Onboarding (install-time greeting)

`references/onboarding.md` holds the voice-neutral three-door script (play /
workshop / roster) with slots for the host world's persona. Read it when the
game is installed in a world with its own resident voice; skip it in worlds
without one.

## Failure playbook

| Symptom | Fix |
| --- | --- |
| identity drift between actions | regenerate that action's cell; keep the rest — per-action repair, never whole-fighter |
| baked text/labels in first frame | regenerate first frame before video; never animate a labeled frame |
| any action falls back to colorkey cutout | hard fail — fix cutout (wrapper update / provider), re-extract; never publish colorkey frames |
| fighter too small in frame (<200px of 380) | occupancy below 55% or global-scale bug — re-shoot with ≥55% occupancy |
| fighter floats/sinks | anchors wrong — never hand-edit per-frame; the composer owns baseline 360 |
| select card empty | anims entry missing or file mismatch — check DevTools network |
| hitboxes whiff | tune move `box` ranges against a working fighter's reach |
| gray-slab residue frames | qa_fighter_assets.py catches them — drop bad source frames, re-assemble |
| new fighter HD vs pixel roster | pixelate its sheets to roster density (2x nearest-neighbor) before shipping |
| publish misses new fighter | re-publish (content-hashed); verify the new asset URLs return 200 and `verify-roster.mjs <dir> <cid> --live` prints ROSTER_OK |
| user sees only NAIWA on the select screen | the fighter never reached the published `DATA` (round stopped at the card, or `data.js` cache-bust missed) — finish BUILD, bump `?v=` on data.js, re-publish, re-run the roster gate |
| SecurityError: canvas tainted | a CDN image loaded without CORS — the loader sets `crossOrigin` on CDN assets (sprites.js); check the URL is in ASSET_CDN, never strip the readback |
| CDN asset 404 in game | map/sprites mismatch — fix `assets/cdn-map.json`, then `python3 tools/sync_cdn_map.py --write-back`; re-upload if the link itself died |

## Host workspace policies

Obey host-world rules (logs, records, public share). This world's host rule:
record changes in `logs/`. The runtime itself never writes outside the project
folder and `runs/`.