Skip to content
Back to skills

Game

ASecurity

Make or change a rules-plus-view multiplayer web game inside a Homie studio — the server runs the rules, each browser predicts its own movement and renders the game, strangers meet in public rooms, bots fill empty seats, rounds end and restart — prove it with two browsers finishing a round, and give it an epic landing page (a full-bleed hero from its own footage or art, the pitch, Play, phone / computer / TV, live rooms, how to play, credits). Use when someone in a Homie studio (a folder with...

  • 1,531 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentstypescriptrustgoswiftshellbashreactnodeexpressaws

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add hashgraph-online/awesome-codex-plugins --skill game --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Game?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Game
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hashgraph-online-game/badge)](https://www.skillsdirectory.com/skills/hashgraph-online-game)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: game
description: Make or change a rules-plus-view multiplayer web game inside a Homie studio — the server runs the rules, each browser predicts its own movement and renders the game, strangers meet in public rooms, bots fill empty seats, rounds end and restart — prove it with two browsers finishing a round, and give it an epic landing page (a full-bleed hero from its own footage or art, the pitch, Play, phone / computer / TV, live rooms, how to play, credits). Use when someone in a Homie studio (a folder with studio.json) asks for a new game, a change to a game, a game whose own AI decides (opponents picking tactics, an NPC's reaction, a director's call, a turn's move), or a better page for a game ("make my game's landing page epic").
compatibility: Node 22 and Chrome. The studio's pinned Wrangler runs the dev site; the GitHub CLI (gh) opens a pull request when the studio publishes that way; Ollama with clef-flash, optional, answers a game's AI decisions on this computer under dev (downloaded only after the person's yes).
metadata:
  providers: cloudflare github ollama
---

For a game with its own fixed integration rate, use the opt-in
`ctx.substeps(body, hz, step)` facility in `RULES.md` (Integrating at the game's
own rate). Its callbacks share the tick budget and its recorded poses drive
local drawing. Keep the game's input-transition policy explicit.

Read the installed toolkit's `node_modules/@homie-rocks/studio/guides/game/{RULES,REWRITE}.md`
before authoring: these match its runtime even when this plugin is older. Toolkit
check/build report stale installed plugin copies and the update step. For solid slopes and terrain, use heightTiles with `base` and the source
`diagonal`; test sides, underside, step clearance and rays. Maps are indexed at
compile time up to 100,000 shapes; measure default-budget spawn and traversal. For live
cover, doors and platforms, use declared entity `collider` state and movement's
`ctx.world.sweep/support/overlaps`; test prediction, loss, rejoin and restore.


**Apps:** For a business, venue, cause or customer app, follow the `app` skill: `apps/<id>/app.json`, one morphing screen, roles and parts. Reuse these engines and workflows; do not impose game rounds, scores, bots, a game demo or page navigation. The app check proves shared actions and reconnect; app stores use the same standalone command.


# Make a game

For place-based builds, read [the six-build patterns](../app/PLACES.md): owner words to needs, records as objects, a local scene and ambience, one trusted operation for screens and AI, graceful graphics, and adversarial proof. Apply the same authority and honest-evidence rules to game economies and tickets.


You are in a Homie studio when the folder (or one above it) has `studio.json`. If there
is none, use the `studio-setup` skill first.

**Asked to rewrite an existing browser-hosted game for the server? Read
[REWRITE.md](REWRITE.md) in full before editing.** Preserve the renderer, feel,
other games and every existing test file, including its import paths. If an old
`rules.ts` exports tested helpers, keep those exports when adding defineRules;
do not redirect the tests to a new helper file. Run the original test command.

For a request involving a larger room or map, read RULES.md's **Spatial delivery**
section. `players.max` chooses the room size and Gate layout. Choose `room.view.radiusM`
for the playable neighbourhood, and visual precision and far update rates to fit
the game. Local measurements are in docs/rooms-milestone-2-notes.md; they are not
Cloudflare or physical-phone capacity guarantees.

## Start it

- **Look for pieces before writing them.** Games build on each other by sharing parts: a creature from one game,
  a level from another, a bot brain from a third. For each system the game needs (camera, movement, bots, pickups,
  effects, sound, UI, environment), before writing it from scratch:
  - check the `@homie-rocks/*` packages (npm) for the general mechanism: camera, input, audio, effects;
  - run `parts_find` for pieces other studios shared, and bring one in with `part_add`.

  Search by system (touch controls, feedback, sound, scoring, title and results UI, rooms), not only by genre. Add a fitting result with `part_add` before implementing a replacement; import and use it in the game. A starter or npm package is not a catalog search. Record part version, source game/studio, licence and integration in CODEX **Built from**, or the search and concrete gap if nothing fits. In a hosted workspace these same tools work; after `part_add`, wait for the committed preview through `build_progress`.

  Record both in the game's CODEX (Built from) and its credits: what came from where, and what you wrote here and
  why. After building a piece another game could use, offer to make it a part (`part_new`) and, only if the person
  asks, to share it (`part_share`). The `parts` skill has the method.
- **A new studio's first game:** follow `studio-setup`, choose sensible defaults from the request,
  write the plan and build it. The live demo is optional; never require a detour or an interview.
- **A game with a Game Codex** (`games/<id>/CODEX.md`): read it first; it is the plan. Every decision that
  changes it goes into it in the same change, with a dated line under Latest, and the page is redrawn
  (`npx --no-install homie-studio codex <id>`). A big change to a game without one: plan it first.
- **Other studios** beside this one (a neighbouring folder with its own `studio.json`) are other people's
  work: never read their games or copy from them, not even as a model for this one, unless the person asks.
  What you may read is this studio and the toolkit in its `node_modules/@homie-rocks/studio/`.
- **New game:** call the Homie MCP tool `game_make` (id, name) for the exact command
  and rules (without the connector, go on: the command is here and the rules are this skill's), then run
  `npx --no-install homie-studio game new <id> --from gem-rush --name "<Name>"`
  (`--from block-drop` for a falling-block puzzle; `--from gem-rush-3d` for a 3D game: three.js with free library models; `--from hero-rush-3d` for a 3D game with
  animated characters that run, jump and swing). The id becomes the game's
  address (`/<id>/`); lowercase, digits, hyphens.
- **Make an existing single-player web game multiplayer:** use the `port` skill (it
  grades the port, brings the game in, and proves it with the owner tests).
- **Build on another studio's game:** through parts, never the whole game (remix was
  retired; `game remix` answers with a sentence that says so). `parts_find` searches the pieces
  other studios shared (a creature, a level, a bot brain) and `part_add` brings one in with its
  licence and credit: the `parts` skill has the method. When someone asks to copy or remix a
  whole game, say that plainly and offer to make a game of their own like it, with parts where
  some fit.
- **A game's own licence** is game.json `"license"`: an SPDX identifier (`"MIT"`), said on its
  page; a game that names none says nothing. A game.json with `"remixOf"` was made from another
  studio's game: keep the key, whatever else changes (its landing says "Based on <game> by
  <studio>").

## Write and change rules plus view

For wrapping worlds or generated levels, use the map's opt-in `wrap` and room-scoped
`world.map.replace` APIs described in RULES.md's **Wrapped worlds and generated levels**.
Prefer one indexed map revision for a generated maze; keep independently moving
doors and platforms as live colliders. Generation and navigation baking spend the
room handler's budget.

**Read [RULES.md](RULES.md) before writing code.** All starters use this contract.
Work in few, large edits: read each source file once, decide its changes, then
make every change it needs in ONE file_edit with an edits list. Build after each
coherent change across rules, movement and view. When a diagnostic names a file and line, read that exact location and repair the expression it names before unrelated cleanup. Rebuild to verify that error is gone. `room.me.seat` identifies a player; there is no `room.me.slot`. Prefer targeted edits for repairs. A write replaces the whole file: never send only its tail or resume a truncated file with a replacement containing the remaining lines.

New games use `game.json` `"entry": "src/view.ts"`, `"room": { "host": "server" }`,
and a studio-chosen `players.max`. HTML loads `<script type="module"
src="./assets/main.js"></script>`, not the source-file URL. Choose seats, tick rate (20 by default), dimensions and
starter yourself from the request; never ask the person an engineering question.
`block-drop` is the falling-block/board-game reference: separate stationary wells, seven-piece bags, rotation, line clears, real keyboard and touch controls. Use it for Tetris-like requests and retain its working mechanic while changing the requested look or features. `coin-dash` is the smallest movement reference; `gem-rush` adds bumps; `ember-vale` adds
companions, decisions and character saves; the two 3D starters add models and jumps.
A starter is a starting point: implement the requested mechanic, not a renamed gem collector.
The build refuses a game with neither rules plus view nor a browser-hosted netplay contract;
a plain browser bundle is not a multiplayer game.

- **Truth: `src/rules.ts`.** Default-export `defineRules({ contract: 2, ... })`.
  Declare every lasting value in entity `fields`, movement `motion`, or room `shared`,
  and payloads in `shapes`. An entity handler changes only `self`; room handlers alone
  change shared state. Affect others with declared events, delivered on a later tick.
  Validate commands against current turn, cooldown, distance and ownership in rules.
  IDs are opaque `f.ref()` strings; compare them, never parse or sort them as numbers.
- **Movement: `src/move.ts`.** Export `move = defineMove(...)`, import it in rules.
  The server and local prediction run the same step. Read only body pose/motion,
  input, public tune, map plus declared live colliders and `ctx` clock/math. Sweep against the world, normalize
  diagonals, and declare the normal running `maxSpeed`. Put jumps, dashes and knocks
  here, with rule decisions encoded in `motion`. Do not add `body.move: 'owner'`:
  authoritative movement already predicts immediately and keeps the wall solid.
- **View: `src/view.ts`.** `import type rules from './rules'` and
  `openRoom<typeof rules>()`; send `room.input` and `room.command`, draw `room.me`,
  `room.each`, `room.shared`, `room.round` and `room.roster`. Handle a null own body.
  Camera, models, DOM, audio and particles stay here. The local pose is predicted;
  other poses are interpolated. A button or swing can animate on press, but damage,
  pickups and scores wait for the server. No second physics loop or local score authority.
- **Determinism and budget.** Use `world.tick`, `world.dt`, `world.ticks`, `world.after`,
  `world.random` and `world.math` (movement: `ctx`). No clocks, timers, async, network,
  `Math.random`, transcendental `Math` functions, `**`, changing module state or
  load-time computation in rules. Bounded local queries, bounded collections and
  declared fields keep work under the deterministic budget. Read all build messages
  and repair the named handler; never evade the guard or raise budgets to hide a bug.
- **Levels:** declare `map: './map'` and put bounds, solids and named spots in
  `map/main.json`. Draw from the same map. In 3D rules use metres, z up and feet
  positions; Three.js draws `(x,z,y)`. Height tiles are terrain; boxes are solid
  platforms. Decorative hills or meshes do not collide unless represented in the map.
- **Rounds and arrivals:** use automatic rounds or `rounds.seconds: 0` plus a room
  handler that calls `world.round.end()` for turn games. Never give a quiz or word
  game fake movement just to satisfy a test. Initialize late arrivals in `on.arrive`
  as well as existing bodies in `onRoom.roundStart`. `think` returns inputs, not commands;
  bot actions go through the same rules as a person's. Show the actual goal and result.
- **Companions:** `guide.view` returns declared `shapes.view`; `guide.floor` chooses
  a vocabulary goal without a model; `think` follows `self.goal` and calls
  `world.goalDone`. Write `agents.json` goals, lines and asks in the game's voice.
  Draw `room.askButtons(id)` and call `room.ask`. Server policies reserve companion
  seats; input never grants a role. Use `world.level`, `world.guideLevel`,
  `world.guideSeats` and `world.kids`. Preserve visible AI labels.
- **Game decisions:** declare `asks` with state, questions and a synchronous floor;
  call `world.ask` per beat or turn and handle `on.answer` once (`ai`, `local`, or
  `floor`). No promise/await or `net.decide` in rules. The floor must play well alone.
- **Hosting and saving:** server hosting is the default. Browser hosting is for
  offline play, local experiments or private friends games, using the same rules;
  it is not a fix for a failed build. Room state saves automatically and restores
  as a whole, but the room ends 60 seconds after its last person leaves. Character
  progress uses the separate saves API below. No secrets in entity/shared state:
  every replica receives it. A prop disguise is a visual mechanic, not secure hidden
  information. Private per-seat information and bigger rooms belong to milestone 2.
- **Changing a rules game:** read its codex, declarations, move and view; change the
  responsible half, preserve unrelated play, and rebuild. The build/state hashes
  manage compatibility automatically; do not hand-pack snapshots or bump netplay.version.
  Recheck the changed mechanic, a late join and a reload in two browsers.
- **Changing an old-style game:** existing netplay games keep working on 0.45.0
  with their original 32-seat limit. Preserve their behavior on ordinary edits.
  For a server migration or larger room, follow [REWRITE.md](REWRITE.md).
  New games use rules plus view; there is no automatic converter yet.
- Phones get touch, computers get keys; typed names and chat are text only.
  Keep native DOM buttons outside `guardGestures`' touch-drag selector; otherwise bind
  touch pointers explicitly. Verify actual touchscreen taps, not only `.click()`.
  Keep controls clear of the shell's pills (`room.net.shell`). Watchers use
  `room.net.viewSeat` for their camera and HUD, and `room.net.spotlight` for action.
  `watch: 'overview'` or `false` changes the offered UI, not secrecy of snapshots.

Make it the requested game, one milestone at a time. Read each file, make coherent
edits, build, act on the diagnostics and test the actual play before declaring it done.

- **Room chat and speech bubbles** (NETPLAY.md section 19; `chat/CHAT.md`). Every game has room chat on its
  play page with no code: reactions that float up every screen, quick lines, and typing where the rules allow.
  Give it the game's own voice in `game.json` `"chat"`: `"lines"` (up to 12 quick lines, short and kind:
  `{ "gg": "Good game!", "gem": "Grab that gem!" }`), up to 3 extra `"emoji"` (`{ "gem": "💎" }`), and the
  defaults that suit its players (`"mode": "lines"` for a game for children; the owner can change any of it).
  Draw what players say over their characters with the port kit, after the name labels:

  ```ts
  const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; } });
  net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
  net.on('unchat', (e) => e.ids.forEach((id) => bubbles.remove(id)));
  paintBubbles(ctx, bubbles.place(speakers.map((p) => ({ key: p.seat, x: p.labelX, y: p.labelTop, self: p.mine }))));
  ```
  A game's own chat keys (a quick-line wheel): `net.sayLine('gg')`, `net.react('fire')`. Never draw a name or a
  message as markup. **Where chat sits:** the Chat pill rides the room button's band (`screen.share` below; a
  round icon on a phone), and the strip of new lines shows for a moment at the bottom left. If the game's HUD or
  controls are there, put the strip where it has room in `game.json` `"screen": { "chat": { "at": "top-right",
  "y": 110 } }` (a corner, `top-center` or `bottom-center`; `x` / `y` in pixels; per device: `desk`, `phone`,
  `sideways`, `tv`), or keep new lines in the Chat sheet with `"lines": "sheet-only"` (the pill counts them).
  Never turn typing off to make room: place the strip instead (`chat/CHAT.md` has every field).
- Update `game.json` `name` and `blurb`, and the `<title>`.

## Lasting worlds and account characters

For a world that should still exist tomorrow, set game.json:

```json
{"room":{"host":"server","lifetime":"persistent","characters":true}}
```

Lasting worlds default to `offline: false`: wait for the durable server. A studio
may explicitly set `offline: true` for separate local practice, whose progress
is not saved to the world or account. Never count that as proof of persistence.

`characters` is optional: `lifetime` alone keeps the world. Account characters
require the existing studio sign-in/passkey. Play redirects to the account page
when needed. In `room.join(ctx, player)`, return
`player.character ?? { kind: 'runner', at: spawn }`. Keep inventory in that player
kind's declared fields and change it only in rules. The runtime returns its kind,
position, heading, fields and motion to the account when it leaves. Use
`player: true` or `leave: 'despawn'`; a returned character cannot also become a bot.
Do not reset inventory in `arrive`, round-start handlers or `world.finish`.

The public world has a stable room name; `?room=<name>` chooses another world.
Every tick commits before publishing its results. An empty world sleeps and
retains state. A failed write stops play with the last good save intact. Another
room waits for the current holder to return the character; no timeout grants a
stale copy. No database or IDs are set up by hand. Ordinary games keep their
existing save frequency and behavior.

Write idempotent pickups using a saved collectible ID/set; duplicate commands
must not mint again. Events arrive later and can become `undeliverable` when a
recipient leaves; do not treat sending one as an atomic inventory transfer.
Trading and shop-grant consumption need the later economy slice. Browser saves
remain player-owned and are not an authoritative inventory.

This first slice saves the whole world every tick, so opted-in games pay for
more writes. Compatible deploys still restart/reload and then restore the world.
A state-schema change is refused on restore with data retained; keep the previous
compatible build until migrations ship. Offline play has no account authority.

For proof, build normally, then sign two players in and test movement, pickup,
restart mid-pickup, a compatible rebuild, and a fresh profile signing in as the
same player. Verify actual inventory and world state. A lasting world need not
finish a round: its browser proof replaces the match check's round requirement.

## Progress that lasts: cloud saves

When the plan says progress persists across sessions or devices (the codex's "Progress that lasts", or the person
asks for a character, levels, unlocks, a collection, hardcore), wire in saves. For player-owned browser progress use the existing saves API below. For a server-authoritative
world or inventory, use Lasting worlds above. Ordinary match rooms expire after their last player leaves.

- `game.json`: `"saves": true`. A new game of that kind can start from the starter:
  `npx --no-install homie-studio game new <id> --from ember-vale --name "<Name>"` (a hero that lasts, lifetime
  stats, a hardcore mode with a hall of the fallen, on the same rooms as any game).
- In the game: `import { createSaves } from '@homie-rocks/studio/saves'`; `const saves = createSaves({ game: '<id>' })`.
  Load on arrival (`await saves.get('hero')`), save when it changes (`saves.set('hero', hero)`, at most about once a
  second), and reload on `saves.on('player', ...)` (the player signed in on this device). Keep one character in ONE
  key. Lifetime numbers: `saves.stats.add({ kills: 1 })`. Hardcore: `saves.fall({ character, summary, wipe: true })`.
- Rules emit declared effects; the view checks the effect's entity ID against `room.me?.id` before
  changing its own character save. These browser-written saves are player-owned progress, not a
  server-verified economy or authority for purchases. Room durability is separate and automatic.
- A character's name is the save's, not the room's: the room knows a person by their account's name or a two-word
  handle. Draw the character's name over its body and in the ranking (a declared command requests it; rules validate and keep it in the player entity), except on a kids server, where the others stay handles. Ember Vale does it.
- Show who is playing (`saves.player.name`, guest or signed in) and a small "Keep my progress" button that calls
  `saves.signIn()`; the play page shows its passkey sheet. Pressing Play never needs an account.
- A guest who signs in to an account that already has the same key: say how the two combine, once, in
  `site/src/worker.mjs`: `mergeSaves({ '<id>': { saves: { coins: 'sum', 'best-level': 'larger', settings: 'newer' },
  stats: { kills: 'sum' } } })` from `@homie-rocks/studio/worker` (or a function for an inventory). Server records
  are joined without a rule; any other key with no rule reaches the game's `onConflict` (SAVES.md, "When a guest
  signs in to their account").
- Prove it: build, `npm run dev`, open `http://localhost:8787/<id>/play` (passkeys need `localhost`, not
  `127.0.0.1`), play, reload: the progress is still there. The whole guide is
  `node_modules/@homie-rocks/studio/saves/SAVES.md` (the limits, offline and conflicts, what is stored, privacy).

## Prove it

Open a progress feed for every build, titled with what it does (the codex's milestone); the codex
page's Build status tab and Claude Code's status line follow it by themselves (in Codex and Grok Build
the codex page is the view: `npx --no-install homie-studio codex <id> --open`). When the person is
following along in the Claude app (or anywhere they cannot see your terminal), share it and show it:

```sh
npx --no-install homie-studio progress start <id> --share --title "<what this build does>" [--budget <dollars>]
npx --no-install homie-studio progress stage plan done --note "<the plan in one line>"
```

It prints a build id: call the Homie MCP tool `build_progress` with it once, and where the app
draws cards (the Claude app) the card follows the build by itself (stages, each check going green, a
preview, spend, Stop). The commands below report into it. If a command answers `stopped`, the person pressed Stop:
end there and ask before starting again.

**Started from the Claude app** (a Claude Code session whose prompt came from a "Build it"
card, naming a build `hb_...` and maybe a setup `hs_...`): the chat already opened the build, so
take it instead of starting one, then work on a branch and publish as a pull request. The
prompt names the studio's repository: first check this session is in it
(`git remote get-url origin`). If it is in another one (`homie-rocks/homie` is Homie's
engine and template, never a studio), stop and say so; never attach from it.

```sh
npm install                                            # the studio's pinned toolkit, from registry.npmjs.org
npx --no-install homie-studio setup attach hs_...        # only when the prompt names a setup: once, first
npx --no-install homie-studio progress attach hb_...     # this session takes the chat's build (once)
npx --no-install homie-studio chrome install           # Linux without Chrome: Chrome for Testing, once
# ... make the game, build, dev, check (below) ...
npx --no-install homie-studio progress change "<what the change does, one line>"
git switch -c <short-branch> && git add -A && git commit -m "<what it does>" && git push -u origin HEAD
gh pr create --fill
npx --no-install homie-studio progress pr --url <the pull request's address>
```

The card's Publish button opens the pull request for the person; their merge in GitHub is
the approval. Workers Builds deploys the branch as a Preview (run `check --url <the Preview
URL>` when the pull request shows it, and pass it as `progress pr --preview`) and `main`
after the merge; the card says Live by itself. Never merge the pull request yourself. If an
attach fails, quote the toolkit's message as it is: it names the directory's status and its
own words, or the connection error and whether this machine's proxy was used. Never guess at
the cause. Only when it says the network proxy refused homie.rocks, pass on the setting it
names; then go on, since the build works with its local feed. On Linux without a GPU,
`check` measures seats, rooms and rounds; its frame rate is SwiftShader's, not a person's:
say so rather than calling the game slow.

```sh
npm run build                                          # fix every error it names (a game that does not build fails it)
npx --no-install homie-studio build --types            # the same, with the TypeScript checked first: run it before a deploy
npm run dev                                            # in the background: http://127.0.0.1:8787/<id>/play
npx --no-install homie-studio check <id> --url http://127.0.0.1:8787 --shots ./.checks
# With no --url the check runs against this studio's own build (its running dev server, or one it starts and
# stops); with no <id>, on the games this checkout changed. The live site only with --live or its address.
npx --no-install homie-studio shoot <id> --preview     # pictures of the built game on a stepped clock, no site needed (a 3D game where there is no GPU)
npx --no-install homie-studio preview <id>             # only that game's built files at an address (alone, offline): for a capture script
```

`npx --no-install homie-studio dev --timestamps` puts a time on every line the site prints (room
sockets and errors always have one): use it to lay a connection loss beside a check's report.
`shoot <id> --url http://127.0.0.1:8787` adds a two-client seat smoke check in a private room.
Where there is no terminal, the same commands go through the `studio_run` tool, by their words.

`check` passes only when two fresh browsers (a computer and a phone) press Play, land
in the same room, and both see a round finish with both of them in the results. Look
at the screenshots it saves. Never say a game works without a passing `check`.
Start `npm run dev` as a background task your app keeps alive (Claude Code: the Bash
tool's `run_in_background`), and when you are done with it: stop it with `npx --no-install homie-studio dev --stop`, which stops exactly this studio's dev server (and its Wrangler) and nothing else. Never `pkill`, `killall` or `lsof ... | xargs kill` by name or port: other projects on this machine may run their own `wrangler dev`, and a pattern stops theirs too.
Before giving a live preview link, fetch it and confirm its server will outlive the
task. A terminal session may end with a headless agent. If you stopped the server
or cannot keep it alive, say the game is built and give `npm run dev` and the local
path; do not describe a stopped preview as running.
In a headless authoring task, always include the start command and game path in
the handoff: a successful curl proves the link works now, not after the agent exits.

Then make it good, not just working:

- **Sound**: the `sound` skill makes the game's effects and a synthesized theme for free and wires
  them in (`sound.play('coin')` where it happens, in every browser). A silent game is not finished.
- **Look**: first the decisions (the `style` skill: render style, palette, light, camera, fonts, budgets; automatic
  from the person's words, drawn in the codex), then the models (the `models` skill: the engine and the free CC0
  starter library first, the person's own files with their licence, generated props and characters only on their own
  fal account under a budget), the characters' rigs and clips (the `animate` skill), then the `art` skill's cover from a real frame and, with a budget, painted backdrops and
  textures. "Make the look better" goes through `style` and `models` before any painting.
- **Match the brief's tone**: a cozy, calm or gentle brief is not a fight. Score together (in `gem-rush-3d`,
  game.json `"scoring": "together"`: one total the room fills, no places), make contact gentle or none, and give the
  bots friendly names; keep rivals, rankings and knocks for briefs that ask for competition.
- **A place, not a board**: a 3D game's play area reads as somewhere. Give it a heart the theme names (a den, a
  campfire, a well, a market stall) built in the game's style, a few solid features players move round, and
  something to do within a few steps of any spot, so a phone's close view is never bare ground. Look at the computer
  and phone frames before you call it done.
- **Models in code**: a three.js game loads every model through `@homie-rocks/studio/assets` (`createModels()`,
  `instance(url)`, `placeholder(size)`): it refuses unsafe or oversized files and decodes the phone-sized format
  `assets add` writes. Its development warnings use a small prop's budget (1,500 triangles, 300 KB a model); a game
  whose models are bigger on purpose sets its own once in game.json, `"assets": { "budgets": { "triangles": 8000,
  "bytes": 1500000 } }`, instead of living with a warning per model. Code a round does not need at once
  (a later level, an editor) can load later: `await import('./level-2')` becomes a file of its own in the build. Read colours and fonts from the game's `style.json` instead of hard-coding them, so the
  locked palette reaches the world and the HUD. `games/<id>/assets/manifest.json` records every model's origin and
  licence; keep it true (`assets add` and `assets remove`, never a hand-copied .glb), and `assets check <id>`
  before a deploy. The `gem-rush-3d` starter (`game new <id> --from gem-rush-3d`) is Gem Rush in 3D with library
  models: start a 3D game from it. With characters that move (people, heroes, fighters, creatures), start from
  `hero-rush-3d` instead: animated CC0 heroes through `@homie-rocks/studio/animate` (idle, walk, run, jump, a swing,
  hits, a cheer), one shared clip library per skeleton, a jump and a swing tuned in the Game Lab. The `animate` skill
  has rigs, clips, retargeting and feel.
- **Playtest**: the `playtest` skill plays it on a computer and a phone held both ways, measures the
  first ten seconds, the look, the UI, the real sound and a round, and runs the owner tests.
  Read the pictures and save an honest local verdict; use a fresh outside reviewer when requested.
  Fix the most important finding and repeat the affected checks.
- **Speed**: when it stutters, loads slowly or a phone struggles, the `perf` skill measures it (frame
  times, CPU per frame on player browsers, time to playable, downloads, memory, netplay; server
  tick work is a separate measurement) and
  keeps a change only when it is faster beyond the noise and two browsers still finish a round.
- **Feel**: when a move feels floaty, stiff, weak or unclear ("the jump", "the hit", "the drift"), the `lab` skill
  builds a Game Lab for it: one take in the new build beside the last commit, frame by frame, with named phases,
  graphs and sliders that write kept values into the game's `tunables.json`.

Then give it its landing (below) and a working local result. Publish with `npm run
deploy` and `studio_publish` (see `publish`) when publishing is in the owner's
requested scope, then check again on the live site. A build-only request needs no
Cloudflare setup or release question. Honor a local-only or no-deploy instruction.

## Its landing page: `/<id>/`

Every game gets a landing page from the studio template, made from the game's own files
(`node_modules/@homie-rocks/studio/site/SITE.md` has every field): a full-bleed hero, the pitch, a big
Play button that drops a visitor into a public room, how to play on a phone, a computer and a TV (with
the join code), the live rooms, how to play and credits. It is as good as what you give it. "Make the landing
page epic" means all of this, in this order:

1. **Footage in the hero.** The biggest single difference. Capture the game running with the `video`
   skill (`capture <slug> --game <id> --url http://127.0.0.1:8787 --seconds 30`), pick 8 to 12 s where a
   lot happens, and cut two silent loops into `games/<id>/hero/`: `wide.mp4` (16:9) and `tall.mp4`
   (9:16, the phone's). Under 3 MB each, so a phone starts it at once:

   ```sh
   C=videos/<slug>/work/capture/capture.mp4
   ffmpeg -y -ss <start> -t 10 -i $C -an -vf "scale=1600:-2,fps=30" -c:v libx264 -crf 27 -preset slow -pix_fmt yuv420p -movflags +faststart games/<id>/hero/wide.mp4
   ffmpeg -y -ss <start> -t 10 -i $C -an -vf "crop=ih*9/16:ih,scale=720:-2,fps=30" -c:v libx264 -crf 27 -preset slow -pix_fmt yuv420p -movflags +faststart games/<id>/hero/tall.mp4
   ffmpeg -y -ss <start+2> -i $C -frames:v 1 -q:v 3 games/<id>/hero/wide.jpg
   ```

   Look at them (`ffprobe`, and a frame or two): the game, not a menu, a QR code or a black frame. A
   finished trailer in `videos/` with `"for": { "game": "<id>" }` is used when there is no `hero/`.
   No footage at all: the cover (the `art` skill's `cover`, from a real frame) moves slowly instead.
2. **The words**, in game.json's `landing` block: `pitch` (one line a stranger gets at once), `about`
   (a short paragraph), `howToPlay` (three to five short lines), `controls` for `phone`, `computer`
   (and `tv` if it differs), `players` (what a player is called: `{ "one": "pilot", "many": "pilots" }`),
   `hero.alt` (what the footage shows, for a screen reader), `hero.focus` (`"50% 35%"` keeps the action
   in frame on a phone), `hero.tint` (0 to 80: more when the art is bright and the title hard to read).
   A game with a `style.json` gets its landing in that palette by itself (its paper, ink and accents,
   light or dark as its paper is), so a bright game is not shown on the studio's dark page; leave
   `scheme` and `theme` out unless the person wants something else. A game without one whose picture is
   white or cream (a light arena) takes `"scheme": "light"`: its landing is drawn light, where the studio's
   dark tint would turn the picture grey. `hero/wide.jpg` is also the game's picture on every card and in
   the directory, and the play page's arrival card while the game loads, so pick a frame that reads small.
   game.json `"genre"` (a word, or up to three: `["Racing", "Party"]`) and pictures of real play in
   `games/<id>/screenshots/` (at most eight) go on the landing and into its structured data for search
   engines; say what the game is, never invent a rating or a review.
3. **Credits**: `landing.credits` names who made what (`[{ "role": "Music", "name": "..." }]`). A port
   keeps its `credits.json` (the original, its author and licence, every part inside); a game that has
   game.json `remixOf` keeps it; never drop either.
4. **The look**: the studio's `site/theme.json` colours; `landing.theme` gives this game its own
   `accent` and `glow` on its page, when two games of one studio should not look alike.
5. **A band of its own**, when the game has something to say that the template does not (a soundtrack, a
   mode, a season): `site/partials/game-<id>.html`, a short section in the page's own classes
   (`<p class="kicker">`, `<h2>`, `<p class="lead">`, `<a class="ghost">`).

**The play page's first seconds**: from the first paint it shows the game's arrival card (its title, pitch,
hero still, a progress line and `landing.controls` for the device) until the game says it is playable, never a
blank screen (`SITE.md`, "The play page"). Its words and picture are the landing's, so give the landing its
`pitch`, `controls` and a hero still. A game that keeps loading after the room's first state (models,
textures, a baked world) passes `arrival: 'game'` to `openRoom`, calls `net.loading(p, 'the heroes')`
while it loads and `net.playable()` once its world and the player's own body are drawn, so nobody sees
stand-ins (NETPLAY.md section 21); the starters do. `perf` measures it: `load.look` (the first meaningful
frame), `load.playable` (control-ready: seated, a body, the card gone) and `load.ready` (the game's own
`net.playable()`), with the arrival mode beside them. `check` says seated, ready (the card lifted) and connected
apart: a seat is not ready, and a finished round is not an uninterrupted one.

**The play page's room button** (Invite, Big screen, the room code) sits top right, with the Chat pill
beside it. If the game draws a score, a timer or a bar there, move them in game.json: `"screen": { "share":
{ "desk": "bottom-left", "phone": { "at": "top-left", "y": 56 } } }` (a corner or `top-center`, per device:
`desk`, `phone`, `sideways`; `x` / `y` move it in, in pixels; `"label": false` keeps both small icons). Room
chat's strip of new lines moves with `"screen": { "chat": … }` (above). Look at `/<id>/play` on a computer and
a phone, both ways up, while a round is on and somebody says something in chat.

Then `npm run build`, `npm run dev`, and look at `http://127.0.0.1:8787/<id>/` as a stranger would:
a computer (1440 wide) and a phone (390 wide, and turned sideways), from the top, scrolling to the end.
The `playtest` skill takes the screenshots. The hero reads at a glance, Play is above the fold on a phone,
nothing is cut off or runs off the side. Fix what you see; build again.

A whole landing of the studio's own (`site/pages/<id>/index.html`) replaces the generated one: only when
the person asks for a hand-made page, and start from the generated one's HTML so Play, the TV road and
"Made with Homie" stay.

## Physics

For movement against a static GLB's triangles, use the opt-in physics bridge in
[RULES.md](RULES.md#physics). Install `@homie-rocks/physics` 0.1.2, set
`map/main.json`'s `physics.level` to the game's GLB, and call
`ctx.physics.sweep(body, delta, radius, height)` inside the game's movement.
`ctx.substeps(body, 300, step => { ... })` shares its existing handler budget and
predicts the same baked world in the browser. The full example, cost table and
save contract are in that section. Prefer kernel collision for its primitive
maps; use physics for arbitrary static triangle levels. Initialization is awaited
before the first room tick, calls in handlers are synchronous, and the complete
physics snapshot is saved with the room. No opt-in means no physics load or cost.

### Input under loss

Rules games inherit redundant sequenced input, late movement replay and applied-tick
receipts. Do not add a game-side resend loop or interpret negative held values as
late-release repayment. Use `f.held()` for duration, sampled fields for latest
state and `f.pulse()` for ordered actions. Choose `player.pulses: 'hold'` when a
knock should buffer an action; the default drops it. See RULES.md, “Input during
loss and stalls”, for timestamps, replay boundaries and visual correction settings.

Files in this skill

  • REWRITE.md6.8 KB
  • RULES.md94 KB
  • SKILL.md38.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.