실행 중인 게임으로부터 스토어 에셋 제작
실제 실행 중인 게임을 캡처하여 정확한 스토어 이미지를 생성합니다(목업이 아닙니다). 재사용 가능한 도구는 scripts/ 디렉터리에 있습니다. 게임별로 작은 설정(URL, 준비 완료 확인, 게임 조작/숨김/구동 방법, 제목 텍스트)만 제공하면 됩니다.
세 가지 결과물:
- 스크린샷 (
scripts/screenshots.cjs) — 정확한 스토어 크기로 배치 캡처 - 피처 그래픽 — 1024x500 프로모션 배너: 깔끔한 전체 채움 게임 프레임(
screenshots.cjs에서 가로 방향target사용) 또는 프레임+제목 합성(scripts/feature_graphic.py사용). 분위기 중심 게임은 기본적으로 전체 채움을 권장합니다. - 스토어 설명문 — 짧은 설명 + 전체 설명:
references/listing-text.md참조(제한 사항, 어조, 주장하지 말아야 할 내용, "AI 생성 에셋" 양식 질문)
스토어 크기 요구사항과 프리셋은 references/play-store-sizes.md를 참조하세요.
워크플로우: 여러 후보 → 사용자 선택
사용자가 선택합니다. 다양성을 제공하고 선택을 쉽게 만듭니다.
- 스테이지/레벨/테마별로 많은 프레임을 캡처합니다(예: 5개 스테이지 × 8~10장). 게임 진행도에 따라 다양화하세요(초기/중기/후기 업그레이드 단계: 초기 프레임은 비어 보이고 후기 프레임은 주제를 묻힐 수 있습니다).
- 다양성을 갖춘 ~15장을 선별합니다(각 스테이지, 통과/버스트 순간, 배경이 풍부한/조용한 장면). 먼저
scripts/contact_sheet.py(번호 매김)로 직접 확인하세요. 그런 다음 Artifact가 사용 가능할 때는 선택 페이지(아래)에서 사용자가 선택하도록 합니다(사용자들이 이를 보고 요청한 방식). Artifact를 사용할 수 없으면 번호가 매겨진 접촉 시트 이미지를 보내고 번호를 입력받습니다. - 세트를 추천하고 이유를 함께 제시합니다(색상/스테이지 다양성, 동일 순간 반복 금지). 사용자가 요청한 대로 다시 촬영하고(예: "피사체를 조금 더 중앙으로"), 재촬영본을 동일한 선택 키를 공유하는 별도 그룹으로 선택 페이지에 추가합니다.
- 최종 세트를 업로드 순서대로 하나의 폴더에 복사합니다(
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"(피처 그래픽, 앱 아이콘). 여러 그룹이 하나의pick키를 공유할 수 있음(첫 번째 후보 + 재촬영본 = 하나의 선택).rec에서 "추천" 칩, ⤢ 확대, 상단에 선택된 세트를 보여주는 트레이, "추천으로 채우기" 버튼. - 텍스트 그룹 — 제출 텍스트가 있는 라디오 옵션, 그 아래 번역,
limit에 대한 문자 수 카운터, 각 옵션 오른쪽 상단의 복사 버튼(제출 텍스트만 복사). - 의견을 지시사항으로 — 그룹별 의견 상자(
noteKey) + 전체 "전체에 대한 지시" 상자. 사용자가 여기에 지시사항을 작성하고("2안의 마지막 줄은 필요 없음", 편집 및 번역할 일본어 초안) "저장했습니다"라고 말합니다. - 선택 — 작은 예/아니오 라디오(피처 그래픽에 제목 텍스트 포함 여부 등).
- 모든 내용은 하나의 db 문서(
doc, 기본값picks/current)에 저장됩니다.ArtifactData(action: "get", collection: "picks", doc_id: "current"기본값)로 읽습니다. 선택/선택 사항/의견 키는 해당 문서를 공유하므로, 빌더는 충돌하는 키와 중복 항목 ID를 거부합니다. 사용자가 저장했다고 말할 때마다 작업합니다. 비어 있지 않은 모든 의견에 대해 작업한 후 변경 사항을 알려줍니다. 페이지를 변경하려면 명세를 편집하고 다시 빌드한 후 동일한 파일 경로로 게시합니다(동일 URL; 저장된 선택은 ID가 동일하므로 유지됨). 첫 번째 게시 후picks를 한 번 읽어 스토어 답변을 확인합니다.
앱 아이콘도 동일한 페이지를 사용합니다: 아이콘 전용 선택기(하나의 "single" 그룹, aspect: "1/1", thumbWidth: 512) 또는 아이콘을 스토어 이미지 옆에 추가 그룹으로 배치합니다. 후보는 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; 가변 폰트는 weight 축 지원).
안티앨리어싱: 슈퍼샘플링, 업스케일 금지
정확한 출력 크기로 캡처하면 계단 현상이 발생합니다. 고밀도로 캡처 후 다운스케일: superSample(deviceScaleFactor) = 2로 설정하고 도구가 대상 크기로 다운스케일하도록 합니다. 중요: superSample으로 밀도를 높이고 훨씬 더 큰 뷰포트를 사용하지 마세요 — 매우 큰 캔버스는 headless 렌더/물리 루프를 멈출 수 있습니다(게임이 스폰 시 얼어버림). 게임이 실제로 플레이되는 크기(예: 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용 가장 간단한 HUD 숨김), routes [{match, replace: [[from,to]…]}](캡처 전용 서빙 소스 패치), waitFontsReady (true), hudHide (함수), 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에 Playwright가 있는 디렉터리에서 실행하거나 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 배너(가로 뷰포트는 레터박스 처리되고, 게임 월드는 세로 뷰 너머로 끝남): 캡처 시에만 종횡비 제한을 해제하고, 더 정사각형에 가까운 뷰를 2x로 캡처한 후 밴드를 자릅니다.
{
"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) — 후처리 효과/블룸 게임용
live 모드는 게임의 물리가 headless에서 실행된다고 가정합니다. 하지만 종종 그렇지 않습니다. WebGL 블룸/후처리 파이프라인은 headless 렌더+물리 루프를 멈출 수 있어 피사체가 스폰 시 얼어버립니다(속도는 설정되지만 위치가 절대 통합되지 않음) — 모든 "라이브" 프레임이 동일하게 나오고 피사체는 스폰 지점에 고정됩니다. 실제 프로젝트에서 이것이 큰 문제가 되었습니다. 먼저 확인할 증상: 두 번의 폴링에서 피사체의 y 값을 확인합니다. 절대 변하지 않으면 물리가 멈춘 것입니다.
이런 경우, 싸우지 말고 수동으로 피사체를 구동하세요:
node ${CLAUDE_SKILL_DIR}/scripts/pose-capture.cjs <config.(cjs|json)> [outDir]
각 샷은 월드를 일시 중지한 후, N 프레임에 걸쳐 사용자가 정의한 경로를 따라 피사체를 애니메이션합니다(모든 트레일/파티클 이미터 실행). 따라서 물리와 관계없이 실제 모션 트레일이 생성됩니다. 샷별 설정: {name, setup, poseAt(t), steps, stepMs} — setup은 한 번 실행되며 씬을 샷의 상태로 만듭니다(예: 공과 게이트 게임에서 게이트 스폰, state="playing", world.pause(), trail.start(), 준비 힌트 숨기기, 점수 설정). poseAt(t)는 [0..1] 범위의 t에 대해 피사체를 배치합니다.
포즈 트레일이 실제 모션으로 읽히게 하는 요소(예제 게임의 피사체는 중력 아래로 떨어짐; 게임에 맞게 조정):
- 베지어 경로, 직선 보간이 아님. 직선 경로는 뻣뻣하고 자처럼 곧은 트레일을 만듭니다. 측면 제어점 오프셋이 있는 2차 베지어는 게임이 실제로 생성하는 자연스러운 곡선 호를 제공합니다. 샷마다 곡선을 변경하세요.
t*t로 ease-in (가속)은 중력처럼 읽힙니다.- 적당한 이미터 부스트 샷에만 적용(높은
frequency+ 긴lifespan). 수동 애니메이션은 실제 빠른 낙하보다 느리기 때문에 기본 이미터는 파티클이 너무 적게 남습니다. 적당히 부스트하세요 — 과도하게 주입된 트레일은 게임과 전혀 다르게 보입니다.
피처 그래픽
python3 ${CLAUDE_SKILL_DIR}/scripts/feature_graphic.py <config.json>
구성: 어두운 배경 → 실제 게임 스크린샷 (왼쪽, 네온 프레임) → 제목 + 태그라인 (오른쪽, 번들 폰트) → 악센트 라인. superSamplex로 렌더링 후 다운스케일.
설정: 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
}
교훈 (이것들을 따르세요)
- Headless에서 WebGL은 SwiftShader가 필요합니다.
--use-angle=swiftshader --enable-unsafe-swiftshader없이 three.js 캔버스는 검은색입니다. 느리므로 한 번에 한 페이지만 열어 처리하세요(5개의 병렬 WebGL 페이지는 스크린샷 호출에서 타임아웃되었습니다). - CSS로 HUD 숨기기: 캔버스의 부모를 숨기지 마세요. 무분별한
div { visibility: hidden }은 캔버스도 숨깁니다(모든 프레임이 동일해지고 ~8 KB). DOM을 한 번 조사하고 ID로 캔버스 래퍼를 제외하세요. 동일한 작은 파일 = 아무것도 렌더링되지 않음. - 세로 모드 전용 게임과 가로 배너. 최대 종횡비 제한은 넓은 뷰포트를 레터박스 처리합니다(중간에 좁은 컬럼).
routes를 통해 캡처당 제한을 해제하세요.
(이하 원본 SKILL.md의 Lessons와 References 부분은 번역 대상에서 제외하거나 필요에 따라 추가 번역 가능. 원문이 길어 일부 생략되었으나, 필요시 전체를 번역할 수 있습니다.)