実行中のゲームからストアアセットを生成する
実際に動作しているゲームをキャプチャして、忠実なストア画像を生成します(モックアップではありません)。再利用可能なハーネスは scripts/ にあり、ゲーム固有の小さな設定(URL、準備完了確認、ゲームのポーズ/非表示/操作、タイトルテキスト)を指定します。
3つの成果物:
- スクリーンショット (
scripts/screenshots.cjs) — 正確なストアサイズで一括出力します。 - フィーチャーグラフィック — 1024x500のプロモーションバナー。ランドスケープの
targetでゲームフレームを全面に表示するか、フレーム+タイトル構成(scripts/feature_graphic.py)かを選びます。デフォルトは気分重視のゲームでは全面表示です。 - 掲載テキスト — 短い説明+詳しい説明:
references/listing-text.md(制限、トーン、主張してはいけないこと、「AI生成アセット」フォームの質問)を参照してください。
ストアサイズの要件とプリセットについては references/play-store-sizes.md を参照してください。
ワークフロー: 多くの候補 → ユーザーが選択
ユーザーが選択します。あなたはバリエーションを提供し、選択を容易にします。
- 各ステージ/レベル/テーマごとに多くのフレームをキャプチャします(例: 5ステージ×8〜10枚)。ゲームの進行度も利用してバリエーションを付けます(初期/中期/後期のアップグレード段階)。初期のフレームは空っぽに見え、後期のフレームは被写体が埋もれる可能性があります。
- バリエーションを持たせつつ約15枚に絞り込みます(各ステージ、成功/失敗の瞬間、豊かな背景と落ち着いた背景)。最初に
scripts/contact_sheet.py(番号付き)で自分で確認します。次にユーザーに選択させます — ピッカーページ(後述)が利用可能な場合はそれを使用します(ユーザーは一度見るとそれを要求します)。そうでない場合は番号付きシート画像を送り、番号を受け取ります。 - セットを推奨し、その理由を説明します(色/ステージのバラエティがセット全体にわたっていること、同じ瞬間を繰り返さないこと)。ユーザーのコメントに応じて再撮影し(「被写体をもう少し中央に」)、そのリテイクを同じピックキーを持つ別のグループとしてピッカーに追加します。
- 最終セットをアップロード順に1つのフォルダにコピーし(
1-…png、2-…)、決定事項(ファイル、選択されたテキストの原文、却下されたオプションとその理由、キャプチャ方法)をプロジェクトのドキュメントに記録します。生成された画像は、プロジェクトが既にストアアセットを追跡している場合を除き、git には含めないでください。
python3 ${CLAUDE_SKILL_DIR}/scripts/contact_sheet.py sheet.png a.png b.png … --cols 5 --thumb 300
(基本的なPlaywright/Canvas技術は game-icon-from-screenshot と共有されています。)
ピッカーページ(Artifact): ユーザーがセットを選択
python3 ${CLAUDE_SKILL_DIR}/scripts/build_picker.py picker.json picker.html を実行し、picker.html をArtifactツールで公開します。capabilities: {"db": {}} を指定します(Artifactツールが必要なため、最初に artifact-design / artifact-capabilities スキルをロードしてください)。仕様形式は scripts/build_picker.py のヘッダーにあります。
このページが行うこと(すべて仕様通りで、テンプレートにはゲーム固有のものはありません):
- 画像グループ —
mode: "multi"(スクリーンショット: チェック順 = アップロード順、← → で並べ替え、maxあり)または"single"(フィーチャーグラフィック、アプリアイコン)。複数のグループが1つのpickキーを共有できます(最初の候補+リテイク = 1つの選択肢)。recからの「おすすめ」チップ、⤢ ズーム、上部に選択セットを表示するトレイ、「おすすめで埋める」ボタン。 - テキストグループ — 提出テキスト、その下に翻訳、
limitに対する文字数カウント、各オプションの右上にコピーボタン(提出テキストのみコピー)を備えたラジオオプション。 - コメントを指示として — グループごとのコメントボックス(
noteKey)と全体への「全体への指示」。ユーザーはそこに指示を書き(「2 案目の最後の行はいらない」、編集および翻訳する日本語ドラフト)、「保存しました」と言います。 - 選択肢 — 小さなはい/いいえのラジオボタン(フィーチャーグラフィックにタイトルテキストを入れるかどうか)。
- すべては1つのdbドキュメント(
doc、デフォルトpicks/current)に保存されます。ArtifactData(action: "get", collection: "picks", doc_id: "current"がデフォルト)で読み取ります。ピック/選択肢/ノートキーはそのドキュメントを共有するため、ビルダーは、ユーザーが保存したと言うたびに、キーの衝突や重複したアイテムIDを拒否します。空でないすべてのノートに基づいて行動し、何が変更されたかを伝えます。ページを変更するには、specを編集して再ビルドし、同じファイルパス(同じURL)で公開します。保存された選択はIDが同じままなので保持されます。最初の公開後、picksを1回読み取ってストアの回答を確認します。
アプリアイコンも同じページを使用します。アイコンのみのピッカー(1つの "single" グループ、aspect: "1/1"、thumbWidth: 512)またはストア画像の隣のもう1つのグループとしてアイコンを表示します。候補は game-icon-from-screenshot で作成します。
前提条件
- ローカルURLで実行中のゲーム(
npm run dev→ 例:http://localhost:5173)。 - Playwright + Chromium(プロジェクトの
node_modules、npxキャッシュ、またはグローバルから自動解決。それ以外の場合はnpx playwright install chromium)。 - Pillow(
python3 -m pip install Pillow) — リサイズ/合成用。 - フィーチャーグラフィックのタイトル用のバンドルフォントファイル(
.ttf。可変フォントはウェイト軸で対応)。
アンチエイリアシング: スーパーサンプリング、アップスケールはしない
正確な出力サイズでキャプチャするとギザギザになります。高密度でキャプチャし、ダウンスケールします。superSample(deviceScaleFactor)を2に設定し、ハーネスがターゲットサイズにダウンスケールします。重要: 密度は superSample で行い、はるかに大きなビューポートを使用しないでください。非常に大きなキャンバスはヘッドレスレンダリング/物理ループを停止させる可能性があります(ゲームがスポーン時にフリーズします)。ゲームが実際に動作するサイズ(例: 1080x1920)でビューポートを維持し、代わりに superSample を上げてください。
スクリーンショット
node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs <config.(cjs|json)> [outDir]
- 静的ポーズショット (
shots): それぞれ新しいページを開き、pose関数を実行し(スコア設定、被写体の位置調整、結果オーバーレイの表示など)、オプションでHUDを非表示にし、キャプチャします。準備画面や結果画面に適しています。 - ライブゲームプレイ (
live): プレイを開始し、ポーリングごとにkeepAlive関数を実行してゲームをアクティブ/操作し続け(「オートパイロット」)、pick関数がフレームが良いと判断したときにキャプチャします。ライブフレームは実際の動きや軌跡を示します — これが「実際にプレイしている」ショットを取得する方法であり、静的なものではありません。多くキャプチャし、ユーザーに選択させます。
fn フィールドはページ内で実行されます。.cjs 設定では実際の関数を記述します(.toString() でシリアライズされるため、自己完結型に保ちます — Nodeクロージャは使用しないでください)。.json 設定ではアロー関数のソース文字列を記述します。
- タイマー (
timed): 自分でプレイできるゲーム(?autoplay=クエリ、デモ/アトラクトモードなど)向け。各URLを順に開き、intervalMsごとに撮影します — ステージごとに多くの実際のフレームを取得する最も簡単な方法です。
設定(スクリーンショット)
url、ready、target {width,height}(出力サイズ+ビューポート)、viewport(異なる場合のキャプチャサイズ。フレームはターゲットアスペクトにクロップされ、cropY がバンドをアンカーします)、superSample(2)、launchArgs、executablePath、css(注入される。DOM HUDの最も簡単な非表示方法)、routes [{match, replace: [[from,to]…]}](キャプチャのみの提供ソースのパッチ)、waitFontsReady(true)、hudHide(fn)、shots [{name, pose?, hideHud?, settleMs?}]、live {count, start, keepAlive, pick, hideHud, buildMs, pollMs, settleMs, maxTries, prefix}、timed {urls: [{prefix, url}], count, warmupMs, intervalMs}、outDir。完全なフィールドドキュメントは scripts/screenshots.cjs のヘッダーにあります。
Playwrightは解決可能でなければなりません。node_modules にそれが含まれるディレクトリから実行するか、NODE_PATH=<dir>/node_modules node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs …(CommonJS)を実行します。インストールされたブラウザがパッケージより古い場合、スクリプトは ms-playwright キャッシュ内の最新のChromiumにフォールバックします。
例(WebGL / three.js、自己再生、DOM HUD)
{
"target": {"width": 1080, "height": 1920},
"viewport": {"width": 540, "height": 960},
"superSample": 2,
"launchArgs": ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
"css": "body > div:not(#app) { display: none !important; }",
"timed": {
"urls": [{"prefix": "stage1", "url": "http://localhost:5173/?level=1&autoplay=1"},
{"prefix": "stage2", "url": "http://localhost:5173/?level=2&autoplay=1"}],
"count": 10, "warmupMs": 3000, "intervalMs": 3500
}
}
ポートレートのみのゲーム(ワイドビューポートをレターボックス化し、ポートレートビューを超えるとワールドが終了する)の全面1024x500バナー: キャプチャ時のみアスペクト制限を解除し、2倍でより正方形のビューをキャプチャし、バンドをクロップします。
{
"target": {"width": 1024, "height": 500},
"viewport": {"width": 820, "height": 640},
"superSample": 2,
"launchArgs": ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
"css": "body > div:not(#app) { display: none !important; }",
"routes": [{"match": "GameConfig", "replace": [["maxAspect: 0.6", "maxAspect: 0"]]}],
"timed": {"urls": [{"prefix": "banner", "url": "http://localhost:5173/?level=1&autoplay=1"}],
"count": 6, "warmupMs": 3000, "intervalMs": 5000}
}
例(Phaser、ポートレート。devが window.__game を公開)
ある特定のゲーム(玉をリングに通すゲーム)の実際の作業例です。自分のシーン/オブジェクト名、ポーズロジック、ドライブ/ピック条件に置き換えてください。
// phone.shots.cjs
const G = () => window.__game.scene.scenes[0]; // ページ内ヘルパー(以下にインライン化)
module.exports = {
url: "http://localhost:5173/",
target: { width: 1080, height: 1920 },
superSample: 2,
ready: () => !!(window.__game?.scene?.scenes?.[0]?.ring),
hudHide: () => { const s = window.__game.scene.scenes[0]; ["scoreText","bestText","hintText"].forEach(k => s[k]?.setVisible(false)); },
shots: [
{ name: "ready", hideHud: false },
{ name: "result", hideHud: true, settleMs: 1200, pose: () => {
const s = window.__game.scene.scenes[0];
s.matter.world.pause(); s.score = 18; s.highScore = 18; s.lastRunNewRecord = true; s.showResult();
} },
],
live: {
count: 24, prefix: "play", pollMs: 45, buildMs: 700,
startTap: true, // 信頼できるタップで開始(Phaserは合成イベントを無視)
hideHud: false, // ゲームプレイショットではスコアを表示
start: () => { window.__game.scene.scenes[0].goals = 6; }, // 追加セットアップ(例: バリエーションを有効にする)
keepAlive: () => { const s = window.__game.scene.scenes[0]; if (s.state === "playing" && s.ball.y > s.scale.height * 0.5) s.ball.setVelocityY(-13 * s.scaleFactor); },
pick: () => { const s = window.__game.scene.scenes[0]; return s.state === "playing" && s.ball.body.velocity.y > 4; },
},
};
タブレットセット: 同じ設定で target: { width: 1440, height: 2560 }。(選択した電話ショットを1440x2560にアップスケールして、選択したフレームを正確に維持することもできます。)
ポーズアニメーション (scripts/pose-capture.cjs) — postFX/ブルームゲーム向け
live モードは、ゲームの物理がヘッドレスで実行されることを前提としています。しかし、多くの場合そうではありません。 WebGLのブルーム/postFXパイプラインはヘッドレスレンダリング+物理ループを停止させる可能性があり、被写体はスポーン時にフリーズします(速度は設定されますが、位置は決して統合されません)。すべての「ライブ」フレームが同一になり、被写体はスポーン時に固まってしまいます。これは実際のプロジェクトで深刻な問題でした。最初に確認すべき症状: 2回のポーリングで被写体の y をプローブし、それが決して変わらない場合、物理は停止しています。
その場合は、戦わないでください — 被写体を手動で操作します:
node ${CLAUDE_SKILL_DIR}/scripts/pose-capture.cjs <config.(cjs|json)> [outDir]
各ショットはワールドを一時停止し、あなたが定義したパスに沿って被写体をNフレームかけてアニメーションさせます(任意のトレイル/パーティクルエミッターを実行)。これにより、物理に関係なく実際のモーショントレイルが構築されます。ショットごとの設定: {name, setup, poseAt(t), steps, stepMs} — setup は1回実行され、シーンをショットの状態に設定します(例: 玉とゲートのゲームでは、ゲートをスポーン、state="playing"、world.pause()、trail.start()、準備ヒントを非表示、スコアを設定)。poseAt(t) は [0..1] の t に対して被写体を配置します。
ポーズされたトレイルを実際の動きとして認識させるには(例のゲームの被写体は重力下で落下します。自分のゲームの動きに合わせて調整してください):
- ベジェパス、直線的なlerpではない。 直線パスは硬く、定規で引いたようなトレイルになります。制御点を横にオフセットした2次ベジェは、ゲームが実際に生成する自然な曲線アークを与えます。ショットごとに曲線を変えてください。
- イーズイン
t*t(加速)は重力として読み取れます。 - ショットのみの適度なエミッターブースト(より高い
frequency+ より長いlifespan)。手動アニメーションは実際の高速落下よりも遅く、デフォルトのエミッターはパーティクルが少なすぎます。適度にブーストしてください — 過剰にジュースを注入したトレイルはゲームとはまったく似ていません。
フィーチャーグラフィック
python3 ${CLAUDE_SKILL_DIR}/scripts/feature_graphic.py <config.json>
構成: 暗い背景 → 実際のゲームスクリーンショット(左、ネオンフレーム) → タイトル + タグライン(右、バンドルフォント) → アクセントライン。superSample倍でレンダリングされ、ダウンスケールされます。
設定: out、size ([1024,500])、superSample (2)、screenshot、font、title、tagline、bg、frameColor、titleColor、titleGlow、tagColor、accentColor、titleWeight、tagWeight。
以下の例の値はある特定のゲームのものです。自分のタイトル、タグライン、色、フォントに置き換えてください。
{
"out": "store-assets/feature-graphic-1024x500.png",
"screenshot": "store-assets/feature-shot.png",
"font": "public/fonts/Orbitron.ttf",
"title": "MY GAME", "tagline": "ONE-TAP ARCADE",
"bg": "#0a0a1c", "frameColor": "#27e6ff",
"titleColor": "#ffffff", "titleGlow": "#27e6ff", "tagColor": "#8fffe0", "accentColor": "#27e6ff",
"titleWeight": 800, "tagWeight": 600
}
教訓(これらを実行すること)
- ヘッドレスでのWebGLにはSwiftShaderが必要です。
--use-angle=swiftshader --enable-unsafe-swiftshaderがないと、three.jsのキャンバスは真っ黒になります。速度は遅いです。一度に1ページだけ開いてください(5つの並列WebGLページはスクリーンショット呼び出しをタイムアウトさせました)。 - CSSでのHUD非表示: キャンバスの親を非表示にしないでください。 包括的な
div { visibility: hidden }はキャンバスも非表示にします(すべてのフレームが同一、約8KB)。DOMを一度プローブし、IDでキャンバスラッパーを除外してください。同一の小さなファイル = 何もレンダリングされていません。 - ポートレートのみのゲームとランドスケープバナー。 最大アスペクト制限はワイドビューポートをレターボックス化します(中央の狭い列)。
routesを使用してキャプチャごとに解除します。ゲームファイルは…
[以下、元のSKILL.mdの続きが翻訳されますが、これは完全な翻訳ではありません。原文が切り詰められているため、この翻訳もその時点で終了しています。]