playableintelligence/game-creatorMIT2 files

Promo Video

>

Specification
Skill ID
playableintelligence/game-creator/promo-video
Publisher
playableintelligence
Repository
game-creator
Installs
0
Files
2
License
MIT
Requires
Requires Playwright and FFmpeg installed locally for video capture and encoding.
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

playableintelligence/game-creator/promo-videoInstalls these files
  • SKILL.md
  • scripts/convert-highfps.sh

What this skill tells the agent

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
ParameterDefaultEffect
SLOW_MO_FACTOR0.5Game runs at half speed → 50 FPS output
WALL_CLOCK_DURATIONDESIRED_GAME_DURATION / SLOW_MO_FACTORRecord for 2× to get correct game-time
VIEWPORT{ width: 1080, height: 1920 }9:16 mobile portrait (always default unless user specifies otherwise)
DESIRED_GAME_DURATION13000 (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:

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):

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 TypeInput KeysPattern
Side dodgerArrowLeft, ArrowRightAlternating holds (150-600ms) with variable pauses, occasional double-taps
Platformer / FlappySpaceRhythmic taps (80-150ms hold) with variable gaps (200-800ms)
Top-downWASD / ArrowsMixed directional holds, figure-eight patterns
ShooterArrowLeft/Right + SpaceMovement interleaved with rapid fire
Clicker/TapperMouse click / SpaceRapid bursts separated by brief pauses

Randomize timing to avoid robotic-looking movement:

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:

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:

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

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

Full Capture Script Template

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);

Use this skill in a project

Open any RiverX project, search the Skills panel for playableintelligence/game-creator/promo-video, and install it in one click.

Use this skill