LLMSkillHub
mirroredCopied from autonomous-ai/openharness under MIT. autonomous-ai has not claimed this page, and is not affiliated with LLM SkillHub.

autonomous-ai / harness-phaser

Use this skill first in a Harness Phaser workspace. The project layout, the Vite dev-server pane and its live reload, the webview keyboard-focus rule (click-to-play gate and on-screen touch buttons), procedural art with no asset files, and .harness/verdict.json. Triggers on: Harness, the pane, dev server, keyboard focus, click to play, touch buttons, verdict, workspace layout, new game.

0
SKILL.md
7.6 KB

harness-phaser

How a Phaser game is built inside a Harness workspace: where the files go, what the pane is, why
the first scene must wait for a click, and what the verdict checks.

Related skills: ../game-setup-and-config/SKILL.md, ../scenes/SKILL.md,
../input-keyboard-mouse-touch/SKILL.md, ../graphics-and-shapes/SKILL.md, ../scale-and-responsive/SKILL.md

The workspace

index.html          loaded by the pane; script tag → /src/main.js
vite.config.mjs     960×540 FIT, base './', build.outDir 'out/dist', cacheDir '.vite'
src/main.js         new Phaser.Game({...}) — config and the scene list. The entry; keep the name.
src/scenes/*.js     one class per file, `export default class X extends Phaser.Scene`
src/touch.js        touchPad(scene, labels) → on-screen controls
public/             served at the root: /style.css, and any real asset files
out/dist/           the production build, written by the verdict. Generated; never edit.
node_modules        a SYMLINK to the Harness package's shared install. Never npm install here.
.harness/verdict.json   the pane header. Written by the verdict script only.

Phaser is 4.2.1, loaded as a module: import Phaser from 'phaser'; at the top of every file
that names it. There is no global Phaser and no <script src>.

The pane is a Vite dev server

Harness runs Vite's dev server on the workspace (with this vite.config.mjs, on its own port) and
shows the game beside the terminal, in a frame with screen sizes, pause, restart, a physics-debug
toggle and full screen. That means:

  • Saving a file reloads the game. No build step, no refresh, no URL to print. A module with
    export default class ... extends Phaser.Scene reloads through a full page reload. The pane then
    takes the player back to the scene they were in, once the scene before it (the title, the
    preloader) is running again — so a scene must work when started directly with scene.start(key)
    and its own init data; if it crashes that way, the pane falls back to the first scene.

  • Errors are shown, not hidden. A syntax error, an unresolved import or an exception in
    create() appears in the pane as a card with the file, the line and the code around it, and
    clears on the next good save. The verdict still reports build errors for the header.

  • The controls hint under the pane is read from the keys the running scene registers
    (createCursorKeys, addKeys, keyboard.on('keydown-X')) and whether it listens for the pointer.
    To say it in words instead, put <meta name="harness:controls" content="←→ move · SPACE jump">
    in index.html.

  • Never start a second server (npm run dev, vite preview, python -m http.server). The port
    is taken and the pane is already pointed at it.

  • An error card in the pane is a JavaScript error, not a Phaser problem. Run the verdict: the Vite
    build reports the same error with a file and a line.

Keyboard focus: the rule that makes or breaks the game

The pane is a WKWebView. It does not receive key events until the player clicks inside it. A
game that starts straight into update() reading cursors.left.isDown looks broken — it draws, it
just never moves.

Every game therefore opens on a gate:

export default class Title extends Phaser.Scene {
    constructor() { super('Title'); }

    create() {
        const { width, height } = this.scale;
        this.add.text(width / 2, height / 2 - 40, 'SPACE RUNNER', {
            fontFamily: 'ui-sans-serif, sans-serif', fontSize: '64px', color: '#e9ecf5'
        }).setOrigin(0.5);
        const hint = this.add.text(width / 2, height / 2 + 50, 'Click to play', {
            fontFamily: 'ui-sans-serif, sans-serif', fontSize: '30px', color: '#06d6a0'
        }).setOrigin(0.5);
        this.tweens.add({ targets: hint, alpha: 0.2, duration: 700, yoyo: true, repeat: -1 });
        this.add.text(width / 2, height - 56, '←  →   move    ·    SPACE   jump', {
            fontFamily: 'ui-monospace, monospace', fontSize: '16px', color: '#6b7794'
        }).setOrigin(0.5);

        // The click starts the game AND gives the webview its keyboard focus.
        this.input.once('pointerdown', () => this.scene.start('Play'));
    }
}

The prompt must say click, not "press any key" — a key press cannot arrive yet. Put the controls
on the title screen too: the pane is where the user reads them.

On-screen controls, always worth adding

Pointer input needs no focus at all, so a pad makes the game playable before the click and on a
phone. src/touch.js in the template:

import { touchPad } from '../touch.js';

create() {
    this.cursors = this.input.keyboard.createCursorKeys();
    this.keys = this.input.keyboard.addKeys('A,D,SPACE');
    this.pad = touchPad(this, { space: 'JUMP' });   // { space: null } for no action button
}

update() {
    const left  = this.cursors.left.isDown  || this.keys.A.isDown || this.pad.left;
    const right = this.cursors.right.isDown || this.keys.D.isDown || this.pad.right;
    const jump  = this.cursors.space.isDown || this.keys.SPACE.isDown || this.pad.space;
}

Each button is a Rectangle with setInteractive() and pointerdown/pointerup/pointerout
handlers flipping a boolean, at setScrollFactor(0).setDepth(1000) so a moving camera leaves it
alone. Write your own variant when the game needs a different set (a d-pad, two action buttons).

Also avoid keys the browser keeps: Cmd/Ctrl combinations, F-keys, Tab. Arrows, WASD, space, Z/X and
Enter are all free.

Art without asset files

A fresh workspace has no images and needs none. Draw once, bake to a texture, use it like any other:

preload() {
    const g = this.add.graphics();
    g.fillStyle(0xffd166, 1).fillRoundedRect(0, 0, 48, 48, 8);
    g.fillStyle(0x0b0d12, 1).fillCircle(16, 18, 4).fillCircle(32, 18, 4);
    g.generateTexture('player', 48, 48);
    g.destroy();                      // one Graphics object, cleared and reused, then dropped
}

create() {
    this.player = this.physics.add.image(120, 300, 'player');
}

generateTexture goes through the Canvas API, so gradients (fillGradientStyle) do not survive it —
use flat fills, or a Gradient game object instead. Shapes (this.add.rectangle, .circle,
.triangle, .star) are fine for HUD and background, but a physics body is happier on an image.
Real assets, when the user supplies them, go in public/ and load as /name.png.

The verdict

python3 "$PHASER_TOOLCHAIN/verdict.py"             # after every change
python3 "$PHASER_TOOLCHAIN/verdict.py" --no-build  # a fast pass; leaves Build unverified

It writes .harness/verdict.json and prints the same thing. Three phases:

PhaseWhat it means
Writesrc/main.js exists and a file declares a scene
Buildvite build --outDir out/dist succeeded — the compile check; a syntax error, a bad import or an unresolved module becomes a finding with its file and line
Playa scene has a create() and the game handles input; a missing click-to-play gate is an info, not a failure

ready is true when it builds and something would respond to the player. Run it after every pass,
not at the end: the header is how the user sees progress. Never write the file by hand.