# Packager

> Use when turning a fully-polished Godot title (both a confirmed visual asset_pass AND audio_pass) into the inputs an Android store build needs — launcher icons at every density, the Play 512 + adaptive layers, a boot splash, gameplay screenshots, a texture atlas, an asset size-budget report, and an Android export preset. Derives the packaging set from concept.theme, records store_pass, and hands off to validator's packaging gate for "packaged".

- Skill: `qmertesdorf/packager` (Agent Skill)
- Install (CLI): `npx skillmds@latest add qmertesdorf/packager`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qmertesdorf/packager/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: qmertesdorf (https://skillmd.com/u/qmertesdorf)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qmertesdorf/packager

---


# packager

Turn a folder of mobile-grade assets into the **inputs a shippable Android build needs**, with the same legible-attribution discipline as every prior milestone: a re-runnable tool (`tools/package.mjs` + the `tools/godot/` pixel scripts) that derives the packaging set from the manifest, records it in `store_pass`, and is gated by `validator` Method 5. This runs as a title-level bolt-on **after** a game has both its visual and audio identity:

```
… → asset → [styled] → audio → [scored] → packager → validator(Method 5) → [packaged]
                                                                                 └→ (later) APK feasibility gate → store submission (owner)
```

## Inputs (both polish passes required)

- `manifests/<id>.json` carrying **both** an `asset_pass` (visual identity, owner A/B-confirmed → the game reached `styled`) **and** an `audio_pass` (audio identity, owner A/B-confirmed → the game reached `scored`). A primitive-art or silent game is **not** store-ready — both identities are mandatory (spec §2). The canonical incoming status is `scored`.
- The game on disk at `games/<id>/`, with its committed raster art under `games/<id>/art/`.
- The pinned Godot from `README.md` (`tools/package.mjs` spawns it for the pixel ops). ComfyUI **is** needed — but only for the single GPU step: generating the bespoke transparent icon focal. Everything else (atlas, screenshots, splash composite, budget, preset, build) reuses existing art or is fully deterministic.

## Outputs

- `games/<id>/store/icons/*.png` — every `iconSizeTable()` entry (launcher mdpi→xxxhdpi, Play 512, adaptive fg/bg 432).
- `games/<id>/store/atlas.png` + `games/<id>/store/atlas.json` — the texture atlas + coordinate map.
- `games/<id>/store/screenshots/*.png` — gameplay frames at the game's portrait resolution (the tool saves whatever the running game renders — `builder` ships 720×1280; it does not resize to a fixed Play-store target).
- `games/<id>/store/splash.png` (and the Godot `boot_splash` config).
- `games/<id>/export_presets.cfg` — a valid Android export preset.
- A populated `store_pass` block (icons, splash, screenshots, atlas, size_budget, export_preset, icon_master, notes).
- Hand-off to `validator` — **do not** set `packaged` yourself.

## Step 0 — Derive the packaging set FIRST (the real deliverable)

Like `asset`/`audio` Step 0, write the packaging decisions down **before** generating anything, anchored to `concept.theme` (premise/tone/mood_keywords/setting — the same modality-neutral world the visuals and audio express). The icon, splash, and screenshots are the title's **store face**; they must read as *that theme's world*, not a fourth independent interpretation. Decide and record verbatim in `store_pass`:

- **Icon master** — a **bespoke transparent focal** generated via `tools/comfy.mjs` + the asset checkpoint with **LayerDiffuse** (RGBA), square (1024²), recorded as `store_pass.icon_master` (e.g. `store/icon_focal.png`). Build the prompt in **four parts — subject → genre style → finish → palette — all derived from `concept.theme`** (these encode what ASO research finds in top-grossing game icons; the icon is one of the highest-CTR assets we ship):

  **1. Subject — the biggest CTR lever.** Pick exactly ONE dominant subject and crop it tight so it **fills ~70 % of the frame** (60–80 %), centered, edge-bleed allowed. A small subject adrift in empty canvas is the #1 amateur tell.
  - If the theme has a **character / mascot / creature**, lead with its **face**: a forward or 3⁄4-angle head with a clear **emotion** and **eyes meeting the viewer**. A face is an "eyeball magnet" and out-reads any object at thumbnail size. If the whole body would shrink the face below recognisable, **crop to just the head/face**.
  - If the theme's hero is an **object** (a sim/puzzle/cozy title with no character — e.g. shopkeep's shell), use that **single iconic hero object**, oversized and centered. Still ONE object, never a collection or a scene.

  **2. Genre style** — branch the expression/lighting/palette on `concept.theme` tone + genre:
  - **action / strategy / mid-core** → an **intense** face (angry / yelling / determined), 3⁄4 angle, dramatic rim light, **dark-saturated** *subject* lighting (deep reds / golds / blues) — but let the **plate** contrast that subject (the 48 px gate decides), don't darken the plate just to match the mood: a dark hero on a dark plate vanishes at thumbnail size while the bright complementary plate reads better *and* is the proven mid-core look.
  - **cozy / casual / sim / match-3** → a **friendly, delighted / smiling** face *or* a glossy hero object, **warm-bright** palette, soft cheerful candy-gloss lighting.
  - **puzzle / word / arcade** → a **single iconic object or one giant glyph** on a clean radial-glow plate, maximally saturated, bold simple shape.

  **3. Finish (fixed — the app-store "mini-poster" look):** `glossy, dimensional, 3D-style cartoon · soft studio lighting · bright specular highlight + rim light + soft drop shadow · simple iconic silhouette · NO text · NO words · NO scene/background · NO multiple objects`. This is deliberately **more polished/glossy than the flat in-game sprites** — the icon is the store face; a flat scaffold yields a flat, un-app-storey icon (the dogfood needed `glossy · dimensional · 3d-style · studio lighting · highlight` to land it).

  **4. Palette:** restrain to **2–3 colours with ONE signature dominant hue** (shelf recognition), saturated. Keep the **subject's hue distinct from the icon background** so it pops — `icon_compose` draws the bg from the theme palette and `resolveIconBg` rotates it toward the focal's complement (see below), so choose a focal hue that reads as the complement of, not a sibling of, that background. Also keep the subject **tonally uniform** — one bold value, **no dark core or facets**: the plate is a single flat fill, so a subject that is dark in one place and bright in another blends into *some* plate whatever colour it is (a faceted gem with a dark top facet fails *every* background — dark plates swallow the dark facet, bright/same-hue plates swallow the bright wings; a single solid-tone glowing orb of the same hue passes cleanly all round). The auto-complement only contrasts the focal's **dominant** hue, so it does its job only when that one hue stands for the whole subject — exercised on the puzzle/object branch, where a multi-tone gem WARNed on every plate and a uniform orb passed (ΔE 73.5 on the auto-complement, 32.2 on a dark plate).

  (`comfy.mjs` checkpoint must be the **exact filename** incl. `.safetensors`, e.g. `cartoonArcadiaXL_v2.safetensors` — a newer ComfyUI rejects the bare name.)

  `icon_compose` builds the adaptive layers from this focal: the transparent foreground is placed inside the ~66% Android safe zone; the background defaults to a **radial glow + edge vignette** (the app-store look — `--bg-style linear` for the old flat gradient) behind the subject, with a **soft contact shadow** under the focal on the legacy/Play composites. The background colour is auto-derived from the `concept.theme` palette and **conditioned by `resolveIconBg`**: it is forced into a deep, saturated band that survives both light and dark store chrome, and — using the focal's dominant hue (probed by `focal_hue.gd`) — rotated to the subject's **complement** when the two would clash, so a **tonally-uniform** subject pops (a multi-tone subject can still blend along the tones the complement doesn't cover — see Palette; the 48 px gate is the check). An explicit `--bg "#top,#bottom"` or recorded `store_pass.icon_bg` is treated as a deliberate choice and used **verbatim** (no conditioning). This also fixes the old bug where the adaptive foreground and background were identical (square-stretched sprite used for both). The **final** icon/splash art is an **owner aesthetic A/B**, recorded as deferred in `notes`.

  **Legibility gate (48 px).** `icons` auto-runs `legibility`: `icon_legibility.gd` downscales the Play composite to 48 px and builds a subject **silhouette mask** by contain-fitting the focal's alpha into the same `COMPOSITE_SAFE` box the focal was composited into, then the pure JS `scoreIconLegibility` walks that silhouette and scores the **CIELAB ΔE** between the subject's edge and the plate immediately behind it — reporting the **worst-reading arc** (10th-percentile ΔE), not an average. (ΔE, not luminance — a complementary pairing like coral-on-teal reads high-contrast yet has near-equal luminance.) Worst-arc is deliberate: a subject can contrast the plate on three sides and **melt into it on the fourth**, and that fourth side is what kills the read at thumbnail size — the earlier centre-vs-corner metric missed exactly this (it scored a dark-cored gem on a bright same-hue plate as "high contrast" while its rim blended into the plate). A worst-arc ΔE below the floor prints a `LEGIBILITY WARN`. It is **advisory** (record it in `store_pass.notes` and prefer to fix it; it does not block the build) — but **trust it over genre-mood instinct**: a dark, "intense" action plate can score *worse* than a bright one because a dark hero's edges vanish into it; the legible choice (and the proven mid-core look) is the **bright complementary plate the auto path already picks**. Fix a WARN by enlarging the subject, giving it a **uniform tone or a bright rim** so its whole outline separates, or choosing a plate that contrasts the subject's *actual* tones — not its genre's mood.

  **Where the focal lives:** `comfy.mjs gen <id> icon_focal …` writes to `games/<id>/art/icon_focal.png`. **Move it to `games/<id>/store/icon_focal.png`** before composing — `atlas` packs every `art/*.png`, so a focal left under `art/` pollutes the sprite atlas. Set `store_pass.icon_master` to `store/icon_focal.png`.
- **Splash** — the boot image and whether to show it.
- **Screenshots** — which gameplay moments read best in a store listing (e.g. a mid-combo frame, a near-miss).
- **Atlas membership** — which sprite PNGs pack into the atlas.
- **Size budget** — the per-title byte budget (default 50 MiB; override via `GAMEFORGE_SIZE_BUDGET` or the tool opt).

## Flow

Run the tool (it pairs the pure JS seam with the Godot pixel scripts, exactly like `comfy.mjs`):

```
node tools/comfy.mjs gen <id> icon_focal '<recipe-json>'   # bespoke transparent icon focal (LayerDiffuse, square 1024², icon scaffold) -> games/<id>/art/icon_focal.png
mv games/<id>/art/icon_focal.png games/<id>/store/icon_focal.png   # keep the focal OUT of the sprite atlas; set store_pass.icon_master to store/icon_focal.png
node tools/package.mjs icons <id> [--bg "#top,#bottom"] [--bg-style radial|linear]   # focal -> adaptive fg/bg + composited legacy/Play; radial glow + drop shadow + auto-contrast bg by default; auto-runs the 48px legibility check
node tools/package.mjs legibility <id>   # ASO 48px ΔE check: does the subject still pop at thumbnail size? (also auto-run by `icons`; advisory WARN)
node tools/package.mjs atlas <id>        # pack art/*.png → store/atlas.png + atlas.json (headless Image)
node tools/package.mjs screenshot <id> --script res://_shots.gd   # run the game's capture harness on the REAL renderer
node tools/package.mjs splash <id> [#RRGGBBAA]   # composite the icon master onto a themed bg → store/splash.png (headless Image); pick the bg from concept.theme
node tools/package.mjs budget <id>       # sum store assets vs the budget (run AFTER icons/atlas/screenshot/splash so it includes them)
node tools/package.mjs preset <id>       # print the Android export_presets.cfg (redirect into games/<id>/export_presets.cfg)
node tools/package.mjs build <id>            # build the debug APK via headless Godot (toolchain-guarded: skips with exit 3 if ANDROID_HOME unset)
node tools/package.mjs build <id> --release --aab   # build the signed release AAB (needs tools/android-signing.local.json)
```

`screenshot <id> <name> [frames]` still works as the boot-only fallback (no harness script needed), but the `--script` form is preferred: it drives the game's own capture harness through showcase moments rather than capturing a single boot frame.

Then record `store_pass` (arrays replace wholesale — pass the full set):

```
node tools/manifest.mjs merge <id> "{\"store_pass\": { \"icon_master\": \"store/icon_focal.png\", \"icon_bg\": \"#top,#bottom\", \"icons\": [ … ], \"splash\": { … }, \"screenshots\": [ … ], \"atlas\": { … }, \"size_budget\": { … }, \"export_preset\": { \"path\": \"export_presets.cfg\", \"platform\": \"android\", \"package\": \"com.gameforge.<id>\" }, \"build_artifact\": { … }, \"notes\": \"…\" }}"
node tools/manifest.mjs validate <id>
```

Record `store_pass.build_artifact` from the `build` command's JSON (format, build_type, path, bytes, package). If the build skipped (exit 3, no Android toolchain), omit `build_artifact` and note the deferred toolchain gate — do not fabricate a record.

## Hard requirements & honesty

- **Headless vs real renderer.** Icon resize + atlas composite use Godot's `Image` API and run **headless**. Screenshots **must not** be headless — the dummy renderer captures no pixels; `package.mjs` runs the harness on the real Vulkan renderer (see the `asset` raster note + `godot-binary-path`).
- **Capture-harness pattern.** Copy `tools/godot/shots.template.gd` → `games/<id>/_shots.gd`. Edit the `const PHASE` preload and drive the view's state to each showcase moment the same way `selftest` Stage 6 does: set phase/collections directly, call `main._rebuild_ui()`, then capture. Clear `user://save.json` at the start and end of the script so each run is clean. The harness prints `wrote <path> (WxH)` per frame and `SHOTS OK` on completion. Commit `_shots.gd` alongside the game — it is a permanent artifact like `selftest.gd`, not a throwaway.
- **Recording screenshots — assemble the schema shape, don't paste tool output.** `package.mjs screenshot` returns `{name, source, path}`, but `store_pass.screenshots[]` records `{name, px, source}` (schema-validated). When you merge, **drop the tool's `path`** (it's an absolute disk path) and **add `px`** as the captured `"WxH"` (e.g. `"720x1280"`). Pasting the tool's object verbatim fails the very next `validate` step (extra `path`, missing `px`). (This applies to **boot mode**. The `--script` runner instead returns `{ script, shots: [{name, px, source}] }` — the `shots[]` entries are **already** schema-compliant; record `shots` directly as `store_pass.screenshots`, not the outer object.)
- **Mobile density.** Every launcher density comes from **downscaling** the high-res master, never upscaling — that is the app-store readiness the raster masters were sized for.
- **Boot splash.** `splash` composites the icon master (~60%, centered) onto a themed solid background at the canonical portrait size (`splashSize()` → 1080×1920) and returns the `boot_splash_cfg` block for `project.godot`'s `[application]` section. `store_pass.splash` records only `{source, show_image}`; splicing `boot_splash_cfg` into the game's `project.godot` is applied at **real-package time** alongside the export preset (the foundation commits the asset + record, not the runtime project change). The v2 icon focal is **transparent**, so the splash composites cleanly over whatever background colour you choose — no opaque-bg bleed. The old caution ("A splash composited from a master with an opaque background will show that background") applies only as a fallback when an opaque master is unavoidable; prefer the transparent focal.
- **IP safety.** The icon/splash/screenshots inherit the art's IP posture; do not introduce franchise/character/studio likenesses. The owner aesthetic A/B is the final IP review (same as `asset`).
- **Do not** edit `concept`, `builder`, `asset`, or `audio`. Consume `concept.theme` + the two pass blocks as-is.
- **Do not** set `packaged` — hand to `validator`.

## Deferred (named gates — track, do not fake)

- **APK/AAB build is now in scope** (no longer deferred): `node tools/package.mjs build <id>` shells out to headless Godot, guarded by `ANDROID_HOME`. On a toolchain-equipped machine it produces a debug APK (and `--release --aab` a signed AAB) and records `store_pass.build_artifact`. Without the SDK it skips cleanly (exit 3) — the same no-GPU/no-ComfyUI posture. The one-time machine setup (debug keystore, Godot editor SDK path, AVD) is documented in `README.md` → "Android export" + `tools/android-setup.ps1`.
- **Real store submission** (Play developer account, release-keystore custody, listing copy, content rating, legal) → owner-gated. The signed AAB is built locally now; uploading it is the owner step (Play developer account, release-keystore custody, listing copy, content rating, AAB upload).
- **Icon/splash aesthetic A/B** → owner-gated, like every art/audio A/B.

## Hand off to the validator

Hand off to `validator`, which runs **Method 5 — packaging gate** (both pass blocks present + A/B-confirmed; every icon at its exact px; atlas map covers every member; budget passes; export preset parses; headless run still clean; cross-modal cohesion A/B) and advances `scored → packaged` on success, or records legible `issues` (attributed to `packager`/`package.mjs`) and stops.

