Activos de ficha de tienda desde el juego en ejecución
Produce imágenes fieles a la tienda capturando el juego real en ejecución (sin maquetas).
El andamiaje reutilizable está en scripts/; tú proporcionas una configuración pequeña y específica del juego
(URL, comprobación de listo, cómo posar/ocultar/conducir el juego, texto del título).
Tres entregables:
- Capturas de pantalla (
scripts/screenshots.cjs) — lotes en los tamaños exactos de la tienda. - Feature graphic — banner promocional de 1024x500: ya sea un fotograma del juego a sangre y fondo limpio (
screenshots.cjscon untargetapaisado) o una composición de fotograma + título (scripts/feature_graphic.py). Pregunta cuál; por defecto usa el de sangre y fondo para juegos ambientales. - Texto de la ficha — descripción breve + completa: lee
references/listing-text.md(límites, tono, qué no afirmar, la pregunta del formulario "Activos generados por IA").
Para los requisitos de tamaño de la tienda y ajustes predefinidos, lee references/play-store-sizes.md.
Flujo de trabajo: muchos candidatos → el usuario elige
El usuario elige; tú ofreces variedad y facilitas la elección.
- Captura muchos fotogramas por fase/nivel/temática (p. ej., 5 fases × 8–10). Usa también la progresión del juego para variarlos (etapas de mejora temprana / intermedia / tardía): los fotogramas tempranos se ven vacíos, los tardíos pueden ocultar el sujeto.
- Preselecciona ~15 con variedad (cada fase; momentos de pase/ráfaga; fondos ricos vs. tranquilos).
Míralos tú mismo primero con
scripts/contact_sheet.py(numerados). Luego deja que el usuario elija — en la página de selección (abajo) cuando los Artefactos estén disponibles, que es lo que los usuarios pidieron una vez que la vieron; de lo contrario, envía la imagen de la hoja numerada y toma los números. - Recomienda un conjunto, con una razón (variedad de color / fase entre el conjunto, sin repetir el mismo momento). Vuelve a capturar lo que pidan los comentarios del usuario ("被写体をもう少し中央に") y añade las retomas al selector como otro grupo que comparte la misma clave de selección.
- Copia el conjunto final en una carpeta en orden de subida (
1-…png,2-…), y registra la decisión (archivos, texto elegido textualmente, opciones rechazadas y por qué, cómo se capturaron) en la documentación del proyecto. Mantén las imágenes generadas fuera de git a menos que el proyecto ya rastree los activos de la tienda.
python3 ${CLAUDE_SKILL_DIR}/scripts/contact_sheet.py sheet.png a.png b.png … --cols 5 --thumb 300
(La técnica base de Playwright/canvas se comparte con game-icon-from-screenshot).
Página de selección (Artefacto): el usuario marca el conjunto
python3 ${CLAUDE_SKILL_DIR}/scripts/build_picker.py picker.json picker.html, luego publica
picker.html con la herramienta Artifact y capabilities: {"db": {}} (carga primero las habilidades
artifact-design / artifact-capabilities, ya que la herramienta Artifact lo requiere). El formato
de la especificación está en el encabezado de scripts/build_picker.py.
Qué hace la página (todo desde la especificación, nada específico del juego en la plantilla):
- Grupos de imágenes —
mode: "multi"(capturas de pantalla: orden de marca = orden de subida, ← → para reordenar,max) o"single"(feature graphic, icono de aplicación). Varios grupos pueden compartir una clavepick(primeros candidatos + retomas = una elección). Chips "おすすめ" derec, ⤢ zoom, una bandeja en la parte superior mostrando el conjunto elegido, y un botón "おすすめで埋める". - Grupos de texto — opciones de radio con el texto de envío, una traducción debajo,
un contador de caracteres contra
limit, y un botón de copiar en la esquina superior derecha de cada opción (copia solo el texto de envío). - Comentarios como instrucciones — un cuadro de comentarios por grupo (
noteKey) más uno general "全体への指示". Los usuarios escriben instrucciones allí ("2 案目の最後の行はいらない", un borrador en japonés para editar y traducir) y dicen "保存しました". - choices — radios pequeños de sí/no (texto del título en la feature graphic o no).
- Todo se guarda en un documento de db (
doc, por defectopicks/current); léelo conArtifactData(action: "get", collection: "picks", doc_id: "current"para el valor por defecto). Las claves de selección / elección / nota comparten ese documento, por lo que el constructor rechaza claves en conflicto e identificadores de ítems duplicados cada vez que el usuario dice que guardó. Actúa sobre cada nota no vacía, luego diles qué cambió. Para cambiar la página, edita la especificación y reconstruye, luego publica la misma ruta de archivo (misma URL; las selecciones guardadas sobreviven porque los ids siguen siendo los mismos). Después de la primera publicación, haz una lectura depickspara confirmar las respuestas de la tienda.
Los iconos de aplicación usan la misma página: un selector solo de iconos (un grupo "single",
aspect: "1/1", thumbWidth: 512) o iconos como un grupo más junto a las imágenes de la tienda.
Crea los candidatos con game-icon-from-screenshot.
Prerrequisitos
- Juego ejecutándose en una URL local (
npm run dev→ p. ej.,http://localhost:5173). - Playwright + Chromium (resuelto automáticamente desde
node_modulesdel proyecto, el caché de npx o global; si no,npx playwright install chromium). - Pillow (
python3 -m pip install Pillow) para redimensionar/componer. - Un archivo de fuente empaquetado para el título de la feature graphic (un
.ttf; las fuentes variables son compatibles mediante ejes de peso).
Anti-aliasing: supersample, no redimensionar
Capturar al tamaño de salida exacto se ve dentado. Captura denso y luego reduce:
configura superSample (deviceScaleFactor) = 2 y deja que el andamiaje reduzca al
tamaño objetivo. IMPORTANTE: haz la densidad con superSample, no usando un viewport mucho más grande
— un canvas muy grande puede bloquear el bucle de renderizado/física headless (el juego se congela al aparecer).
Mantén el viewport en un tamaño donde el juego realmente funcione (p. ej., 1080x1920) y aumenta superSample en su lugar.
Capturas de pantalla
node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs <config.(cjs|json)> [outDir]
- Tomas posadas estáticas (
shots): cada una abre una página nueva, ejecuta tu funciónpose(establece la puntuación, posiciona el sujeto, muestra la superposición de resultados…), opcionalmente oculta el HUD, y captura. Bueno para pantallas de listo/resultado. - Juego en vivo (
live): inicia el juego, ejecuta tu funciónkeepAliveen cada sondeo para mantener el juego vivo/conduciéndolo (un "piloto automático"), y captura fotogramas cuando tu funciónpickdice que el fotograma es bueno. Los fotogramas en vivo muestran movimiento/estelas reales — así es como obtienes tomas "realmente jugando", no estáticas. Captura muchos y deja que el usuario elija.
Los campos fn se ejecutan DENTRO DE LA PÁGINA. En una configuración .cjs escribe funciones reales
(se serializan con .toString(), así que mantenlas autocontenidas — sin closures de Node).
En una configuración .json escribe cadenas fuente de funciones flecha.
- Temporizado (
timed): para juegos que pueden jugarse solos (una consulta?autoplay=, un modo demo/atracción). Abre cada URL por turno y dispara cadaintervalMs— la forma más fácil de obtener muchos fotogramas reales por fase.
Configuración (capturas de pantalla)
url, ready, target {width,height} (tamaño de salida + viewport), viewport
(tamaño de captura cuando deba diferir; los fotogramas se recortan a la relación de aspecto objetivo,
cropY ancla la banda), superSample (2), launchArgs, executablePath,
css (inyectado; la forma más simple de ocultar HUD para HUDs DOM), routes [{match, replace: [[from,to]…]}] (parches solo de captura del código servido), 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 documentación completa de los campos está en el encabezado de scripts/screenshots.cjs.
Playwright debe ser resoluble: ejecuta desde un directorio que lo tenga en node_modules, o
NODE_PATH=<dir>/node_modules node ${CLAUDE_SKILL_DIR}/scripts/screenshots.cjs … (CommonJS). Si
el navegador instalado es más antiguo que el paquete, el script recurre al Chromium más reciente en el caché ms-playwright.
Ejemplo (WebGL / three.js, auto-reproducción, 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
}
}
Banner de 1024x500 a sangre y fondo para un juego solo vertical (convierte en buzón los viewports anchos, y su mundo termina más allá de la vista vertical): levanta el límite de relación de aspecto solo para la captura, captura un viewport más cuadrado a 2x, recorta la banda.
{
"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}
}
Ejemplo (Phaser, vertical; dev expone window.__game)
Un ejemplo resuelto, de un juego específico (un juego de pasar una pelota por un aro). Sustituye tus propios nombres de escena/objeto, lógica de pose y condiciones de conducción/selección.
// phone.shots.cjs
const G = () => window.__game.scene.scenes[0]; // ayuda en página (en línea abajo)
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, // toque de confianza para comenzar (Phaser ignora eventos sintéticos)
hideHud: false, // mantener la puntuación visible en las capturas de juego
start: () => { window.__game.scene.scenes[0].goals = 6; }, // configuración extra (p. ej., habilitar variedad)
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; },
},
};
Conjunto para tableta: la misma configuración con target: { width: 1440, height: 2560 }. (También puedes
redimensionar las tomas de teléfono elegidas a 1440x2560 para mantener los fotogramas exactos elegidos.)
Animación posada (scripts/pose-capture.cjs) — para juegos con postFX/bloom
El modo live asume que la física del juego se ejecuta en headless. A menudo no es así.
Una canalización WebGL bloom / postFX puede BLOQUEAR el bucle de renderizado+física headless, por lo que
el sujeto se congela en su aparición (la velocidad se establece pero la posición nunca se integra) —
cada fotograma "en vivo" sale idéntico, el sujeto atascado en su aparición.
Esto afectó mucho en un proyecto real. Síntoma a comprobar primero: sondea la y del sujeto
en dos sondeos; si nunca cambia, la física está bloqueada.
Cuando eso suceda, no luches contra ello — conduce el sujeto manualmente:
node ${CLAUDE_SKILL_DIR}/scripts/pose-capture.cjs <config.(cjs|json)> [outDir]
Cada toma pausa el mundo, luego anima el sujeto a lo largo de una ruta QUE TÚ DEFINES
sobre N fotogramas (con cualquier estela/emisor de partículas en ejecución), de modo que se construya
una estela de movimiento real independientemente de la física. Configuración por toma:
{name, setup, poseAt(t), steps, stepMs} — setup se ejecuta una vez y pone la escena en el estado de
la toma (p. ej., en un juego de pelota y compuerta: genera la compuerta, state="playing", world.pause(),
trail.start(), oculta la pista de listo, establece una puntuación), poseAt(t)
posiciona el sujeto para t en [0..1].
Qué hace que una estela posada se lea como movimiento real (el sujeto del juego de ejemplo cae bajo gravedad; adáptalo a cómo se mueve el tuyo):
- Trayectoria Bezier, no una interpolación lineal. Una trayectoria lineal produce una estela rígida, recta como una regla; una bezier cuadrática con un desplazamiento lateral del punto de control da el arco curvo natural que el juego realmente produce. Varía la curva por toma.
- Facilidad de entrada
t*t(acelerando) se lee como gravedad. - Aumento moderado del emisor solo para la toma (mayor
frequency+ mayorlifespan), porque la animación manual es más lenta que una caída rápida real y el emisor por defecto deja muy pocas partículas. Aumenta MODERADAMENTE — una estela demasiado intensa no se parece en nada al juego.
Feature graphic
python3 ${CLAUDE_SKILL_DIR}/scripts/feature_graphic.py <config.json>
Compone: fondo oscuro → captura de pantalla real del juego (izquierda, marco neón) → título +
eslogan (derecha, fuente incluida) → línea de acento. Renderizado a superSamplex y luego
reducido.
Configuración: out, size ([1024,500]), superSample (2), screenshot, font,
title, tagline, bg, frameColor, titleColor, titleGlow, tagColor,
accentColor, titleWeight, tagWeight.
Los valores de ejemplo a continuación son de un juego específico — sustituye tu propio título, eslogan, colores y fuente.
{
"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
}
Lecciones (haz esto)
- WebGL en headless necesita SwiftShader. Sin
--use-angle=swiftshader --enable-unsafe-swiftshaderun canvas de three.js es negro. Es lento: abre una página a la vez (5 páginas WebGL paralelas agotaron el tiempo de espera de la llamada de captura de pantalla). - Ocultar el HUD con CSS: no ocultes el padre del canvas. Un
div { visibility: hidden }genérico también ocultó el canvas (cada fotograma idéntico, ~8 KB). Sonda el DOM una vez y excluye el contenedor del canvas por ID. Archivos idénticos y diminutos = nada renderizado. - Juegos solo verticales y banners apaisados. Un límite de relación de aspecto máxima convierte en buzón
un viewport ancho (una columna estrecha en el medio). Levántalo por captura mediante
routes; los archivos del juego s...
...y evita el uso de componentes físicos en modo headless si no es necesario.