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.
An installable skin package contains:
skin.json, describing the character and its animation clips;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.
The boundary is deliberate:
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;dist/; it does not execute or publish
anything from source/.Choose one validation profile:
{ "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 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.

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.
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.
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.
The stable public identity of a published package is:
(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. 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.
Each required family must provide at least v01 and may provide up to ten
variants:
idle.v01
idle.v02
war-cry.v01
war-cry.v02Every 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;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;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.
A skin may define up to four custom action families. Each may contain up to ten variants:
{
"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.
Every animation is a horizontal strip:
192 × 208 px per frame;1 and 8 frames;frames × 192;208;All skins share this default placement contract:
{
"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.
skin.json#Minimal product-profile example:
{
"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.
Product walk variants contain:
{
"strips": {
"left": "strips/walk.v01.left.png",
"right": "strips/walk.v01.right.png"
}
}Game walk variants contain:
{
"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.
A package is valid when:
skin.json parses and uses schemaVersion: 1;profile is product or game;v01;192 × 208 px frames;frameDurationsMs, when present, matches the frame count;alert-low, alert-high, and fail remain visibly distinct;The browser installer applies bounded local-ingest limits before creating any preview URLs:
skin.json no larger than 128 KiB;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.
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:
profile on newly authored packages;official field as trusted;S badge from a community skin.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 a192 × 208 pxcell. 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.
SmolForge does not prescribe:
1–8 limit;Those details can evolve without invalidating a compatible skin.