brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
gameMake 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 fo…
Independent GitHub snapshot · Automated classification · Original documentation governs
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.
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 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.
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:
@homie-rocks/* packages (npm) for the general mechanism: camera, input, audio, effects;parts_find for pieces other studios shared, and bring one in with part_add.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 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 by
").
Read RULES.md before writing code. All five 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; use diagnostics to guide the next edit.
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.
coin-dash is the smallest 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.
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.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.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.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.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.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.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.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.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:
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>.
For a world that should still exist tomorrow, set game.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.
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).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 }).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.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.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").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).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:
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.
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.
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 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.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.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.@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 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.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.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.
/<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:
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:
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.
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.
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.
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.
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.