К основному содержанию
скилл CX Codex

promo-video — скилл для Claude Code: промо-ролик игры на Phaser

Записывает автономный промо-ролик игры на Phaser в MP4 50 FPS, вертикально 9:16. Пригодится, когда нужен клип для соцсетей без ручной съёмки.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add PlayableIntelligence/game-creator --skill promo-video -a claude-code
Открыть на GitHub

Что делает promo-video

Скилл открывает игру в браузере через Playwright и сам играет за игрока: жмёт клавиши, делает паузы, варьирует тайминги, чтобы движение не выглядело роботом. Смерть и проигрыш на время записи отключаются — ролик показывает непрерывный геймплей. На выходе MP4 в мобильной вертикали 1080×1920.

Загвоздка в том, что recordVideo в Playwright упирается в 25 FPS и настройки для этого нет. Обход такой: игра замедляется в 0.5× через пять подсистем Phaser (дельта обновления, твины, таймеры сцены, физика, анимации), запись идёт вдвое дольше нужного, а FFmpeg ускоряет её в 2× — получается 50 FPS. У физики Arcade шкала обратная, поэтому там множитель 1/factor.

Перед запуском нужны Playwright и FFmpeg локально, плюс запущенный dev-сервер на localhost. Скрипт scripts/capture-promo.mjs пишется под конкретную игру: агент читает исходники, ищет метод, который вызывается при смерти, и патчит его. Если FFmpeg не найден, скилл предупредит и пропустит шаг с видео — сама игра от этого не ломается.

Какие задачи решает

  • записать вертикальный промо-клип игры без ручной съёмки
  • обойти ограничение Playwright в 25 FPS
  • показать непрерывный геймплей без экрана проигрыша
  • подобрать естественные нажатия клавиш под тип игры
  • собрать MP4 9:16 для соцсетей из готовой сборки Phaser

Примеры запросов агенту

  • запиши промо-видео для игры в games/my-game
  • сделай геймплейную запись на 13 секунд для Reels
  • сними промо-ролик этой Phaser-игры в 9:16

Как установить promo-video

1Через Skills CLI (Vercel)

npx skills add PlayableIntelligence/game-creator --skill promo-video -a claude-code

2Вручную: положить файлы

~/.claude/skills/promo-video/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/promo-video/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора27

qa-game Скилл настраивает автотесты на Playwright для веб-игры: проверяет запуск, переход между сценами, счёт, снимки экранов и FPS. Пригодится для быстрой проверки механики без ручных тестов. CX Codex +1 931установок meshyai Генерирует 3D-модели из текста или картинки через Meshy AI, ставит скелет на гуманоидов и добавляет анимации ходьбы и бега для Three.js. CX Codex +1 907установок quick-game Генерирует черновой играбельный прототип на Phaser или Three.js без графики, звука и полировки. Пригодится, когда нужно сразу запустить и проверить базовую механику. CX Codex +1 866установок monetize-game Регистрирует браузерную игру на Play.fun, подключает клиентский SDK с очками и таблицей лидеров, а затем выдаёт ссылку для игроков. CX Codex +1 862установок record-promo Записывает автономный ролик геймплея в 50 FPS для TikTok, Reels и X. Нужен, когда игра уже играется, а показать её нечем. CX Codex +1 840установок use-template Копирует готовый шаблон игры из галереи в новый проект. Рабочий код появляется сразу, без ИИ-пайплайна. CX Codex +1 838установок worldlabs Генерирует фотореалистичные 3D-окружения из текста или картинки через World Labs Marble API. Пригодится, когда нужен уровень или локация для игры. CX Codex +1 827установок fetch-tweet Извлекает текст публикации, медиафайлы и счётчики реакций из X без рендеринга JavaScript. Скилл запрашивает данные напрямую через публичный API fxtwitter по ссылке на твит. CX Codex +1 802установок

Похожие

demo-video Alireza Rezvani Превращает описания сцен и скриншоты в готовые демо-ролики или GIF с озвучкой и анимацией прямо в Claude Code. 708установок newsletter-promo charlie947 Собирает 12–20-секундный промо-клип для LinkedIn, Instagram или X из одного выпуска рассылки: хук, три кадра-доказательства и финальная карточка. CX Codex +1 1 400установок motion-broll Barty-Bart Создаёт моушн-графику и перебивки для разговорных видео по таймкодам субтитров. Генерирует анимации с морфингом одного элемента в такт речи спикера. CX Codex +1 1 700установок browser-video-recording MengTo Собирает демо-видео интерфейса в 4K 60 fps с пропорциями 4:3 из снимков браузера. Пригодится для показа кликов и плавного зума в стиле Dribbble. CX Codex +1 1 300установок showcase-video rehan-remade Записывает окно игры с её собственным звуком и собирает из нарезки короткий ролик о моде: титры, переходы, музыка, затемнение. CX Codex +1 1 400установок playwright-recording digitalsamba Скилл записывает действия в браузере как видео через Playwright — для демо, обзоров интерфейса и футажа под Remotion. CX Codex +1 1 300установок

Рядом в теме «Геймдев и 3D: Unity, Unreal, Godot, Blender»

Всё по теме «Геймдев и 3D: Unity, Unreal, Godot, Blender» →

Вопросы о promo-video

Как установить promo-video в Claude?

Выполните npx skills add PlayableIntelligence/game-creator --skill promo-video -a claude-code. Скилл встанет в ~/.claude/skills/promo-video/SKILL.md.

Что делает promo-video?

Записывает автономный промо-ролик игры на Phaser: агент сам играет за игрока через Playwright, а FFmpeg ускоряет запись до 50 FPS в вертикали 9:16.

Что нужно установить заранее?

Playwright и FFmpeg локально, а игра должна быть доступна на localhost через dev-сервер. Без FFmpeg шаг с видео пропускается, игра при этом работает.

Исходный текст

SKILL.md · MIT
---
name: promo-video
description: >
  Record a high-FPS autonomous promo video of a Phaser game using Playwright.
  Triggers on: promo video, gameplay recording, marketing video, game capture.
argument-hint: "[path-to-game]"
license: MIT
compatibility: Requires Playwright and FFmpeg installed locally for video capture and encoding.
metadata:
  author: OpusGameLabs
  version: 1.3.0
  tags: [game, promo, video, recording, playwright, ffmpeg]
---

# Promo Video Recording

Record smooth, autonomous promo footage of a Phaser game for marketing / social media. The output is a 50 FPS MP4 in mobile portrait (9:16) — ready for TikTok, Reels, Moltbook, or X.

## Technique

Playwright's `recordVideo` caps at 25 FPS with no config option. We work around it:

1. **Slow the game to 0.5×** by patching all 5 Phaser time subsystems
2. **Record for 2× the desired duration** at Playwright's native 25 FPS
3. **FFmpeg speed-up 2×** → effective 50 FPS output

| Parameter               | Default                                  | Effect                                                                |
| ----------------------- | ---------------------------------------- | --------------------------------------------------------------------- |
| `SLOW_MO_FACTOR`        | `0.5`                                    | Game runs at half speed → 50 FPS output                               |
| `WALL_CLOCK_DURATION`   | `DESIRED_GAME_DURATION / SLOW_MO_FACTOR` | Record for 2× to get correct game-time                                |
| `VIEWPORT`              | `{ width: 1080, height: 1920 }`          | 9:16 mobile portrait (always default unless user specifies otherwise) |
| `DESIRED_GAME_DURATION` | `13000` (ms)                             | ~13s of game-time → ~6.5s promo clip                                  |

## Prerequisites

- **Playwright** — must be installed (`npm install -D @playwright/test && npx playwright install chromium`)
- **FFmpeg** — must be available on PATH (`brew install ffmpeg` on macOS)
- **Dev server running** — game must be served on localhost

Check both before starting:

```bash
npx playwright --version
ffmpeg -version | head -1
```

If FFmpeg is not found, warn the user and skip the promo video step (it's non-blocking — the game still works without it).

## Capture Script — Game-Specific Adaptation

Every game gets a custom `scripts/capture-promo.mjs`. The subagent **must read the game's source files** to determine:

### 1. Death/Failure Patching (CRITICAL)

The video must show continuous gameplay — never game over. Read `GameScene.js` (or equivalent) to find the death/failure method and monkey-patch it out.

**How to find it**: Search for the method called on collision/death. Common patterns:

- `this.triggerGameOver()` — dodge games
- `this.takeDamage()` → `this.lives <= 0` — multi-life games
- `this.gameOver()` — direct call
- `eventBus.emit(Events.PLAYER_HIT)` / `eventBus.emit(Events.GAME_OVER)` — event-driven

**Patch template** (adapt per game):

```js
await page.evaluate(() => {
  const scene = window.__GAME__.scene.getScene("GameScene");
  if (scene) {
    // Patch ALL paths to game over
    scene.triggerGameOver = () => {};
    scene.onPlayerHit = () => {};
    // For multi-life games, also prevent damage:
    // scene.takeDamage = () => {};
    // scene.playerDied = () => {};
  }
});
```

### 2. Input Sequence Generation

The video must show dynamic, natural-looking gameplay. Read the game's input handling to determine:

- **Which keys** — ArrowLeft/ArrowRight? Space? WASD? Mouse clicks?
- **Input style** — continuous hold (movement), tap (jump/shoot), or both?
- **Movement pattern** — should the player sweep across the screen, dodge reactively, jump rhythmically?

**Input patterns by game type:**

| Game Type           | Input Keys              | Pattern                                                                    |
| ------------------- | ----------------------- | -------------------------------------------------------------------------- |
| Side dodger         | ArrowLeft, ArrowRight   | Alternating holds (150-600ms) with variable pauses, occasional double-taps |
| Platformer / Flappy | Space                   | Rhythmic taps (80-150ms hold) with variable gaps (200-800ms)               |
| Top-down            | WASD / Arrows           | Mixed directional holds, figure-eight patterns                             |
| Shooter             | ArrowLeft/Right + Space | Movement interleaved with rapid fire                                       |
| Clicker/Tapper      | Mouse click / Space     | Rapid bursts separated by brief pauses                                     |

**Randomize timing** to avoid robotic-looking movement:

```js
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);
```

**Add a pause at the start** (1-2s) to let the entrance animation play — this is the hook.

### 3. Game Boot Detection

All Phaser games built with the `/viral-game` or `/make-game` pipelines expose these globals:

- `window.__GAME__` — Phaser.Game instance
- `window.__GAME_STATE__` — GameState singleton
- `window.__EVENT_BUS__` — EventBus singleton

Wait for both boot and active gameplay:

```js
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, {
  timeout: 10000,
});
```

### 4. Time Scaling Injection

Slow all 5 Phaser time subsystems for the recording:

```js
await page.evaluate(
  ({ factor }) => {
    const game = window.__GAME__;
    const scene = game.scene.getScene("GameScene");

    // 1. Update delta — slows frame-delta-dependent logic
    const originalUpdate = scene.update.bind(scene);
    scene.update = function (time, delta) {
      originalUpdate(time, delta * factor);
    };

    // 2. Tweens — slows all tween animations
    scene.tweens.timeScale = factor;

    // 3. Scene timers — slows scene.time.addEvent() timers
    scene.time.timeScale = factor;

    // 4. Physics — slows Arcade/Matter physics
    // NOTE: Arcade physics timeScale is INVERSE (higher = slower)
    if (scene.physics?.world) {
      scene.physics.world.timeScale = 1 / factor;
    }

    // 5. Animations — slows sprite animation playback
    if (scene.anims) {
      scene.anims.globalTimeScale = factor;
    }
  },
  { factor: SLOW_MO_FACTOR },
);
```

**The 5 subsystems:**

1. **Update delta** — `scene.update(time, delta * factor)` slows frame-delta-dependent logic
2. **Tweens** — `scene.tweens.timeScale` slows all tween animations
3. **Scene timers** — `scene.time.timeScale` slows `scene.time.addEvent()` timers
4. **Physics** — `scene.physics.world.timeScale` slows Arcade/Matter physics (uses inverse: `1/factor`)
5. **Animations** — `scene.anims.globalTimeScale` slows sprite animation playback

### 5. Video Finalization

```js
const video = page.video();
await context.close(); // MUST close context to finalize the video file
const videoPath = await video.path();
```

## Full Capture Script Template

```js
import { chromium } from "playwright";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_DIR = path.resolve(__dirname, "..");

// --- Config ---
const args = process.argv.slice(2);
function getArg(name, fallback) {
  const i = args.indexOf(`--${name}`);
  return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
}

const PORT = getArg("port", "3000");
const GAME_URL = `http://localhost:${PORT}/`;
const VIEWPORT = { width: 1080, height: 1920 }; // 9:16 mobile portrait
const SLOW_MO_FACTOR = 0.5;
const DESIRED_GAME_DURATION = parseInt(getArg("duration", "13000"), 10);
const WALL_CLOCK_DURATION = DESIRED_GAME_DURATION / SLOW_MO_FACTOR;
const OUTPUT_DIR = path.resolve(PROJECT_DIR, getArg("output-dir", "output"));
const OUTPUT_FILE = path.join(OUTPUT_DIR, "promo-raw.webm");

// <ADAPT: Generate game-specific input sequence>
function generateInputSequence(totalMs) {
  const sequence = [];
  let elapsed = 0;

  // Pause for entrance animation
  sequence.push({ key: null, holdMs: 0, pauseMs: 1500 });
  elapsed += 1500;

  // <ADAPT: Replace with game-specific keys and timing>
  const keys = ["ArrowLeft", "ArrowRight"];
  let keyIdx = 0;

  while (elapsed < totalMs) {
    const holdMs = 150 + Math.floor(Math.random() * 450);
    const pauseMs = 50 + Math.floor(Math.random() * 250);

    // Occasional double-tap for variety
    if (Math.random() < 0.15) {
      sequence.push({ key: keys[keyIdx], holdMs: 100, pauseMs: 60 });
      elapsed += 160;
    }

    sequence.push({ key: keys[keyIdx], holdMs, pauseMs });
    elapsed += holdMs + pauseMs;

    // Alternate direction (with occasional same-direction repeats)
    if (Math.random() < 0.75) keyIdx = 1 - keyIdx;
  }

  return sequence;
}

async function captureGameplay() {
  console.log("Capturing promo video...");
  console.log(
    `  URL: ${GAME_URL} | Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`,
  );
  console.log(
    `  Game duration: ${DESIRED_GAME_DURATION}ms | Wall clock: ${WALL_CLOCK_DURATION}ms`,
  );

  fs.mkdirSync(OUTPUT_DIR, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: VIEWPORT,
    recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
  });

  const page = await context.newPage();
  await page.goto(GAME_URL, { waitUntil: "networkidle" });

  // Wait for game boot + gameplay active
  await page.waitForFunction(() => window.__GAME__?.isBooted, {
    timeout: 15000,
  });
  await page.waitForFunction(() => window.__GAME_STATE__?.started, {
    timeout: 10000,
  });
  await page.waitForTimeout(300);
  console.log("  Game active.");

  // <ADAPT: Patch out death — find the actual methods from GameScene.js>
  await page.evaluate(() => {
    const scene = window.__GAME__.scene.getScene("GameScene");
    if (scene) {
      scene.triggerGameOver = () => {};
      scene.onPlayerHit = () => {};
    }
  });
  console.log("  Death patched.");

  // Slow all 5 Phaser time subsystems
  await page.evaluate(
    ({ factor }) => {
      const game = window.__GAME__;
      const scene = game.scene.getScene("GameScene");
      const originalUpdate = scene.update.bind(scene);
      scene.update = function (time, delta) {
        originalUpdate(time, delta * factor);
      };
      scene.tweens.timeScale = factor;
      scene.time.timeScale = factor;
      if (scene.physics?.world) scene.physics.world.timeScale = 1 / factor;
      if (scene.anims) scene.anims.globalTimeScale = factor;
    },
    { factor: SLOW_MO_FACTOR },
  );
  console.log(`  Slowed to ${SLOW_MO_FACTOR}x.`);

  // Execute input sequence
  const sequence = generateInputSequence(WALL_CLOCK_DURATION);
  console.log(
    `  Playing ${sequence.length} inputs over ${WALL_CLOCK_DURATION}ms...`,
  );

  for (const seg of sequence) {
    if (!seg.key) {
      await page.waitForTimeout(seg.pauseMs);
      continue;
    }
    await page.keyboard.down(seg.key);
    await page.waitForTimeout(seg.holdMs);
    await page.keyboard.up(seg.key);
    if (seg.pauseMs > 0) await page.waitForTimeout(seg.pauseMs);
  }

  console.log("  Input complete.");

  // Finalize video
  const video = page.video();
  await context.close();
  const videoPath = await video.path();

  if (videoPath !== OUTPUT_FILE) {
    fs.renameSync(videoPath, OUTPUT_FILE);
  }

  await browser.close();
  console.log(`  Raw recording: ${OUTPUT_FILE}`);
  console.log("Done.");
}

captureGameplay().catch((err) => {
  console.error("Capture failed:", err);
  process.exit(1);
});
```

## FFmpeg Conversion

After recording, convert the raw slow-mo WebM to a high-FPS MP4. The `convert-highfps.sh` script is bundled with this skill at `skills/promo-video/scripts/convert-highfps.sh`.

```bash
# Copy to project (orchestrator does this)
cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/scripts/

# Run conversion
bash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5
```

The script:

- Applies `setpts` to speed up the video by `1/factor`
- Sets output framerate to `25 / factor` (= 50 FPS for 0.5× slow-mo)
- Encodes H.264 with `crf 23`, `yuv420p`, `faststart`
- Verifies output duration, frame rate, and file size

## Viewport Defaults

**Always record in mobile portrait (9:16)** unless the user explicitly requests otherwise. Rationale:

- Games are played on phones — promo footage should show the real mobile experience
- 9:16 is native for TikTok, Instagram Reels, YouTube Shorts
- 1080×1920 is the standard resolution

| Aspect Ratio       | Viewport      | Use Case                                          |
| ------------------ | ------------- | ------------------------------------------------- |
| **9:16 (default)** | `1080 × 1920` | Mobile portrait — TikTok, Reels, Shorts, Moltbook |
| 1:1                | `1080 × 1080` | Square — Instagram feed, X posts                  |
| 16:9               | `1920 × 1080` | Landscape — YouTube, trailers, desktop games      |

## Duration Guidelines

| Game Type       | Recommended Duration | Why                                 |
| --------------- | -------------------- | ----------------------------------- |
| Arcade / dodger | 10-15s               | Fast action, multiple dodge cycles  |
| Platformer      | 15-20s               | Show jump timing, level progression |
| Shooter         | 12-18s               | Show targeting, enemy waves         |
| Puzzle          | 8-12s                | Show one solve sequence             |

## Checklist

Before running the capture:

- [ ] Dev server is running and responding
- [ ] FFmpeg is installed on the system
- [ ] Playwright is installed with Chromium
- [ ] Game boots directly into gameplay (no menu blocking)
- [ ] Death/failure method identified and patched
- [ ] Input keys match the game's actual controls
- [ ] Entrance animation pause is included (1-2s)
- [ ] Output directory exists

After capture:

- [ ] Raw WebM exists in output/
- [ ] FFmpeg conversion produces valid MP4
- [ ] Duration is ~half the raw recording (speed-up worked)
- [ ] Frame rate is 50 FPS
- [ ] Video shows gameplay (not a black screen)

© OpusGameLabs · MIT · PlayableIntelligence/game-creator

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой