# SmolForge Character Skin Specification

Status: Draft v0.4

This is the smallest contract for a character that can move through SmolForge
product UI today and, optionally, a future 2.5D multiplayer game.

The engine decides **what action is happening**. A skin decides **how that
action looks**.

## 1. Start here

An installable skin package contains:

- `skin.json`, describing the character and its animation clips;
- one transparent horizontal sprite strip per clip;
- optional `LICENSE.txt` and `NOTICE.txt`;
- `integrity.json`, hashing the manifest and every runtime asset.

Keep generation references, contact sheets, prompts, videos, and provenance
reports in `source/`; only inert runtime media and metadata belong in `dist/`.
The machine-validatable contract is [skin.schema.json](skin.schema.json).

The boundary is deliberate:

- selected source pixels may originate in a nondeterministic art workflow;
- once those exact pixels are pinned, validation, packaging, runtime conversion,
  contact sheets, previews, integrity hashes, and the regeneration receipt must
  be deterministic;
- `dist/` contains only declarative JSON, license and notice text, and inert
  PNG/WebP media. It must never contain scripts, CSS, audio, executable markup,
  remote asset URLs, or network behavior;
- an installer validates and hashes `dist/`; it does not execute or publish
  anything from `source/`.

Choose one validation profile:

```json
{ "profile": "product" }
```

Use `product` for SmolForge web, repository, desktop-pet, and other non-game
surfaces. Use `game` only when the character supplies the complete future-game
action set.

The vNext official Thunder Smith wears a small horned smith helmet and an
uppercase `S` on a work apron. Community characters do not need an `S`, an
apron, horns, or any other SmolForge-specific body feature. The shared frame,
actions, and compositing rules are what make a skin compatible.

The accompanying official
[product-profile manifest](design/thunder-smith-kit/official/product-skin/source/skin.json)
is the finished apron-and-helmet Thunder Smith package used by SmolForge. All
eight product families, including independently authored left- and right-facing
walks, use the current identity. It is a curated, artifact-pinned package; the
legacy atlas and crop-only exporter remain a separate compatibility path.

![Thunder Smith vNext apron and horned-helmet identity study](https://gist.githubusercontent.com/swyxio/c24498dffecd22ea88697ff9771b5157/raw/thunder-smith-vnext-apron-helmet.png)

## 2. Profiles

### Product

A `product` skin must provide:

| Family | Meaning |
| --- | --- |
| `idle` | Neutral standing |
| `walk` | Left and right movement across product UI |
| `work` | Build, forge, process, type, or operate |
| `review` | Inspect, read, think, or examine |
| `wait` | Await approval, help, or user input |
| `fail` | A completed negative reaction after failure |
| `wave` | Greeting or friendly attention |
| `celebrate` | Success, merge, publish, deploy, or victory |

`interact` is recommended but optional. When it is absent, product UI may use
`work`.

### Game

A `game` skin must provide every product family plus:

| Family | Meaning |
| --- | --- |
| `interact` | Pick up, place, open, press, or operate |
| `sleep` | AFK, away, disconnected, or long idle |
| `talk` | Conversational body motion |
| `dance` | Dance or rhythmic celebration |
| `attack-short` | Close-range attack |
| `attack-long` | Ranged attack or projectile release |
| `war-cry` | Rally, intimidate, or power up |
| `alert-low` | Caution, warning, or minor error |
| `alert-high` | Danger, panic, or major alarm |

Game walking uses four isometric facings: `ne`, `nw`, `se`, and `sw`.

Combat animation remains cosmetic. Damage, range, hit timing, projectiles, and
other mechanics belong to the game engine.

## 3. Package structure

```text
my-skin/
├── source/
│   ├── reference.png
│   ├── contact-sheet.webp
│   └── provenance.json
└── dist/
    ├── skin.json
    ├── integrity.json
    ├── LICENSE.txt
    ├── NOTICE.txt
    └── strips/
        ├── idle.v01.png
        ├── walk.v01.left.png
        ├── walk.v01.right.png
        └── ...
```

A game package adds its extra action strips and replaces the product walk pair
with `walk.v01.ne.png`, `walk.v01.nw.png`, `walk.v01.se.png`, and
`walk.v01.sw.png`.

### Package identity and trust

The stable public identity of a published package is:

```text
(publisher, id, skinVersion, packageDigest)
```

`id` and `skinVersion` inside an upload are self-asserted metadata. A trusted
registry binds them to an authenticated publisher and the SHA-256 digest of the
exact validated `dist/` bytes. Reusing the same publisher, ID, and version with
different bytes is an immutable-version conflict, not an update. A package is
official only when the registry or a built-in release channel says so; neither
`skin.json` nor a filename can self-assert that status.

Keep `integrity.json` beside the installable files. Keep a deterministic
regeneration receipt in `source/` recording the canonical schema digest, source
asset digests, validation result, and QA-output digests. The receipt proves
which selected pixels were packaged; it does not claim that an upstream image
or video generator can reproduce those pixels.

Backgrounds, UI, speech bubbles, projectiles, status indicators, cast shadows,
and official SmolForge props are separate layers. Do not bake them into a
character strip.

Pages place skins through a [scene socket](scene-socket.example.json). The
scene owns its canvas, background, optional foreground occlusion layer, socket
position, rendered frame height, and initial direction. The skin owns only its
fixed ground anchor and inert character media; it never carries per-page
offsets, props, CSS, scripts, audio, or network behavior.

## 4. Clips and variants

Each required family must provide at least `v01` and may provide up to ten
variants:

```text
idle.v01
idle.v02
war-cry.v01
war-cry.v02
```

Every variant supplies:

- `id`: `<family>.v01` through `<family>.v10`;
- `name`: a short human-readable name;
- `description`: what the character visibly does;
- `whenToUse`: freeform direction for an animation selector;
- `frames`: an integer from `1` through `8`;
- `frameDurationMs`: one positive fallback duration;
- optional `frameDurationsMs`: exact timing for each frame;
- `playback`: `once`, `loop`, `ping-pong`, or `hold`;
- `posterFrame`: the meaningful still used for reduced motion, loading, SSR,
  and picker cards;
- optional cosmetic `cues`, each with a unique ID and frame number;
- `strip`, or directional `strips` for any action;
- `mirrorSafe`, defaulting to `false`.

When `frameDurationsMs` is present, it must contain exactly `frames` positive
integers and takes precedence over `frameDurationMs`.

The text fields help choose an animation. They are never executed and cannot
change product or game rules.

Playback is renderer behavior: `once` completes one cycle and signals its
caller, `loop` repeats, `ping-pong` reverses without duplicated endpoints, and
`hold` plays once and stays on its final frame. Cues are cosmetic signals only;
they never define hit timing, billing, workflow completion, or game mechanics.

## 5. Custom behaviors

A skin may define up to four custom action families. Each may contain up to ten
variants:

```json
{
  "id": "custom-01",
  "name": "Plant care",
  "description": "Small rituals involving the workshop plant.",
  "whenToUse": "Use during quiet periods or after maintenance work.",
  "variants": [
    {
      "id": "custom-01.v01",
      "name": "Water the plant",
      "description": "The character carefully waters a tiny plant.",
      "whenToUse": "Use while idle near a plant.",
      "frames": 6,
      "frameDurationMs": 150,
      "playback": "once",
      "strip": "strips/custom-01.v01.png"
    }
  ]
}
```

Custom behaviors are cosmetic unless the server explicitly binds one to a
mechanic.

## 6. Sprite strips and placement

Every animation is a horizontal strip:

- transparent PNG or WebP;
- `192 × 208 px` per frame;
- between `1` and `8` frames;
- width equals `frames × 192`;
- height is `208`;
- frames run left to right;
- no clipping or overlap.

All skins share this default placement contract:

```json
{
  "frame": {
    "width": 192,
    "height": 208,
    "anchor": { "x": 96, "y": 192 },
    "baseline": 192,
    "safeBounds": {
      "left": 18,
      "top": 16,
      "right": 174,
      "bottom": 192
    },
    "canonicalFacing": "three-quarter-front-right"
  }
}
```

Grounded frames place the midpoint between the feet, wheels, or lowest body
contact at the anchor. Airborne motion moves the character without changing
apparent scale. Keep the camera, material, proportions, and identifying marks
stable across every strip.

Do not assume a skin can be safely mirrored. Mirroring may reverse letters,
swap hands, or break asymmetric accessories.

## 7. `skin.json`

Minimal product-profile example:

```json
{
  "schemaVersion": 1,
  "skinVersion": "1.0.0",
  "profile": "product",
  "id": "thunder-smith",
  "name": "Thunder Smith",
  "description": "A tiny clay workshop familiar with an S-marked apron.",
  "reference": "reference.png",
  "credits": [{ "name": "SmolForge" }],
  "license": {
    "spdx": "CC-BY-4.0",
    "url": "https://creativecommons.org/licenses/by/4.0/"
  },
  "frame": {
    "width": 192,
    "height": 208,
    "anchor": { "x": 96, "y": 192 },
    "baseline": 192,
    "safeBounds": {
      "left": 18,
      "top": 16,
      "right": 174,
      "bottom": 192
    },
    "canonicalFacing": "three-quarter-front-right"
  },
  "actions": {
    "idle": [
      {
        "id": "idle.v01",
        "name": "Workshop idle",
        "description": "A calm breathing and blinking loop.",
        "whenToUse": "Use whenever no more specific state applies.",
        "frames": 6,
        "frameDurationMs": 160,
        "frameDurationsMs": [280, 110, 110, 140, 140, 320],
        "playback": "loop",
        "posterFrame": 0,
        "mirrorSafe": false,
        "strip": "strips/idle.v01.png"
      }
    ],
    "walk": [
      {
        "id": "walk.v01",
        "name": "Purposeful walk",
        "description": "A compact steady gait across product UI.",
        "whenToUse": "Use when moving between interface regions.",
        "frames": 8,
        "frameDurationMs": 120,
        "playback": "loop",
        "posterFrame": 0,
        "mirrorSafe": false,
        "strips": {
          "left": "strips/walk.v01.left.png",
          "right": "strips/walk.v01.right.png"
        }
      }
    ],
    "work": [],
    "review": [],
    "wait": [],
    "fail": [],
    "wave": [],
    "celebrate": []
  },
  "customActions": []
}
```

The empty arrays show the required keys compactly. A submitted package must put
at least `v01` in each required family.

Do not add `"official": true`. Official status comes from a trusted SmolForge
registry or distribution channel, never from a self-asserted upload field.

Authors are responsible for choosing and proving compatible rights for every
uploaded asset. A declared SPDX expression is metadata, not evidence that the
uploader owns those rights. Registries should preserve attribution and license
files with the immutable package.

## 8. Direction rules

Product walk variants contain:

```json
{
  "strips": {
    "left": "strips/walk.v01.left.png",
    "right": "strips/walk.v01.right.png"
  }
}
```

Game walk variants contain:

```json
{
  "strips": {
    "ne": "strips/walk.v01.ne.png",
    "nw": "strips/walk.v01.nw.png",
    "se": "strips/walk.v01.se.png",
    "sw": "strips/walk.v01.sw.png"
  }
}
```

In the fixed isometric camera, `ne` and `nw` travel away from the viewer;
`se` and `sw` travel toward the viewer. A product renderer may use `sw` as
`left` and `se` as `right` when loading a game skin.

## 9. Validation

A package is valid when:

- `skin.json` parses and uses `schemaVersion: 1`;
- `profile` is `product` or `game`;
- every family required by that profile contains `v01`;
- no family contains more than ten variants;
- no more than four custom families exist;
- every referenced file exists and stays inside the package;
- walk directions match the chosen profile;
- every strip uses `192 × 208 px` frames;
- dimensions match the declared frame count;
- `frameDurationsMs`, when present, matches the frame count;
- the background is transparent;
- frames do not overlap or clip;
- anchor, baseline, scale, camera, and identity remain consistent;
- each animation matches its declared action;
- loops are clean;
- `alert-low`, `alert-high`, and `fail` remain visibly distinct;
- short- and long-range attacks remain distinguishable.

The browser installer applies bounded local-ingest limits before creating any
preview URLs:

- at most 256 selected files;
- `skin.json` no larger than 128 KiB;
- each PNG or WebP no larger than 8 MiB;
- no more than 64 MiB across the complete package;
- names up to 80 characters and descriptions or usage notes up to 500
  characters;
- frame and per-frame durations no greater than 60 seconds.

Local installation verifies package structure, safe relative paths, matching
file extensions and MIME types, and decoded strip dimensions. Transparency,
clipping, visual identity, loop quality, and action semantics still require the
contact-sheet and animated-preview review below.

Validation/storage and activation are separate phases of one upload request.
Forge writes every file to an immutable digest-prefixed package before it moves
the user's active pointer; a partial write can never become active. Every Forge
surface reads that account pointer through the shared skin provider and falls
back to the official package when the selected package is unavailable or
invalid.

Signed-in users can keep up to 12 validated custom packages in a private
account library and switch among them without uploading the bytes again. The
library retains at most 128 MiB across installed packages and packages awaiting
deletion. Restoring Thunder Smith changes only the active pointer; it does not
uninstall the custom packages. An active custom package must be switched away
from before it can be removed.

Package removal is split across the two storage owners. D1 transactionally
removes the private catalog row and records the exact immutable R2 install
prefix. Scheduled garbage collection waits 24 hours, rechecks that the prefix
is still unreferenced, and deletes at most 100 objects from each of at most
eight packages per run. It repeatedly lists the first bounded page and retires
the cleanup record only after a strongly consistent empty-prefix result. Every
reinstall uses a new install prefix, so delayed cleanup can never delete a
newly uploaded copy of the same package digest.

Before submission, render a contact sheet and animated preview of every clip.
Repair identity drift, wrong facing, clipping, transparency artifacts, scale
popping, baseline jumping, reversed marks, or misleading action semantics.

The official Thunder Smith regeneration also checks the bottom 24 pixels of
every frame for broad, low-saturation components that are detached from the
character. A component matching that floor-shadow signature fails the build.
Detached pixels are repaired deliberately in the pinned source artwork rather
than silently rewritten during every build. Floor or contact pixels that touch
the character silhouette must be repaired from the upstream art or with an
explicit image-editing mask; the builder must not guess where a boot ends. The generated
`source/qa/walk-shadow-mask-audit.webp` shows the current walk strips above
their conservative spatial, alpha, and low-saturation candidate overlays.

## 10. Legacy v0.2 packages

A `schemaVersion: 1` manifest without `profile` is a legacy v0.2 package.
Validators should continue to require all seventeen families and four
isometric walk directions for that package.

A v0.3 validator:

- accepts legacy v0.2 without silently weakening it;
- requires `profile` on newly authored packages;
- permits recognized extra families in either profile;
- preserves unknown object fields for forward compatibility;
- strictly validates the types of known fields;
- never rejects a product skin for omitting game-only actions;
- never treats a character-supplied `official` field as trusted;
- never requires an `S` badge from a community skin.

## 11. Prompting an image model

Use one canonical character reference and generate one strip at a time:

> Create a SmolForge-compatible animation strip for `[CLIP_ID]`.
>
> Use the attached canonical character. Preserve its identity, body
> proportions, style, palette, camera, accessories, and identifying marks.
>
> Visible action: `[DESCRIPTION]`
>
> Use it when: `[WHEN_TO_USE]`
>
> Create exactly `[FRAME_COUNT]` ordered frames in one horizontal strip.
> Each frame must fit a `192 × 208 px` cell. Keep the character consistently
> scaled and place grounded contact at `(96, 192)`.
>
> Use native transparency or a flat removable background. Do not include
> scenery, floors, cast shadows, UI, speech bubbles, labels, official
> SmolForge props, unrelated characters, or detached effects. Keep every body
> part visible and prevent frames from overlapping.

Generate every directional walk strip separately. Then use deterministic image
tooling to remove the background, size cells, validate alpha, and assemble the
final package.

## 12. Intentionally unspecified

SmolForge does not prescribe:

- species, body shape, palette, material, or art style;
- a universal logo, apron, helmet, horn, or character marking;
- exact choreography;
- frame count within the `1–8` limit;
- game hitboxes, damage, or timing;
- projectile and effect design;
- atlas packing;
- how an image model creates source art.

Those details can evolve without invalidating a compatible skin.
