Communitygithub.com

takumifukasawa/agent-skills

Génère la fiche Google Play d'un jeu : le FEATURE GRAPHIC promo (1024x500) et les CAPTURES D'ÉCRAN du jeu réel tournant dans un Chromium headless, ainsi que le TEXTE de la fiche (description courte/ longue).

Qu'est-ce que agent-skills ?

Cette compétence permet de produire des assets complets pour le Google Play Store à partir d'un jeu en cours d'exécution locale. Elle capture des captures d'écran aux dimensions exactes du store, crée un feature graphic (bannière 1024x500) et génère les textes de description (court et long). Elle prend en charge les jeux utilisant Phaser, PixiJS, Three.js ou un canvas simple. Le flux de travail propose une page de sélection (Picker Artifact) qui permet au développeur de choisir visuellement les captures, le feature graphic, l'icône et les textes, avec des cases à cocher et des boutons de copie. Une attention particulière est portée à la gestion de l'anti-aliasing (sur-échantillonnage puis réduction), à la capture de gameplay réel en mouvement (via des séquences animées ou des 'poses' pilotées) et à l'évitement des problèmes de rendu WebGL en mode headless (SwiftShader).

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/takumifukasawa/agent-skills/tree/HEAD/gamedev/game-store-assets

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Assets du store à partir du jeu en cours

Produisez des images fidèles au store en capturant le vrai jeu en cours (pas des maquettes). Le harnais réutilisable se trouve dans scripts/ ; vous fournissez une petite configuration spécifique au jeu (URL, vérification de prêt, comment poser/cacher/piloter le jeu, texte du titre).

Trois livrables :

  1. Captures d'écran (scripts/screenshots.cjs) — lots aux tailles exactes du store.
  2. Feature graphic — bannière promo 1024x500 : soit un cadre de jeu plein écran propre (screenshots.cjs avec une target paysage) soit un cadre + composition de titre (scripts/feature_graphic.py). Demandez lequel ; par défaut, le plein écran pour les jeux basés sur l'ambiance.
  3. Texte de la fiche — description courte + longue : lisez references/listing-text.md (limites, ton, ce qu'il ne faut pas prétendre, la question du formulaire "Assets générés par IA").

Pour les exigences de tailles du store + préréglages, lisez references/play-store-sizes.md.

Flux de travail : plusieurs candidats → l'utilisateur choisit

L'utilisateur choisit ; vous fournissez de la variété et facilitez le choix.

  1. Capturez de nombreuses images par niveau/stade/thème (par ex. 5 stades × 8–10). Utilisez la progression du jeu pour les varier aussi (premiers / intermédiaires / derniers stades d'amélioration) : les premières images paraissent vides, les dernières peuvent enterrer le sujet.
  2. Présélectionnez ~15 variées (chaque stade ; moments de passage/exploit ; arrière-plans riches vs calmes). Regardez-les d'abord vous-même avec scripts/contact_sheet.py (numérotées). Ensuite, laissez l'utilisateur choisir — dans la page de sélection (ci-dessous) lorsque les Artifacts sont disponibles, ce que les utilisateurs ont demandé dès qu'ils l'ont vue ; sinon, envoyez l'image numérotée et prenez les numéros.
  3. Recommandez un ensemble, avec une raison (variété de couleurs / de stades dans l'ensemble, sans répéter le même moment). Re-shootez ce que les commentaires de l'utilisateur demandent ("被写体をもう少し中央に") et ajoutez les reprises dans le sélecteur comme un autre groupe partageant la même clé de sélection.
  4. Copiez l'ensemble final dans un dossier dans l'ordre de téléchargement (1-…png, 2-…), et enregistrez la décision (fichiers, texte choisi textuel, options rejetées et pourquoi, comment elles ont été capturées) dans les docs du projet. Gardez les images générées hors de git sauf si le projet suit déjà les assets du store.

python3 ${CLAUDE_SKILL_DIR}/scripts/contact_sheet.py sheet.png a.png b.png … --cols 5 --thumb 300 (La technique de base Playwright/canvas est partagée avec game-icon-from-screenshot.)

Page de sélection (Artifact) : l'utilisateur coche l'ensemble

python3 ${CLAUDE_SKILL_DIR}/scripts/build_picker.py picker.json picker.html, puis publiez picker.html avec l'outil Artifact et capabilities: {"db": {}} (chargez d'abord les compétences artifact-design / artifact-capabilities, car l'outil Artifact l'exige). Le format de spécification est dans l'en-tête de scripts/build_picker.py.

Ce que fait la page (tout à partir de la spécification, rien de spécifique au jeu dans le template) :

  • Groupes d'images — mode: "multi" (captures : ordre de coche = ordre de téléchargement, ← → pour réordonner, max) ou "single" (feature graphic, icône d'application). Plusieurs groupes peuvent partager une même clé pick (premiers candidats + reprises = un choix). Chips "おすすめ" de rec, ⤢ zoom, un plateau en haut montrant l'ensemble choisi, et un bouton "おすすめで埋める".
  • Groupes de texte — options radio avec le texte de soumission, une traduction en dessous, un compteur de caractères par rapport à limit, et un bouton de copie en haut à droite de chaque option (copie uniquement le texte de soumission).
  • Commentaires comme instructions — une zone de commentaire par groupe (noteKey) plus une globale "全体への指示". Les utilisateurs y écrivent des instructions ("2 案目の最後の行は いらない", un brouillon japonais à éditer et traduire) et disent "保存しました".
  • Choix — petits radios oui/non (texte du titre sur le feature graphic ou non).
  • Tout est sauvegardé dans un seul document db (doc, par défaut picks/current) ; lisez-le avec ArtifactData (action: "get", collection: "picks", doc_id: "current" pour le défaut). Les clés de sélection / choix / notes partagent ce document, donc le constructeur refuse les clés en conflit et les IDs d'éléments en double chaque fois que l'utilisateur dit avoir sauvegardé. Agissez sur chaque note non vide, puis dites-leur ce qui a changé. Pour modifier la page, éditez la spécification et reconstruisez, puis publiez le même chemin de fichier (même URL ; les sélections sauvegardées survivent car les IDs restent les mêmes). Après la première publication, faites une lecture de picks pour confirmer les réponses du store.

Les icônes d'application utilisent la même page : un sélecteur d'icônes uniquement (un groupe "single", aspect: "1/1", thumbWidth: 512) ou des icônes comme un groupe supplémentaire à côté des images du store. Créez les candidats avec game-icon-from-screenshot.

Prérequis

  • Jeu tournant à une URL locale (npm run dev → par ex. http://localhost:5173).
  • Playwright + Chromium (résolu automatiquement depuis les node_modules du projet, le cache npx, ou global ; sinon npx playwright install chromium).
  • Pillow (python3 -m pip install Pillow) pour le redimensionnement/la composition.
  • Un fichier de police emballé pour le titre du feature graphic (un .ttf ; polices variables supportées via les axes de poids).

Anti-aliasing : sur-échantillonner, ne pas agrandir

Capturer à la taille de sortie exacte donne un rendu crénelé. Capturez dense puis réduisez : définissez superSample (deviceScaleFactor) = 2 et laissez le harnais réduire à la taille cible. IMPORTANT : faites la densité avec superSample, pas en utilisant un viewport beaucoup plus grand — un canvas très grand peut bloquer la boucle de rendu/physique headless (le jeu se fige au spawn). Gardez le viewport à une taille où le jeu fonctionne réellement (par ex. 1080x1920) et augmentez superSample à la place.

Captures d'écran

node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs <config.(cjs|json)> [outDir]

  • Plans posés statiques (shots) : chacun ouvre une nouvelle page, exécute votre fonction pose (définit le score, positionne le sujet, affiche la superposition de résultat…), cache optionnellement le HUD, et capture. Bon pour les écrans de prêt/résultat.
  • Gameplay en direct (live) : commence le jeu, exécute votre fonction keepAlive à chaque interrogation pour garder le jeu en vie/pilotage (un "pilote automatique"), et capture les images lorsque votre fonction pick dit que l'image est bonne. Les images en direct montrent le mouvement réel/les traînées — c'est ainsi que vous obtenez des plans "réellement en jeu", pas des plans statiques. Capturez beaucoup et laissez l'utilisateur choisir.

Les champs fn s'exécutent DANS LA PAGE. Dans une config .cjs, écrivez de vraies fonctions (elles sont sérialisées avec .toString(), donc gardez-les autonomes — pas de fermetures Node). Dans une config .json, écrivez des chaînes source de fonctions fléchées.

  • Temporel (timed) : pour les jeux qui peuvent jouer tout seuls (une requête ?autoplay=, un mode démo/attraction). Ouvre chaque URL à son tour et capture toutes les intervalMs — le moyen le plus simple d'obtenir de nombreuses images réelles par niveau.

Configuration (captures d'écran)

url, ready, target {width,height} (taille de sortie + viewport), viewport (taille de capture lorsqu'elle doit différer ; les images sont recadrées au format de la cible, cropY ancre la bande), superSample (2), launchArgs, executablePath, css (injecté ; le moyen le plus simple de cacher le HUD pour les HUD DOM), routes [{match, replace: [[from,to]…]}] (correctifs de capture uniquement de la source servie), 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. La documentation complète des champs se trouve dans l'en-tête de scripts/screenshots.cjs.

Playwright doit être résoluble : exécutez depuis un répertoire avec lui dans node_modules, ou NODE_PATH=<dir>/node_modules node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs … (CommonJS). Si le navigateur installé est plus ancien que le package, le script se rabat sur le Chromium le plus récent dans le cache ms-playwright.

Exemple (WebGL / three.js, auto-jouable, HUD DOM)

{
  "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
  }
}

Bannière plein écran 1024x500 d'un jeu portrait uniquement (il letterboxe les viewports larges, et son monde se termine au-delà de la vue portrait) : retirez la limite de format pour la capture uniquement, capturez une vue plus carrée en 2x, recadrez la bande.

{
  "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}
}

Exemple (Phaser, portrait ; dev expose window.__game)

Un exemple travaillé, à partir d'un jeu spécifique (un jeu de balle à travers un anneau). Remplacez vos propres noms de scène/objet, logique de pose et conditions de pilotage/sélection.

// phone.shots.cjs
const G = () => window.__game.scene.scenes[0]; // aide dans la page (intégré ci-dessous)
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,            // appui de confiance pour commencer (Phaser ignore les événements synthétiques)
    hideHud: false,            // garder le score visible dans les captures de jeu
    start: () => { window.__game.scene.scenes[0].goals = 6; }, // configuration supplémentaire (par ex. activer la variété)
    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; },
  },
};

Ensemble tablette : même configuration avec target: { width: 1440, height: 2560 }. (Vous pouvez aussi upscaler les captures de téléphone choisies à 1440x2560 pour garder les images exactes choisies.)

Animation posée (scripts/pose-capture.cjs) — pour les jeux avec postFX/bloom

Le mode live suppose que la physique du jeu s'exécute en headless. Souvent, ce n'est pas le cas. Une pipeline WebGL bloom / postFX peut BLOQUER la boucle de rendu+physique headless, donc le sujet se fige à son apparition (la vitesse est définie mais la position n'est jamais intégrée) — chaque image "live" sort identique, le sujet bloqué à son spawn. Cela a été un problème sur un vrai projet. Symptôme à vérifier en premier : interrogez la position y du sujet sur deux points ; si elle ne change jamais, la physique est bloquée.

Lorsque cela se produit, ne luttez pas — pilotez le sujet manuellement :

node ${CLAUDE_SKILL_DIR}/scripts/pose-capture.cjs <config.(cjs|json)> [outDir]

Chaque plan met le monde en pause, puis anime le sujet le long d'un chemin QUE VOUS définissez sur N images (avec toute traînée / émetteur de particules en cours), de sorte qu'une traînée de mouvement réelle se construit indépendamment de la physique. Configuration par plan : {name, setup, poseAt(t), steps, stepMs} — setup s'exécute une fois et met la scène dans l'état du plan (par ex. dans un jeu balle-et-portail : faire apparaître le portail, state="playing", world.pause(), trail.start(), cacher l'indice de prêt, définir un score), poseAt(t) positionne le sujet pour t dans [0..1].

Ce qui fait qu'une traînée posée est lue comme un mouvement réel (le sujet du jeu d'exemple tombe sous la gravité ; adaptez à la façon dont votre sujet se déplace) :

  • Chemin de Bézier, pas une interpolation linéaire. Un chemin droit donne une traînée rigide et droite comme une règle ; une bézier quadratique avec un décalage latéral du point de contrôle donne l'arc courbe naturel que le jeu produit réellement. Variez la courbe par plan.
  • Ease-in t*t (accélération) se lit comme la gravité.
  • Boost modéré de l'émetteur pour le plan uniquement (fréquence frequency plus élevée + durée de vie lifespan plus longue), car l'animation manuelle est plus lente qu'une chute rapide réelle et l'émetteur par défaut laisse trop peu de particules. Boostez MODÉRÉMENT — une traînée trop chargée ne ressemble plus au jeu.

Feature graphic

python3 ${CLAUDE_SKILL_DIR}/scripts/feature_graphic.py <config.json>

Compose : fond sombre → capture d'écran réelle du jeu (à gauche, cadre néon) → titre + slogan (à droite, police emballée) → ligne d'accent. Rendu à superSamplex puis réduit.

Configuration : out, size ([1024,500]), superSample (2), screenshot, font, title, tagline, bg, frameColor, titleColor, titleGlow, tagColor, accentColor, titleWeight, tagWeight.

Les exemples de valeurs ci-dessous proviennent d'un jeu spécifique — remplacez par votre propre titre, slogan, couleurs et police.

{
  "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
}

Leçons (faites ceci)

  • Le WebGL en headless a besoin de SwiftShader. Sans --use-angle=swiftshader --enable-unsafe-swiftshader, un canvas three.js est noir. C'est lent : ouvrez une page à la fois (5 pages WebGL parallèles ont mis en timeout l'appel de capture d'écran).
  • Cacher le HUD avec CSS : ne cachez pas le parent du canvas. Un div { visibility: hidden } général a aussi caché le canvas (chaque image identique, ~8 Ko). Sondez le DOM une fois et excluez le wrapper du canvas par ID. Des fichiers identiques minuscules = rien rendu.
  • Jeux portrait uniquement et bannières paysage. Une limite de format max letterboxe un viewport large (une colonne étroite au milieu). Retirez-la par capture via routes ; les fichiers du jeu...

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills associés