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 :
- Captures d'écran (
scripts/screenshots.cjs) — lots aux tailles exactes du store. - Feature graphic — bannière promo 1024x500 : soit un cadre de jeu plein écran propre
(
screenshots.cjsavec unetargetpaysage) 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. - 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.
- 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.
- 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. - 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.
- 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 "おすすめ" derec, ⤢ 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éfautpicks/current) ; lisez-le avecArtifactData(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 depickspour 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_modulesdu projet, le cache npx, ou global ; sinonnpx 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 fonctionpose(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 fonctionkeepAliveà chaque interrogation pour garder le jeu en vie/pilotage (un "pilote automatique"), et capture les images lorsque votre fonctionpickdit 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 lesintervalMs— 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
frequencyplus élevée + durée de vielifespanplus 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...