@homie-rocks/studio 0.44.0 → 0.44.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +2 -1
- package/bin/homie-studio.mjs +4 -0
- package/guides/game/REWRITE.md +94 -0
- package/guides/game/RULES.md +754 -0
- package/guides/game/SKILL.md +394 -0
- package/lib/authoring-guides.mjs +27 -0
- package/lib/cloudflare.mjs +4 -3
- package/lib/dev.mjs +4 -2
- package/lib/rules-check-worker.mjs +4 -3
- package/lib/rules-guard.mjs +2 -1
- package/lib/scaffold.mjs +9 -2
- package/lib/template-history.json +84 -0
- package/lib/worker-build.mjs +50 -6
- package/lib/worker-config.mjs +2 -2
- package/package.json +5 -3
- package/rules/collision.ts +16 -10
- package/rules/core.ts +35 -14
- package/rules/interest.mjs +5 -3
- package/rules/live.ts +75 -0
- package/rules/map-index.ts +51 -0
- package/rules/math.ts +15 -9
- package/rules/rules.ts +27 -5
- package/rules/terrain.ts +93 -0
- package/rules/types.ts +4 -3
- package/rules/view.ts +22 -4
- package/starters/hero-rush-3d/src/move.ts +6 -2
- package/worker/customer-shop.mjs +1 -1
- package/worker/version.mjs +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: game
|
|
3
|
+
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").
|
|
4
|
+
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).
|
|
5
|
+
metadata:
|
|
6
|
+
providers: cloudflare github ollama
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Read the installed toolkit's `node_modules/@homie-rocks/studio/guides/game/{RULES,REWRITE}.md`
|
|
10
|
+
before authoring: these match its runtime even when this plugin is older. Toolkit
|
|
11
|
+
check/build report stale installed plugin copies and the update step. For solid slopes and terrain, use heightTiles with `base` and the source
|
|
12
|
+
`diagonal`; test sides, underside, step clearance and rays. Maps are indexed at
|
|
13
|
+
compile time up to 100,000 shapes; measure default-budget spawn and traversal. For live
|
|
14
|
+
cover, doors and platforms, use declared entity `collider` state and movement's
|
|
15
|
+
`ctx.world.sweep/support/overlaps`; test prediction, loss, rejoin and restore.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
**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.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Make a game
|
|
22
|
+
|
|
23
|
+
You are in a Homie studio when the folder (or one above it) has `studio.json`. If there
|
|
24
|
+
is none, use the `studio-setup` skill first.
|
|
25
|
+
|
|
26
|
+
**Asked to rewrite an existing browser-hosted game for the server? Read
|
|
27
|
+
[REWRITE.md](REWRITE.md) in full before editing.** Preserve the renderer, feel,
|
|
28
|
+
other games and every existing test file, including its import paths. If an old
|
|
29
|
+
`rules.ts` exports tested helpers, keep those exports when adding defineRules;
|
|
30
|
+
do not redirect the tests to a new helper file. Run the original test command.
|
|
31
|
+
|
|
32
|
+
For a request involving a larger room or map, read RULES.md's **Spatial delivery**
|
|
33
|
+
section. 0.44.0 can reduce what a server player receives with `room.view.radiusM`;
|
|
34
|
+
Gates and public admission above 32 seats are the next slice. Choose a radius
|
|
35
|
+
only when it fits the game. No rules rewrite or new profile is needed.
|
|
36
|
+
|
|
37
|
+
## Start it
|
|
38
|
+
|
|
39
|
+
- **Look for pieces before writing them.** Games build on each other by sharing parts: a creature from one game,
|
|
40
|
+
a level from another, a bot brain from a third. For each system the game needs (camera, movement, bots, pickups,
|
|
41
|
+
effects, sound, UI, environment), before writing it from scratch:
|
|
42
|
+
- check the `@homie-rocks/*` packages (npm) for the general mechanism: camera, input, audio, effects;
|
|
43
|
+
- run `parts_find` for pieces other studios shared, and bring one in with `part_add`.
|
|
44
|
+
|
|
45
|
+
Record both in the game's CODEX (Built from) and its credits: what came from where, and what you wrote here and
|
|
46
|
+
why. After building a piece another game could use, offer to make it a part (`part_new`) and, only if the person
|
|
47
|
+
asks, to share it (`part_share`). The `parts` skill has the method.
|
|
48
|
+
- **A new studio's first game:** follow `studio-setup`, choose sensible defaults from the request,
|
|
49
|
+
write the plan and build it. The live demo is optional; never require a detour or an interview.
|
|
50
|
+
- **A game with a Game Codex** (`games/<id>/CODEX.md`): read it first; it is the plan. Every decision that
|
|
51
|
+
changes it goes into it in the same change, with a dated line under Latest, and the page is redrawn
|
|
52
|
+
(`npx --no-install homie-studio codex <id>`). A big change to a game without one: plan it first.
|
|
53
|
+
- **Other studios** beside this one (a neighbouring folder with its own `studio.json`) are other people's
|
|
54
|
+
work: never read their games or copy from them, not even as a model for this one, unless the person asks.
|
|
55
|
+
What you may read is this studio and the toolkit in its `node_modules/@homie-rocks/studio/`.
|
|
56
|
+
- **New game:** call the Homie MCP tool `game_make` (id, name) for the exact command
|
|
57
|
+
and rules (without the connector, go on: the command is here and the rules are this skill's), then run
|
|
58
|
+
`npx --no-install homie-studio game new <id> --from gem-rush --name "<Name>"`
|
|
59
|
+
(`--from gem-rush-3d` for a 3D game: three.js with free library models; `--from hero-rush-3d` for a 3D game with
|
|
60
|
+
animated characters that run, jump and swing). The id becomes the game's
|
|
61
|
+
address (`/<id>/`); lowercase, digits, hyphens.
|
|
62
|
+
- **Make an existing single-player web game multiplayer:** use the `port` skill (it
|
|
63
|
+
grades the port, brings the game in, and proves it with the owner tests).
|
|
64
|
+
- **Build on another studio's game:** through parts, never the whole game (remix was
|
|
65
|
+
retired; `game remix` answers with a sentence that says so). `parts_find` searches the pieces
|
|
66
|
+
other studios shared (a creature, a level, a bot brain) and `part_add` brings one in with its
|
|
67
|
+
licence and credit: the `parts` skill has the method. When someone asks to copy or remix a
|
|
68
|
+
whole game, say that plainly and offer to make a game of their own like it, with parts where
|
|
69
|
+
some fit.
|
|
70
|
+
- **A game's own licence** is game.json `"license"`: an SPDX identifier (`"MIT"`), said on its
|
|
71
|
+
page; a game that names none says nothing. A game.json with `"remixOf"` was made from another
|
|
72
|
+
studio's game: keep the key, whatever else changes (its landing says "Based on <game> by
|
|
73
|
+
<studio>").
|
|
74
|
+
|
|
75
|
+
## Write and change rules plus view
|
|
76
|
+
|
|
77
|
+
**Read [RULES.md](RULES.md) before writing code.** All five starters use this contract.
|
|
78
|
+
Work in few, large edits: read each source file once, decide its changes, then
|
|
79
|
+
make every change it needs in ONE file_edit with an edits list. Build after each
|
|
80
|
+
coherent change across rules, movement and view; use diagnostics to guide the next edit.
|
|
81
|
+
|
|
82
|
+
New games use `game.json` `"entry": "src/view.ts"`, `"room": { "host": "server" }`,
|
|
83
|
+
and `players.max` at most 32. HTML loads `<script type="module"
|
|
84
|
+
src="./assets/main.js"></script>`, not the source-file URL. Choose seats, tick rate (20 by default), dimensions and
|
|
85
|
+
starter yourself from the request; never ask the person an engineering question.
|
|
86
|
+
`coin-dash` is the smallest reference; `gem-rush` adds bumps; `ember-vale` adds
|
|
87
|
+
companions, decisions and character saves; the two 3D starters add models and jumps.
|
|
88
|
+
A starter is a starting point: implement the requested mechanic, not a renamed gem collector.
|
|
89
|
+
The build refuses a game with neither rules plus view nor a browser-hosted netplay contract;
|
|
90
|
+
a plain browser bundle is not a multiplayer game.
|
|
91
|
+
|
|
92
|
+
- **Truth: `src/rules.ts`.** Default-export `defineRules({ contract: 2, ... })`.
|
|
93
|
+
Declare every lasting value in entity `fields`, movement `motion`, or room `shared`,
|
|
94
|
+
and payloads in `shapes`. An entity handler changes only `self`; room handlers alone
|
|
95
|
+
change shared state. Affect others with declared events, delivered on a later tick.
|
|
96
|
+
Validate commands against current turn, cooldown, distance and ownership in rules.
|
|
97
|
+
IDs are opaque `f.ref()` strings; compare them, never parse or sort them as numbers.
|
|
98
|
+
- **Movement: `src/move.ts`.** Export `move = defineMove(...)`, import it in rules.
|
|
99
|
+
The server and local prediction run the same step. Read only body pose/motion,
|
|
100
|
+
input, public tune, map plus declared live colliders and `ctx` clock/math. Sweep against the world, normalize
|
|
101
|
+
diagonals, and declare the normal running `maxSpeed`. Put jumps, dashes and knocks
|
|
102
|
+
here, with rule decisions encoded in `motion`. Do not add `body.move: 'owner'`:
|
|
103
|
+
authoritative movement already predicts immediately and keeps the wall solid.
|
|
104
|
+
- **View: `src/view.ts`.** `import type rules from './rules'` and
|
|
105
|
+
`openRoom<typeof rules>()`; send `room.input` and `room.command`, draw `room.me`,
|
|
106
|
+
`room.each`, `room.shared`, `room.round` and `room.roster`. Handle a null own body.
|
|
107
|
+
Camera, models, DOM, audio and particles stay here. The local pose is predicted;
|
|
108
|
+
other poses are interpolated. A button or swing can animate on press, but damage,
|
|
109
|
+
pickups and scores wait for the server. No second physics loop or local score authority.
|
|
110
|
+
- **Determinism and budget.** Use `world.tick`, `world.dt`, `world.ticks`, `world.after`,
|
|
111
|
+
`world.random` and `world.math` (movement: `ctx`). No clocks, timers, async, network,
|
|
112
|
+
`Math.random`, transcendental `Math` functions, `**`, changing module state or
|
|
113
|
+
load-time computation in rules. Bounded local queries, bounded collections and
|
|
114
|
+
declared fields keep work under the deterministic budget. Read all build messages
|
|
115
|
+
and repair the named handler; never evade the guard or raise budgets to hide a bug.
|
|
116
|
+
- **Levels:** declare `map: './map'` and put bounds, solids and named spots in
|
|
117
|
+
`map/main.json`. Draw from the same map. In 3D rules use metres, z up and feet
|
|
118
|
+
positions; Three.js draws `(x,z,y)`. Height tiles are terrain; boxes are solid
|
|
119
|
+
platforms. Decorative hills or meshes do not collide unless represented in the map.
|
|
120
|
+
- **Rounds and arrivals:** use automatic rounds or `rounds.seconds: 0` plus a room
|
|
121
|
+
handler that calls `world.round.end()` for turn games. Never give a quiz or word
|
|
122
|
+
game fake movement just to satisfy a test. Initialize late arrivals in `on.arrive`
|
|
123
|
+
as well as existing bodies in `onRoom.roundStart`. `think` returns inputs, not commands;
|
|
124
|
+
bot actions go through the same rules as a person's. Show the actual goal and result.
|
|
125
|
+
- **Companions:** `guide.view` returns declared `shapes.view`; `guide.floor` chooses
|
|
126
|
+
a vocabulary goal without a model; `think` follows `self.goal` and calls
|
|
127
|
+
`world.goalDone`. Write `agents.json` goals, lines and asks in the game's voice.
|
|
128
|
+
Draw `room.askButtons(id)` and call `room.ask`. Server policies reserve companion
|
|
129
|
+
seats; input never grants a role. Use `world.level`, `world.guideLevel`,
|
|
130
|
+
`world.guideSeats` and `world.kids`. Preserve visible AI labels.
|
|
131
|
+
- **Game decisions:** declare `asks` with state, questions and a synchronous floor;
|
|
132
|
+
call `world.ask` per beat or turn and handle `on.answer` once (`ai`, `local`, or
|
|
133
|
+
`floor`). No promise/await or `net.decide` in rules. The floor must play well alone.
|
|
134
|
+
- **Hosting and saving:** server hosting is the default. Browser hosting is for
|
|
135
|
+
offline play, local experiments or private friends games, using the same rules;
|
|
136
|
+
it is not a fix for a failed build. Room state saves automatically and restores
|
|
137
|
+
as a whole, but the room ends 60 seconds after its last person leaves. Character
|
|
138
|
+
progress uses the separate saves API below. No secrets in entity/shared state:
|
|
139
|
+
every replica receives it. A prop disguise is a visual mechanic, not secure hidden
|
|
140
|
+
information. Private per-seat information and bigger rooms belong to milestone 2.
|
|
141
|
+
- **Changing a rules game:** read its codex, declarations, move and view; change the
|
|
142
|
+
responsible half, preserve unrelated play, and rebuild. The build/state hashes
|
|
143
|
+
manage compatibility automatically; do not hand-pack snapshots or bump netplay.version.
|
|
144
|
+
Recheck the changed mechanic, a late join and a reload in two browsers.
|
|
145
|
+
- **Changing an old-style game:** ordinary requested edits keep it working untouched
|
|
146
|
+
in its existing architecture. When its owner asks to move rules to the server,
|
|
147
|
+
follow [REWRITE.md](REWRITE.md). There is no automatic converter.
|
|
148
|
+
- Phones get touch, computers get keys; typed names and chat are text only.
|
|
149
|
+
Keep native DOM buttons outside `guardGestures`' touch-drag selector; otherwise bind
|
|
150
|
+
touch pointers explicitly. Verify actual touchscreen taps, not only `.click()`.
|
|
151
|
+
Keep controls clear of the shell's pills (`room.net.shell`). Watchers use
|
|
152
|
+
`room.net.viewSeat` for their camera and HUD, and `room.net.spotlight` for action.
|
|
153
|
+
`watch: 'overview'` or `false` changes the offered UI, not secrecy of snapshots.
|
|
154
|
+
|
|
155
|
+
Make it the requested game, one milestone at a time. Read each file, make coherent
|
|
156
|
+
edits, build, act on the diagnostics and test the actual play before declaring it done.
|
|
157
|
+
|
|
158
|
+
- **Room chat and speech bubbles** (NETPLAY.md section 19; `chat/CHAT.md`). Every game has room chat on its
|
|
159
|
+
play page with no code: reactions that float up every screen, quick lines, and typing where the rules allow.
|
|
160
|
+
Give it the game's own voice in `game.json` `"chat"`: `"lines"` (up to 12 quick lines, short and kind:
|
|
161
|
+
`{ "gg": "Good game!", "gem": "Grab that gem!" }`), up to 3 extra `"emoji"` (`{ "gem": "💎" }`), and the
|
|
162
|
+
defaults that suit its players (`"mode": "lines"` for a game for children; the owner can change any of it).
|
|
163
|
+
Draw what players say over their characters with the port kit, after the name labels:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; } });
|
|
167
|
+
net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
|
|
168
|
+
net.on('unchat', (e) => e.ids.forEach((id) => bubbles.remove(id)));
|
|
169
|
+
paintBubbles(ctx, bubbles.place(speakers.map((p) => ({ key: p.seat, x: p.labelX, y: p.labelTop, self: p.mine }))));
|
|
170
|
+
```
|
|
171
|
+
A game's own chat keys (a quick-line wheel): `net.sayLine('gg')`, `net.react('fire')`. Never draw a name or a
|
|
172
|
+
message as markup. **Where chat sits:** the Chat pill rides the room button's band (`screen.share` below; a
|
|
173
|
+
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
|
|
174
|
+
controls are there, put the strip where it has room in `game.json` `"screen": { "chat": { "at": "top-right",
|
|
175
|
+
"y": 110 } }` (a corner, `top-center` or `bottom-center`; `x` / `y` in pixels; per device: `desk`, `phone`,
|
|
176
|
+
`sideways`, `tv`), or keep new lines in the Chat sheet with `"lines": "sheet-only"` (the pill counts them).
|
|
177
|
+
Never turn typing off to make room: place the strip instead (`chat/CHAT.md` has every field).
|
|
178
|
+
- Update `game.json` `name` and `blurb`, and the `<title>`.
|
|
179
|
+
|
|
180
|
+
## Progress that lasts: cloud saves
|
|
181
|
+
|
|
182
|
+
When the plan says progress persists across sessions or devices (the codex's "Progress that lasts", or the person
|
|
183
|
+
asks for a character, levels, unlocks, a collection, hardcore), wire in saves. Never keep it in the room: a room
|
|
184
|
+
forgets everything 60 s after its last player leaves.
|
|
185
|
+
|
|
186
|
+
- `game.json`: `"saves": true`. A new game of that kind can start from the starter:
|
|
187
|
+
`npx --no-install homie-studio game new <id> --from ember-vale --name "<Name>"` (a hero that lasts, lifetime
|
|
188
|
+
stats, a hardcore mode with a hall of the fallen, on the same rooms as any game).
|
|
189
|
+
- In the game: `import { createSaves } from '@homie-rocks/studio/saves'`; `const saves = createSaves({ game: '<id>' })`.
|
|
190
|
+
Load on arrival (`await saves.get('hero')`), save when it changes (`saves.set('hero', hero)`, at most about once a
|
|
191
|
+
second), and reload on `saves.on('player', ...)` (the player signed in on this device). Keep one character in ONE
|
|
192
|
+
key. Lifetime numbers: `saves.stats.add({ kills: 1 })`. Hardcore: `saves.fall({ character, summary, wipe: true })`.
|
|
193
|
+
- Rules emit declared effects; the view checks the effect's entity ID against `room.me?.id` before
|
|
194
|
+
changing its own character save. These browser-written saves are player-owned progress, not a
|
|
195
|
+
server-verified economy or authority for purchases. Room durability is separate and automatic.
|
|
196
|
+
- 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
|
|
197
|
+
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.
|
|
198
|
+
- Show who is playing (`saves.player.name`, guest or signed in) and a small "Keep my progress" button that calls
|
|
199
|
+
`saves.signIn()`; the play page shows its passkey sheet. Pressing Play never needs an account.
|
|
200
|
+
- Prove it: build, `npm run dev`, open `http://localhost:8787/<id>/play` (passkeys need `localhost`, not
|
|
201
|
+
`127.0.0.1`), play, reload: the progress is still there. The whole guide is
|
|
202
|
+
`node_modules/@homie-rocks/studio/saves/SAVES.md` (the limits, offline and conflicts, what is stored, privacy).
|
|
203
|
+
|
|
204
|
+
## Prove it
|
|
205
|
+
|
|
206
|
+
Open a progress feed for every build, titled with what it does (the codex's milestone); the codex
|
|
207
|
+
page's Build status tab and Claude Code's status line follow it by themselves (in Codex and Grok Build
|
|
208
|
+
the codex page is the view: `npx --no-install homie-studio codex <id> --open`). When the person is
|
|
209
|
+
following along in the Claude app (or anywhere they cannot see your terminal), share it and show it:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
npx --no-install homie-studio progress start <id> --share --title "<what this build does>" [--budget <dollars>]
|
|
213
|
+
npx --no-install homie-studio progress stage plan done --note "<the plan in one line>"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
It prints a build id: call the Homie MCP tool `build_progress` with it once, and where the app
|
|
217
|
+
draws cards (the Claude app) the card follows the build by itself (stages, each check going green, a
|
|
218
|
+
preview, spend, Stop). The commands below report into it. If a command answers `stopped`, the person pressed Stop:
|
|
219
|
+
end there and ask before starting again.
|
|
220
|
+
|
|
221
|
+
**Started from the Claude app** (a Claude Code session whose prompt came from a "Build it"
|
|
222
|
+
card, naming a build `hb_...` and maybe a setup `hs_...`): the chat already opened the build, so
|
|
223
|
+
take it instead of starting one, then work on a branch and publish as a pull request. The
|
|
224
|
+
prompt names the studio's repository: first check this session is in it
|
|
225
|
+
(`git remote get-url origin`). If it is in another one (`homie-rocks/homie` is Homie's
|
|
226
|
+
engine and template, never a studio), stop and say so; never attach from it.
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npm install # the studio's pinned toolkit, from registry.npmjs.org
|
|
230
|
+
npx --no-install homie-studio setup attach hs_... # only when the prompt names a setup: once, first
|
|
231
|
+
npx --no-install homie-studio progress attach hb_... # this session takes the chat's build (once)
|
|
232
|
+
npx --no-install homie-studio chrome install # Linux without Chrome: Chrome for Testing, once
|
|
233
|
+
# ... make the game, build, dev, check (below) ...
|
|
234
|
+
npx --no-install homie-studio progress change "<what the change does, one line>"
|
|
235
|
+
git switch -c <short-branch> && git add -A && git commit -m "<what it does>" && git push -u origin HEAD
|
|
236
|
+
gh pr create --fill
|
|
237
|
+
npx --no-install homie-studio progress pr --url <the pull request's address>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The card's Publish button opens the pull request for the person; their merge in GitHub is
|
|
241
|
+
the approval. Workers Builds deploys the branch as a Preview (run `check --url <the Preview
|
|
242
|
+
URL>` when the pull request shows it, and pass it as `progress pr --preview`) and `main`
|
|
243
|
+
after the merge; the card says Live by itself. Never merge the pull request yourself. If an
|
|
244
|
+
attach fails, quote the toolkit's message as it is: it names the directory's status and its
|
|
245
|
+
own words, or the connection error and whether this machine's proxy was used. Never guess at
|
|
246
|
+
the cause. Only when it says the network proxy refused homie.rocks, pass on the setting it
|
|
247
|
+
names; then go on, since the build works with its local feed. On Linux without a GPU,
|
|
248
|
+
`check` measures seats, rooms and rounds; its frame rate is SwiftShader's, not a person's:
|
|
249
|
+
say so rather than calling the game slow.
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
npm run build # fix every error it names (a game that does not build fails it)
|
|
253
|
+
npx --no-install homie-studio build --types # the same, with the TypeScript checked first: run it before a deploy
|
|
254
|
+
npm run dev # in the background: http://127.0.0.1:8787/<id>/play
|
|
255
|
+
npx --no-install homie-studio check <id> --url http://127.0.0.1:8787 --shots ./.checks
|
|
256
|
+
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)
|
|
257
|
+
npx --no-install homie-studio preview <id> # only that game's built files at an address (alone, offline): for a capture script
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`npx --no-install homie-studio dev --timestamps` puts a time on every line the site prints (room
|
|
261
|
+
sockets and errors always have one): use it to lay a connection loss beside a check's report.
|
|
262
|
+
`shoot <id> --url http://127.0.0.1:8787` adds a two-client seat smoke check in a private room.
|
|
263
|
+
Where there is no terminal, the same commands go through the `studio_run` tool, by their words.
|
|
264
|
+
|
|
265
|
+
`check` passes only when two fresh browsers (a computer and a phone) press Play, land
|
|
266
|
+
in the same room, and both see a round finish with both of them in the results. Look
|
|
267
|
+
at the screenshots it saves. Never say a game works without a passing `check`.
|
|
268
|
+
Start `npm run dev` as a background task your app keeps alive (Claude Code: the Bash
|
|
269
|
+
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.
|
|
270
|
+
Before giving a live preview link, fetch it and confirm its server will outlive the
|
|
271
|
+
task. A terminal session may end with a headless agent. If you stopped the server
|
|
272
|
+
or cannot keep it alive, say the game is built and give `npm run dev` and the local
|
|
273
|
+
path; do not describe a stopped preview as running.
|
|
274
|
+
In a headless authoring task, always include the start command and game path in
|
|
275
|
+
the handoff: a successful curl proves the link works now, not after the agent exits.
|
|
276
|
+
|
|
277
|
+
Then make it good, not just working:
|
|
278
|
+
|
|
279
|
+
- **Sound**: the `sound` skill makes the game's effects and a synthesized theme for free and wires
|
|
280
|
+
them in (`sound.play('coin')` where it happens, in every browser). A silent game is not finished.
|
|
281
|
+
- **Look**: first the decisions (the `style` skill: render style, palette, light, camera, fonts, budgets; automatic
|
|
282
|
+
from the person's words, drawn in the codex), then the models (the `models` skill: the engine and the free CC0
|
|
283
|
+
starter library first, the person's own files with their licence, generated props and characters only on their own
|
|
284
|
+
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
|
|
285
|
+
textures. "Make the look better" goes through `style` and `models` before any painting.
|
|
286
|
+
- **Match the brief's tone**: a cozy, calm or gentle brief is not a fight. Score together (in `gem-rush-3d`,
|
|
287
|
+
game.json `"scoring": "together"`: one total the room fills, no places), make contact gentle or none, and give the
|
|
288
|
+
bots friendly names; keep rivals, rankings and knocks for briefs that ask for competition.
|
|
289
|
+
- **A place, not a board**: a 3D game's play area reads as somewhere. Give it a heart the theme names (a den, a
|
|
290
|
+
campfire, a well, a market stall) built in the game's style, a few solid features players move round, and
|
|
291
|
+
something to do within a few steps of any spot, so a phone's close view is never bare ground. Look at the computer
|
|
292
|
+
and phone frames before you call it done.
|
|
293
|
+
- **Models in code**: a three.js game loads every model through `@homie-rocks/studio/assets` (`createModels()`,
|
|
294
|
+
`instance(url)`, `placeholder(size)`): it refuses unsafe or oversized files and decodes the phone-sized format
|
|
295
|
+
`assets add` writes. Its development warnings use a small prop's budget (1,500 triangles, 300 KB a model); a game
|
|
296
|
+
whose models are bigger on purpose sets its own once in game.json, `"assets": { "budgets": { "triangles": 8000,
|
|
297
|
+
"bytes": 1500000 } }`, instead of living with a warning per model. Code a round does not need at once
|
|
298
|
+
(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
|
|
299
|
+
locked palette reaches the world and the HUD. `games/<id>/assets/manifest.json` records every model's origin and
|
|
300
|
+
licence; keep it true (`assets add` and `assets remove`, never a hand-copied .glb), and `assets check <id>`
|
|
301
|
+
before a deploy. The `gem-rush-3d` starter (`game new <id> --from gem-rush-3d`) is Gem Rush in 3D with library
|
|
302
|
+
models: start a 3D game from it. With characters that move (people, heroes, fighters, creatures), start from
|
|
303
|
+
`hero-rush-3d` instead: animated CC0 heroes through `@homie-rocks/studio/animate` (idle, walk, run, jump, a swing,
|
|
304
|
+
hits, a cheer), one shared clip library per skeleton, a jump and a swing tuned in the Game Lab. The `animate` skill
|
|
305
|
+
has rigs, clips, retargeting and feel.
|
|
306
|
+
- **Playtest**: the `playtest` skill plays it on a computer and a phone held both ways, measures the
|
|
307
|
+
first ten seconds, the look, the UI, the real sound and a round, and runs the owner tests.
|
|
308
|
+
Read the pictures and save an honest local verdict; use a fresh outside reviewer when requested.
|
|
309
|
+
Fix the most important finding and repeat the affected checks.
|
|
310
|
+
- **Speed**: when it stutters, loads slowly or a phone struggles, the `perf` skill measures it (frame
|
|
311
|
+
times, CPU per frame on player browsers, time to playable, downloads, memory, netplay; server
|
|
312
|
+
tick work is a separate measurement) and
|
|
313
|
+
keeps a change only when it is faster beyond the noise and two browsers still finish a round.
|
|
314
|
+
- **Feel**: when a move feels floaty, stiff, weak or unclear ("the jump", "the hit", "the drift"), the `lab` skill
|
|
315
|
+
builds a Game Lab for it: one take in the new build beside the last commit, frame by frame, with named phases,
|
|
316
|
+
graphs and sliders that write kept values into the game's `tunables.json`.
|
|
317
|
+
|
|
318
|
+
Then give it its landing (below) and a working local result. Publish with `npm run
|
|
319
|
+
deploy` and `studio_publish` (see `publish`) when publishing is in the owner's
|
|
320
|
+
requested scope, then check again on the live site. A build-only request needs no
|
|
321
|
+
Cloudflare setup or release question. Honor a local-only or no-deploy instruction.
|
|
322
|
+
|
|
323
|
+
## Its landing page: `/<id>/`
|
|
324
|
+
|
|
325
|
+
Every game gets a landing page from the studio template, made from the game's own files
|
|
326
|
+
(`node_modules/@homie-rocks/studio/site/SITE.md` has every field): a full-bleed hero, the pitch, a big
|
|
327
|
+
Play button that drops a visitor into a public room, how to play on a phone, a computer and a TV (with
|
|
328
|
+
the join code), the live rooms, how to play and credits. It is as good as what you give it. "Make the landing
|
|
329
|
+
page epic" means all of this, in this order:
|
|
330
|
+
|
|
331
|
+
1. **Footage in the hero.** The biggest single difference. Capture the game running with the `video`
|
|
332
|
+
skill (`capture <slug> --game <id> --url http://127.0.0.1:8787 --seconds 30`), pick 8 to 12 s where a
|
|
333
|
+
lot happens, and cut two silent loops into `games/<id>/hero/`: `wide.mp4` (16:9) and `tall.mp4`
|
|
334
|
+
(9:16, the phone's). Under 3 MB each, so a phone starts it at once:
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
C=videos/<slug>/work/capture/capture.mp4
|
|
338
|
+
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
|
|
339
|
+
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
|
|
340
|
+
ffmpeg -y -ss <start+2> -i $C -frames:v 1 -q:v 3 games/<id>/hero/wide.jpg
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Look at them (`ffprobe`, and a frame or two): the game, not a menu, a QR code or a black frame. A
|
|
344
|
+
finished trailer in `videos/` with `"for": { "game": "<id>" }` is used when there is no `hero/`.
|
|
345
|
+
No footage at all: the cover (the `art` skill's `cover`, from a real frame) moves slowly instead.
|
|
346
|
+
2. **The words**, in game.json's `landing` block: `pitch` (one line a stranger gets at once), `about`
|
|
347
|
+
(a short paragraph), `howToPlay` (three to five short lines), `controls` for `phone`, `computer`
|
|
348
|
+
(and `tv` if it differs), `players` (what a player is called: `{ "one": "pilot", "many": "pilots" }`),
|
|
349
|
+
`hero.alt` (what the footage shows, for a screen reader), `hero.focus` (`"50% 35%"` keeps the action
|
|
350
|
+
in frame on a phone), `hero.tint` (0 to 80: more when the art is bright and the title hard to read).
|
|
351
|
+
A game with a `style.json` gets its landing in that palette by itself (its paper, ink and accents,
|
|
352
|
+
light or dark as its paper is), so a bright game is not shown on the studio's dark page; leave
|
|
353
|
+
`scheme` and `theme` out unless the person wants something else. A game without one whose picture is
|
|
354
|
+
white or cream (a light arena) takes `"scheme": "light"`: its landing is drawn light, where the studio's
|
|
355
|
+
dark tint would turn the picture grey. `hero/wide.jpg` is also the game's picture on every card and in
|
|
356
|
+
the directory, and the play page's arrival card while the game loads, so pick a frame that reads small.
|
|
357
|
+
game.json `"genre"` (a word, or up to three: `["Racing", "Party"]`) and pictures of real play in
|
|
358
|
+
`games/<id>/screenshots/` (at most eight) go on the landing and into its structured data for search
|
|
359
|
+
engines; say what the game is, never invent a rating or a review.
|
|
360
|
+
3. **Credits**: `landing.credits` names who made what (`[{ "role": "Music", "name": "..." }]`). A port
|
|
361
|
+
keeps its `credits.json` (the original, its author and licence, every part inside); a game that has
|
|
362
|
+
game.json `remixOf` keeps it; never drop either.
|
|
363
|
+
4. **The look**: the studio's `site/theme.json` colours; `landing.theme` gives this game its own
|
|
364
|
+
`accent` and `glow` on its page, when two games of one studio should not look alike.
|
|
365
|
+
5. **A band of its own**, when the game has something to say that the template does not (a soundtrack, a
|
|
366
|
+
mode, a season): `site/partials/game-<id>.html`, a short section in the page's own classes
|
|
367
|
+
(`<p class="kicker">`, `<h2>`, `<p class="lead">`, `<a class="ghost">`).
|
|
368
|
+
|
|
369
|
+
**The play page's first seconds**: from the first paint it shows the game's arrival card (its title, pitch,
|
|
370
|
+
hero still, a progress line and `landing.controls` for the device) until the game says it is playable, never a
|
|
371
|
+
blank screen (`SITE.md`, "The play page"). Its words and picture are the landing's, so give the landing its
|
|
372
|
+
`pitch`, `controls` and a hero still. A game that keeps loading after the room's first state (models,
|
|
373
|
+
textures, a baked world) passes `arrival: 'game'` to `openRoom`, calls `net.loading(p, 'the heroes')`
|
|
374
|
+
while it loads and `net.playable()` once its world and the player's own body are drawn, so nobody sees
|
|
375
|
+
stand-ins (NETPLAY.md section 21); the starters do. `perf` measures it: `load.look` (the first meaningful
|
|
376
|
+
frame), `load.playable` (control-ready: seated, a body, the card gone) and `load.ready` (the game's own
|
|
377
|
+
`net.playable()`), with the arrival mode beside them. `check` says seated, ready (the card lifted) and connected
|
|
378
|
+
apart: a seat is not ready, and a finished round is not an uninterrupted one.
|
|
379
|
+
|
|
380
|
+
**The play page's room button** (Invite, Big screen, the room code) sits top right, with the Chat pill
|
|
381
|
+
beside it. If the game draws a score, a timer or a bar there, move them in game.json: `"screen": { "share":
|
|
382
|
+
{ "desk": "bottom-left", "phone": { "at": "top-left", "y": 56 } } }` (a corner or `top-center`, per device:
|
|
383
|
+
`desk`, `phone`, `sideways`; `x` / `y` move it in, in pixels; `"label": false` keeps both small icons). Room
|
|
384
|
+
chat's strip of new lines moves with `"screen": { "chat": … }` (above). Look at `/<id>/play` on a computer and
|
|
385
|
+
a phone, both ways up, while a round is on and somebody says something in chat.
|
|
386
|
+
|
|
387
|
+
Then `npm run build`, `npm run dev`, and look at `http://127.0.0.1:8787/<id>/` as a stranger would:
|
|
388
|
+
a computer (1440 wide) and a phone (390 wide, and turned sideways), from the top, scrolling to the end.
|
|
389
|
+
The `playtest` skill takes the screenshots. The hero reads at a glance, Play is above the fold on a phone,
|
|
390
|
+
nothing is cut off or runs off the side. Fix what you see; build again.
|
|
391
|
+
|
|
392
|
+
A whole landing of the studio's own (`site/pages/<id>/index.html`) replaces the generated one: only when
|
|
393
|
+
the person asks for a hand-made page, and start from the generated one's HTML so Play, the TV road and
|
|
394
|
+
"Made with Homie" stay.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {readFileSync,readdirSync,existsSync} from 'node:fs';
|
|
2
|
+
import {join} from 'node:path';
|
|
3
|
+
import {homedir} from 'node:os';
|
|
4
|
+
import {STUDIO_VERSION} from './version.mjs';
|
|
5
|
+
const json=file=>{try{return JSON.parse(readFileSync(file,'utf8'));}catch{return null;}};
|
|
6
|
+
const older=(a,b)=>{const x=a.split('.').map(Number),y=b.split('.').map(Number);for(let i=0;i<3;i++){if(x[i]!==y[i])return x[i]<y[i];}return false;};
|
|
7
|
+
export function authoringGuidance({home=homedir(),version=STUDIO_VERSION}={}) {
|
|
8
|
+
const installed=[];
|
|
9
|
+
const claude=json(join(home,'.claude/plugins/installed_plugins.json'));
|
|
10
|
+
for(const row of claude?.plugins?.['homie@homie']??[]) {
|
|
11
|
+
const v=row.version??json(join(row.installPath??'','.claude-plugin/plugin.json'))?.version;
|
|
12
|
+
if(v)installed.push({client:'Claude Code',version:v,update:'claude plugin update homie@homie'});
|
|
13
|
+
}
|
|
14
|
+
// Codex's plugin cache is versioned. Report older copies explicitly, including
|
|
15
|
+
// one still loaded by a long-lived session after another copy was installed.
|
|
16
|
+
const cache=join(home,'.codex/plugins/cache/homie/homie');
|
|
17
|
+
if(existsSync(cache))for(const entry of readdirSync(cache)) {
|
|
18
|
+
const v=json(join(cache,entry,'.codex-plugin/plugin.json'))?.version;
|
|
19
|
+
if(v)installed.push({client:'Codex',version:v,update:'Run codex plugin marketplace upgrade homie && codex plugin add homie@homie, then start a new session'});
|
|
20
|
+
}
|
|
21
|
+
const guides='node_modules/@homie-rocks/studio/guides/game/';
|
|
22
|
+
return {version,guides,warning:installed.filter(p=>/^\d+\.\d+\.\d+$/.test(p.version)&&older(p.version,version)).map(p=>`Homie ${p.client} plugin ${p.version} is older than toolkit ${version}. Its guidance may be stale. Read ${guides}{SKILL,RULES,REWRITE}.md. ${p.update}.`)};
|
|
23
|
+
}
|
|
24
|
+
export function authoringWords(options) {
|
|
25
|
+
const g=authoringGuidance(options);
|
|
26
|
+
return [`Author against toolkit ${g.version}: ${g.guides}{SKILL,RULES,REWRITE}.md`,...g.warning];
|
|
27
|
+
}
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {workerEntry} from './worker-build.mjs';
|
|
1
2
|
import { refreshWorkerConfig } from './worker-config.mjs';
|
|
2
3
|
import { paidReleases, uploadPaidParts } from './parts-upload.mjs';
|
|
3
4
|
/**
|
|
@@ -381,14 +382,14 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
|
|
|
381
382
|
if(subscriptions.code!==0)return refuse('Could not register studio functions',subscriptions.out);
|
|
382
383
|
|
|
383
384
|
const started = Date.now();
|
|
384
|
-
let dep = w(['deploy', ...repoVar(root)]);
|
|
385
|
+
let dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
|
|
385
386
|
if (dep.code !== 0 && explainCloudflare(dep.out, accountId)?.needs === 'workers-dev-subdomain') {
|
|
386
387
|
const headers = await cloudflareAuth({ env: { ...process.env, ...projectsCloudflareEnv(root) }, bin: wranglerBin(root), cwd: root,
|
|
387
388
|
exec: async (_bin, args) => { const r = w(args, { cwd: root }); return { code: r.code, stdout: r.stdout }; } });
|
|
388
389
|
const address = await registerWorkersAddress({ accountId, slug: studio.slug, headers, ...(fetchFn ? { fetchFn } : {}) });
|
|
389
390
|
if (!address.ok) return { ok: false, command: 'deploy', needs: 'workers-dev-subdomain', why: address.next };
|
|
390
391
|
step(address.created ? 'registered the account’s workers.dev address automatically' : 'kept the account’s existing workers.dev address');
|
|
391
|
-
dep = w(['deploy', ...repoVar(root)]);
|
|
392
|
+
dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
|
|
392
393
|
}
|
|
393
394
|
if (dep.code !== 0) return refuse(`wrangler deploy failed: ${dep.out.trim().split('\n').slice(-6).join(' ')}`, dep.out);
|
|
394
395
|
if (paidReleases(root).length) {
|
|
@@ -607,7 +608,7 @@ export async function ciDeploy(root, { log = () => {} } = {}) {
|
|
|
607
608
|
} catch { step('could not add the Workers AI binding to wrangler.jsonc: the guides answer from the game\'s script'); }
|
|
608
609
|
}
|
|
609
610
|
const started = Date.now();
|
|
610
|
-
const dep = w(['deploy', ...repoVar(root)]);
|
|
611
|
+
const dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
|
|
611
612
|
if (dep.code !== 0) return refuse(`wrangler deploy failed: ${dep.out.trim().split('\n').slice(-6).join(' ')}`, dep.out);
|
|
612
613
|
step(`deployed ${names.worker ?? 'the Worker'} in ${Math.round((Date.now() - started) / 1000)} s`);
|
|
613
614
|
const repo = repoOf(root);
|
package/lib/dev.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {workerEntry} from './worker-build.mjs';
|
|
1
2
|
import { networkInterfaces } from 'node:os';
|
|
2
3
|
/**
|
|
3
4
|
* `homie-studio dev` and `dev --stop`: the whole site on this computer (pages, rooms in a local Durable Object, D1),
|
|
@@ -194,10 +195,11 @@ export function devConfig(root, remoteAi) {
|
|
|
194
195
|
const stripped = routesOf(json).map((r) => r.pattern);
|
|
195
196
|
const routed = ROUTE_KEYS.some((k) => k in json);
|
|
196
197
|
const aiNote = remoteAi ? 'AI guides think with Workers AI (remote, billed to the signed-in account).' : null;
|
|
197
|
-
|
|
198
|
+
const entry=workerEntry(root);
|
|
199
|
+
if (!entry && Boolean(json.ai) === remoteAi && !routed) return { args: [], note: aiNote, stripped: [] };
|
|
198
200
|
const abs = (p) => (p && !p.startsWith('/') ? join(base, p) : p);
|
|
199
201
|
delete json.$schema;
|
|
200
|
-
json.main = abs(json.main);
|
|
202
|
+
json.main = entry ?? abs(json.main);
|
|
201
203
|
if (json.assets?.directory) json.assets.directory = abs(json.assets.directory);
|
|
202
204
|
json.d1_databases = (json.d1_databases ?? []).map((d) => ({ ...d, ...(d.migrations_dir ? { migrations_dir: abs(d.migrations_dir) } : {}) }));
|
|
203
205
|
for (const k of ROUTE_KEYS) delete json[k];
|
|
@@ -236,7 +236,8 @@ function play(c, companions, allow) {
|
|
|
236
236
|
const ends = (fd) => (fd.t === 'bit' || fd.t === 'press' ? [false, true] : fd.t === 'fix' ? [-1, 1] : [L.P.coerce(fd, -Infinity, c.dims), L.P.coerce(fd, Infinity, c.dims)]);
|
|
237
237
|
const steering = new Map();
|
|
238
238
|
let moveTick = 0;
|
|
239
|
-
|
|
239
|
+
let moveGeometry = c.map;
|
|
240
|
+
const moveCtx = new Map(c.kinds.filter((k) => k.move && k.body).map((k) => [k.name, L.M.moveContext({ tick: () => moveTick, tickHz, tune: c.publicTune, map: c.map, geometry: () => moveGeometry, name: c.map.name, spots: c.map.spots, radius: () => k.body.radius, shape: () => k.body, dims: c.dims })]));
|
|
240
241
|
/**
|
|
241
242
|
* An owner-moved body's own browser, for one tick: the game's `move` from where the last snapshot has the body, by
|
|
242
243
|
* the step a browser takes (pack.ts `stepMove`), and the claim of where that leaves it. Its units are the play's.
|
|
@@ -344,9 +345,9 @@ function play(c, companions, allow) {
|
|
|
344
345
|
const stats = A.core.stats; const cut = stats.ticksCut; const held = stats.held;
|
|
345
346
|
A.tickNow();
|
|
346
347
|
units += A.core.stats.tickUnits;
|
|
347
|
-
for (const [name, tick, input, before, after] of movesToCheck.splice(0)) {
|
|
348
|
+
for (const [name, tick, input, before, after, geometry] of movesToCheck.splice(0)) {
|
|
348
349
|
const kind = c.kinds.find(k => k.name === name);
|
|
349
|
-
moveTick = tick;
|
|
350
|
+
moveTick = tick; moveGeometry = geometry ?? c.map;
|
|
350
351
|
const result = L.P.stepMove(kind.move, before, input, moveCtx.get(name), Math.max(1, Math.floor(budget / 4)), kind.motion, c.dims,
|
|
351
352
|
error => { fault ??= `${siteOf(name, 'move')} ${name}.move: prediction failed: ${error.message}.${when()}`; });
|
|
352
353
|
units += result.used;
|
package/lib/rules-guard.mjs
CHANGED
|
@@ -69,7 +69,8 @@ export const HOOK_NAMES = ['valueOf', 'toString', 'toJSON'];
|
|
|
69
69
|
export const WORLD_METHODS = ['ticks', 'random', 'send', 'sendRoom', 'announce', 'sendArea', 'after', 'emit', 'spawn', 'despawn', 'place', 'near', 'inBox', 'ray', 'sweep', 'ask', 'goalDone', 'finish', 'end', 'spot', 'spots'];
|
|
70
70
|
export const MATH_METHODS = ['sin', 'cos', 'tan', 'atan', 'atan2', 'asin', 'acos', 'exp', 'log', 'pow', 'hypot', 'rad', 'deg', 'clamp', 'lerp', 'vec', 'add', 'sub', 'scale', 'dot', 'cross', 'len', 'dist', 'norm', 'clampLen', 'lerpVec', 'dir', 'angle'];
|
|
71
71
|
const VALUE_METHODS = new Set([...ARRAY_METHODS, ...STRING_METHODS, ...MAPSET_METHODS]);
|
|
72
|
-
const
|
|
72
|
+
export const MOVE_METHODS = ['support', 'overlaps'];
|
|
73
|
+
const CALLABLE = new Set([...VALUE_METHODS, ...WORLD_METHODS, ...MOVE_METHODS, ...MATH_METHODS]);
|
|
73
74
|
const refusedName = (k) => REFUSED_NAMES.includes(k) || k.startsWith('toLocale');
|
|
74
75
|
/** Operators that turn each operand into a number or a text. (`+` is handled beside them; `==` and `!=` are refused.) */
|
|
75
76
|
const CONVERTING = new Set(['-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>', '<', '>', '<=', '>=']);
|
package/lib/scaffold.mjs
CHANGED
|
@@ -68,14 +68,14 @@ export function slugify(name) {
|
|
|
68
68
|
* No `database_id` until one is known: Wrangler (4.45.0+) and the Deploy to Cloudflare flow create the database
|
|
69
69
|
* the binding names and keep it linked.
|
|
70
70
|
*/
|
|
71
|
-
export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layout = 'root', ai = false, routes = null, paidParts = false, partsRateLimit = null, triggers = null }) {
|
|
71
|
+
export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layout = 'root', main = null, ai = false, routes = null, paidParts = false, partsRateLimit = null, triggers = null }) {
|
|
72
72
|
const at = layout === 'site' ? { schema: '../node_modules', main: 'src/runtime/worker.js', dist: './dist', migrations: 'migrations' }
|
|
73
73
|
: { schema: 'node_modules', main: 'site/src/runtime/worker.js', dist: './site/dist', migrations: 'site/migrations' };
|
|
74
74
|
const rooms = [{ name: 'TABLE', class_name: 'Table' }, { name: 'LOBBY', class_name: 'Lobby' }];
|
|
75
75
|
const config = {
|
|
76
76
|
$schema: `${at.schema}/wrangler/config-schema.json`,
|
|
77
77
|
name: worker,
|
|
78
|
-
main: at.main,
|
|
78
|
+
main: main ?? at.main,
|
|
79
79
|
compatibility_date: COMPAT_DATE,
|
|
80
80
|
// The site's own fetches (its directory claim, "played this week") go out as any browser's would, so a studio
|
|
81
81
|
// whose directory is on the same zone (a house studio on *.homie.rocks, or two Workers on one workers.dev
|
|
@@ -122,6 +122,13 @@ the Table/Lobby Durable Objects, all on Cloudflare's free Workers plan), built
|
|
|
122
122
|
from \`@homie-rocks/studio\`, pinned in \`package.json\`. The homie.rocks
|
|
123
123
|
directory lists its games; homie.rocks does not host them.
|
|
124
124
|
|
|
125
|
+
## Version-matched authoring guides
|
|
126
|
+
|
|
127
|
+
Before authoring or rewriting a game, read \`node_modules/@homie-rocks/studio/guides/game/SKILL.md\`,
|
|
128
|
+
\`RULES.md\` and \`REWRITE.md\` in that same folder. These ship with the installed toolkit;
|
|
129
|
+
use them when an installed plugin describes an older API. \`homie-studio check\` reports older
|
|
130
|
+
plugin copies and the update step. After upgrading the plugin, start a new session.
|
|
131
|
+
|
|
125
132
|
## Layout
|
|
126
133
|
|
|
127
134
|
| Path | What it is |
|