@homie-rocks/studio 0.22.0 → 0.24.0
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 +61 -1
- package/README.md +59 -0
- package/bin/homie-studio.mjs +68 -2
- package/chat/CHAT.md +101 -0
- package/chat/OWNERS.md +78 -0
- package/lib/build.mjs +20 -2
- package/lib/chat-cli.mjs +89 -0
- package/lib/cloudflare.mjs +5 -2
- package/lib/scaffold.mjs +59 -2
- package/lib/shop.mjs +270 -0
- package/lib/template-history.json +77 -0
- package/netplay/NETPLAY.md +194 -4
- package/netplay/netplay.ts +156 -3
- package/package.json +7 -1
- package/port/index.ts +3 -2
- package/port/view.ts +238 -0
- package/shop/SELLING.md +83 -0
- package/shop/SHOP.md +154 -0
- package/shop/shop.ts +130 -0
- package/site/SITE.md +22 -1
- package/starters/ember-vale/src/main.ts +22 -2
- package/starters/gem-rush/src/main.ts +21 -1
- package/starters/gem-rush-3d/src/main.ts +22 -1
- package/worker/account-page.mjs +14 -2
- package/worker/agents.mjs +3 -3
- package/worker/chat-page.mjs +556 -0
- package/worker/chat-store.mjs +161 -0
- package/worker/chat-words.mjs +46 -0
- package/worker/chat.mjs +419 -0
- package/worker/index.mjs +189 -7
- package/worker/office-page.mjs +120 -5
- package/worker/office.mjs +173 -6
- package/worker/pages.mjs +25 -8
- package/worker/players.mjs +8 -0
- package/worker/referrals.mjs +328 -0
- package/worker/room.mjs +291 -11
- package/worker/servers.mjs +5 -1
- package/worker/shop-page.mjs +532 -0
- package/worker/shop-rules.mjs +277 -0
- package/worker/shop-store.mjs +217 -0
- package/worker/shop.mjs +686 -0
- package/worker/site.mjs +4 -1
- package/worker/stats.mjs +4 -1
- package/worker/stripe.mjs +149 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,69 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.24.0] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
**Plugin 0.25.0** · [#34](https://github.com/homie-rocks/homie/pull/34)
|
|
23
|
+
|
|
24
|
+
Sell things in your games with your own Stripe account: a supporter pack, cosmetics, a pass, an unlock or a tip, in real money, with refunds from your office and the kids rules built in; Homie never sees the money.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- The shop: `shop.json` at your studio's root lists what you sell (`cosmetic`, `supporter`, `pass`, `unlock`, `tip`), each in real money with the entitlement keys your game reads. `homie-studio shop init --supporter` writes a US$5 Supporter pack (a badge on the player's profile and beside their name in rooms, for a year) and `SELLING.md`, plain words on what selling makes you responsible for (refunds, disputes, tax, kids; not legal advice).
|
|
29
|
+
- Checkout is Stripe's own hosted page, opened by your studio's Worker with your own restricted key, one item at a time. Stripe Tax is on by default; Stripe Managed Payments (Stripe as the seller of record: it registers, files and pays the tax and answers disputes, for 3.5% more) is one switch, `"till": "stripe-managed"`. Stripe's receipt email is the receipt.
|
|
30
|
+
- A signed webhook (`/api/shop/hook`: Stripe's signature, within five minutes, each event once) is the only thing that marks an order paid, refunded or disputed. What a player owns is tied to their passkey account and follows them to every device.
|
|
31
|
+
- `homie-studio shop connect`: a page on your own computer where you paste the restricted key and the webhook secret. It says exactly what to make in Stripe, offers Managed Payments in plain words, and puts both straight into your Worker's secrets: never the chat, a file or a log. Test keys only unless `--live`.
|
|
32
|
+
- In a game: `createShop()` from `@homie-rocks/studio/shop` gives `shop.has('skin:ember')`, `shop.entitlements()`, `shop.on('change')`, `shop.open(item)` and `shop.used(key)`. The play page has a store sheet (from the game, or Shop in the room sheet) that opens Stripe in a new tab while the game keeps running; on a television it shows only a code to buy on a phone.
|
|
33
|
+
- A supporter's badge rides on their seat in every room (`peer.badge`, set by your Worker from what the account owns; a hello can never claim one), and their account page lists their purchases and badges.
|
|
34
|
+
- Refunds: one tap in your office (`/_studio/office/shop`), and a player's own refund of an unused item within your refund window (at least 14 days). Your AI can only ask for a refund (`homie-studio shop refund <order>`), and you confirm it with one tap.
|
|
35
|
+
- The office's shop page: the last 30 days, every order with its Stripe page, disputes, and payouts, balance and tax as links to your own Stripe Dashboard, plus a CSV for your accountant with no names in it.
|
|
36
|
+
- Referrals: a `?via=` link from another site (another studio, or homie.rocks on the same terms) is remembered on a new player's first visit; a kept sale owes that referrer the rate in `shop.json`. `homie-studio shop statements` signs each referrer's monthly statement with your studio's key; the referrer checks it against your manifest and invoices you. Nothing moves through Homie.
|
|
37
|
+
- New D1 migration `0008_studio_shop.sql`: orders, entitlements, the age band, parent links, webhook events and referral books. No card, no address, no email.
|
|
38
|
+
- The kids and fairness rules are built in, not settings. No shop on a kids server, and none at all in a studio whose `studio.json` says `"audience": "kids"`.
|
|
39
|
+
- Spending is off on every account until one neutral question (the year you were born, no default) says adult; the answer is kept only as adult, teen or child. Under 13: nothing, ever. 13 to 17: a one-time link a parent opens on their own phone and pays in their own name.
|
|
40
|
+
- Nothing random for money, no gems or coins, no countdown offers, and nothing that changes play on a beginner server (`"advantage": true` items are not sold or counted there). `shop check` and every build refuse a `shop.json` that breaks these. A monthly cap per player (US$50 at most).
|
|
41
|
+
- A card dispute never locks or deletes an account: nothing changes while it is open, and a lost one takes back only that item.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Deleting a player account that owns things asks first, and keeps the orders without the player. A player's export includes their orders.
|
|
46
|
+
- The directory manifest says whether the shop is open and its till, and how this studio takes referral statements (with the public half of its statement key). Never a key or a sale.
|
|
47
|
+
|
|
48
|
+
### Upgrade notes
|
|
49
|
+
|
|
50
|
+
- `homie-studio upgrade --apply`, then `npm run deploy` applies migration 0008. Nothing is sold until you add `shop.json` and connect a key.
|
|
51
|
+
## [0.23.0] - 2026-10-03
|
|
52
|
+
|
|
53
|
+
**Plugin 0.24.0** · [#33](https://github.com/homie-rocks/homie/pull/33) · [release-2026-10-03-studio-0.23.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.23.0)
|
|
54
|
+
|
|
55
|
+
Room chat in every game: reactions that float up every screen, quick lines, typing where the rules allow, speech bubbles over characters, rules per game and server, moderation in your own Worker, and the owner's tools.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Room chat on every game's play page, with no game code: a Chat pill beside the room button (the corner `screen.share` keeps clear of the game's HUD) and its sheet: the room's last minutes, the five reactions (fire, clap, laugh, heart, wow, in homie.rocks's order, plus up to three of the game's own), the game's quick lines, typing where the rules allow it (and why not where they do not), and Report on a line. New lines show for a moment where the status chip sits (game.json `"screen": { "chat": … }` moves them).
|
|
60
|
+
- Reactions float up every screen in the room, as on the television: the players', the watchers' and the big screen's (at most six a second, eighteen at once). The big screen (`/<game>/tv`) shows the room's lines in a corner, and the watch page has a Chat button and panel. Measured in Chrome with four browsers in one room: a reaction is on the other screens within about 15 ms of the press locally.
|
|
61
|
+
- Speech bubbles over characters: the port kit's `createBubbles` and `paintBubbles` (beside `createLabels`), and the helper's `net.on('say')`. Gem Rush, Ember Vale and Gem Rush 3D draw what a player says over their name. Each player chooses "Show my messages over my character" and "Show chat on this screen".
|
|
62
|
+
- Rules per game and per server: game.json `"chat"` sets a game's defaults (off, emoji, quick lines or typing; who may type and who may react: anyone, signed in with a passkey, or members; slow mode, length, links, swears, the review, bubbles, watchers, homie.rocks), and the owner overrides them in the office or with `homie-studio chat rules`. A kids server and every beginner server keep chat to emoji and quick lines. By default emoji and quick lines are open to everyone and typing needs a signed-in player account.
|
|
63
|
+
- Moderation in the studio's own Worker before a typed line reaches anyone: a built-in word list and patterns (slurs, sexual words, self-harm, personal questions such as a player's age or where they live, contact details and other apps, links, swears) plus your own words, then a review on your own Workers AI with Cloudflare's Clef decision model (`@cf/cloudflare/clef-flash`), within a daily budget of 2,000 neurons (about 850 typed messages; inside the free allocation). Emoji and quick lines are never reviewed. When the review cannot answer, the word list decides alone. `chat/CHAT.md` says why Clef and what the alternatives cost.
|
|
64
|
+
- The owner's tools: every live room's last minutes in the office with Remove (from every screen), Mute and Kick from a line (a watcher too, their lines taken down with them), the same three in the owner's own game, players' reports with Dismiss, the review's day, and an announcement as a studio line in the chat. `homie-studio chat` (rules, rooms, reports), `chat rules`, `chat remove`, `chat budget` and `chat words`.
|
|
65
|
+
- A studio's `/api/rooms` says which rooms let homie.rocks show their chat (`chat: true`), so homie.rocks's room cards can show a room's chat and emoji live from the room's own socket (the hub's side ships with homie.rocks). Turn it off per game or server with the `hub` rule.
|
|
66
|
+
- `chat/OWNERS.md`: a plain note for studio owners on what the chat tooling does, what is stored and for how long, and children's data (not legal advice).
|
|
67
|
+
- Netplay revision 8 (NETPLAY.md section 19): `say` and `react` frames up, `line`, `react`, `lines`, `unline` and `held` down, `policy.chat`, and the owner's `unsay`; the helper's `net.say`, `net.sayLine`, `net.react`, `net.chatRules` and `on('chat' | 'say' | 'unchat' | 'held')`.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `homie-studio deploy` binds Workers AI when a game lets its players type (the review), not only when an AI guide thinks with it.
|
|
72
|
+
- `homie-studio dev --remote-ai` works again: Wrangler's `--local` turned every remote binding off.
|
|
73
|
+
|
|
74
|
+
### Upgrade notes
|
|
75
|
+
|
|
76
|
+
- `homie-studio upgrade --apply` (or the next deploy) adds migration `0007_studio_chat.sql`: the owner's chat rules and players' reports. The chat itself is never stored.
|
|
77
|
+
- Games get the chat panel, the float and the ticker as soon as the studio deploys this version. Rebuild a game to draw speech bubbles (its netplay helper is revision 8).
|
|
78
|
+
- Typing needs a signed-in player account by default. To let guests type, or to keep a game to emoji and quick lines, set game.json `"chat"` or use `homie-studio chat rules`.
|
|
79
|
+
|
|
20
80
|
## [0.22.0] - 2026-10-02
|
|
21
81
|
|
|
22
|
-
**Plugin 0.23.0** · [#32](https://github.com/homie-rocks/homie/pull/32)
|
|
82
|
+
**Plugin 0.23.0** · [#32](https://github.com/homie-rocks/homie/pull/32) · [release-2026-10-03-studio-0.22.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.22.0)
|
|
23
83
|
|
|
24
84
|
Your game's look as decisions you can steer and lock, a style board the engine draws for free, free CC0 3D models, props made on your own fal account, and every asset licensed and checked for phones.
|
|
25
85
|
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ npx homie-studio publish # the homie.rocks directo
|
|
|
26
26
|
npx homie-studio stats # the studio's own numbers, for its owner
|
|
27
27
|
npx homie-studio office # who is playing now, in every live room (the back office)
|
|
28
28
|
npx homie-studio servers # each game's servers: humans-only, hybrid AI seats, beginner guides
|
|
29
|
+
npx homie-studio chat # room chat: each game's rules, the rooms' last minutes, reports
|
|
29
30
|
npx homie-studio upgrade # what a newer template adds to this studio (--apply to take it)
|
|
30
31
|
```
|
|
31
32
|
|
|
@@ -286,6 +287,64 @@ npx homie-studio agents pass night-rush --label Claude
|
|
|
286
287
|
- Everything is in the studio's own Worker and D1 (migration `0006_studio_servers.sql`). The site's side is in
|
|
287
288
|
[site/SITE.md](site/SITE.md); the room's in [netplay/NETPLAY.md](netplay/NETPLAY.md) sections 17 and 18.
|
|
288
289
|
|
|
290
|
+
## Room chat
|
|
291
|
+
|
|
292
|
+
Every game has room chat (0.23.0), with no game code: reactions that float up every screen in the room (players,
|
|
293
|
+
watchers, the big screen), the game's quick lines, and typing where the room's rules allow it. It is the Homie app's
|
|
294
|
+
live-room chat and homie.rocks's room chat in a studio's own game: the same five reactions in the same order, the
|
|
295
|
+
same message shapes, the same float on a television.
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
npx homie-studio chat rules night-rush --mode lines # emoji and quick lines only
|
|
299
|
+
npx homie-studio chat rules night-rush --who anyone --slow 5 # guests may type, one line every 5 s
|
|
300
|
+
npx homie-studio chat rules night-rush --server first-light --mode emoji
|
|
301
|
+
npx homie-studio chat # rules, every room's last minutes, reports
|
|
302
|
+
npx homie-studio chat remove night-rush pub-3 <line id> # take a line down on every screen
|
|
303
|
+
npx homie-studio chat words # the built-in word list the floor holds
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- **The play page:** a Chat pill beside the room button (the corner `screen.share` keeps clear of the HUD), its
|
|
307
|
+
sheet (the last minutes, the reactions, the quick lines, typing, Report), a short ticker of new lines, and two
|
|
308
|
+
switches each player keeps: "Show my messages over my character" and "Show chat on this screen". The big screen
|
|
309
|
+
shows the lines in a corner; the watch page has a Chat button; homie.rocks shows a room's chat on its card.
|
|
310
|
+
- **Rules** per game (game.json `"chat"`) and per server, in the office or with `chat rules`: off, emoji, quick
|
|
311
|
+
lines or typing; who may type and who may react (anyone, signed in with a passkey, members); slow mode, length,
|
|
312
|
+
links, swears, the review, bubbles, watchers, homie.rocks. Default: emoji and quick lines for anyone, typing for
|
|
313
|
+
signed-in players. A kids server and every beginner server keep chat to emoji and quick lines.
|
|
314
|
+
- **Moderation, in the studio's own Worker, before anyone sees a typed line:** a built-in word list and patterns
|
|
315
|
+
(slurs, sexual words, self-harm, personal questions and contact details, links, swears, your own words), then
|
|
316
|
+
Cloudflare's **Clef** decision model on the studio's own Workers AI (`@cf/cloudflare/clef-flash`, about 2.3
|
|
317
|
+
neurons a message measured, 2,000 a day by default: inside the free allocation). Emoji and quick lines are never reviewed.
|
|
318
|
+
- **The owner:** every live room's last minutes in the office with Remove, Mute and Kick (from a line, with their
|
|
319
|
+
lines taken down), the same in the owner's own game, the reports players make, and Announce (a studio line).
|
|
320
|
+
- **Speech bubbles:** `net.on('say')` and the port kit's `createBubbles` / `paintBubbles`; Gem Rush, Ember Vale and
|
|
321
|
+
Gem Rush 3D draw them over the speaker.
|
|
322
|
+
- **Nothing is stored** but a report (the one message, 30 days): the room keeps 15 minutes in memory.
|
|
323
|
+
[chat/CHAT.md](chat/CHAT.md) is the short version and why Clef; [chat/OWNERS.md](chat/OWNERS.md) a plain note for
|
|
324
|
+
studio owners on chat and children's data; NETPLAY.md section 19 the wire.
|
|
325
|
+
|
|
326
|
+
## Selling things: the shop
|
|
327
|
+
|
|
328
|
+
A studio sells items for its games (0.24.0) with **its own Stripe account**: the studio is the seller, money goes
|
|
329
|
+
straight from players to its Stripe, and homie.rocks never sees, holds or moves it (Homie takes no cut; no shared
|
|
330
|
+
currency; not a Connect platform). Real-money prices only, nothing random, the kids rules built in (no shop on a kids
|
|
331
|
+
server, spending off until a neutral age question says adult, nothing for under-13s, 13-17 through a parent's own
|
|
332
|
+
checkout, a monthly cap), and the television only shows a code to buy on a phone.
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
npx homie-studio shop init --supporter # shop.json with a US$5 Supporter pack, and SELLING.md (the owner's plain words)
|
|
336
|
+
npx homie-studio shop check # the kit's rules; every build checks them too
|
|
337
|
+
npx homie-studio shop connect # a page on THIS computer: the owner pastes a restricted key and the webhook secret
|
|
338
|
+
npx homie-studio shop # open (test or live) or what is missing
|
|
339
|
+
npx homie-studio shop refund ord_… # an ASK: the owner taps once (or Refund in /_studio/office/shop)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`); a
|
|
343
|
+
supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), Stripe Tax on, or Stripe Managed
|
|
344
|
+
Payments (Stripe as seller of record, 3.5% more) as one switch; a signed, idempotent webhook; refunds from the
|
|
345
|
+
office; a dispute never touches the account; referral statements signed per referrer (homie.rocks is one more
|
|
346
|
+
referrer). The guide: `shop/SHOP.md`.
|
|
347
|
+
|
|
289
348
|
## Setup status
|
|
290
349
|
|
|
291
350
|
```sh
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
* tap in their own browser, from the link this prints)
|
|
101
101
|
* homie-studio office revoke (every office key, play ticket and pending ask ends)
|
|
102
102
|
*
|
|
103
|
+
* homie-studio chat [--game <id>] room chat (NETPLAY.md section 19): each game's rules, every live room's last
|
|
104
|
+
* minutes, players' reports, the review's day; chat rules <game> [--server <id>]
|
|
105
|
+
* --mode off|emoji|lines|text --who anyone|signed-in|members … | --reset;
|
|
106
|
+
* chat remove <game> <room> <line id>; chat budget <neurons>; chat words
|
|
103
107
|
* homie-studio servers [--game <id>] every server of every game (worker/servers.mjs): its policy, door, the AI's
|
|
104
108
|
* level, live rooms and AI, members, and builds that predate servers
|
|
105
109
|
* homie-studio servers new <game> "<Name>" --policy open|humans-only|hybrid|beginner [--ai <n>] [--guides <n>]
|
|
@@ -117,6 +121,16 @@
|
|
|
117
121
|
* time AI guides may talk are ASKED for, like office kick; --budget is
|
|
118
122
|
* Workers AI neurons a day, or dollars a day for the owner's key)
|
|
119
123
|
* homie-studio agents brain key [--remove] (the owner's AI key, typed into a page on this computer only)
|
|
124
|
+
* homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
|
|
125
|
+
* homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
|
|
126
|
+
* homie-studio shop check shop.json against the kit's rules (real money, nothing random, the kids rules)
|
|
127
|
+
* homie-studio shop connect [--managed] [--live] a page on THIS computer where the owner pastes the studio's
|
|
128
|
+
* restricted Stripe key and webhook secret: straight to the Worker secrets,
|
|
129
|
+
* never a chat or a file (test keys only unless --live)
|
|
130
|
+
* homie-studio shop disconnect both secrets gone: the shop closes
|
|
131
|
+
* homie-studio shop orders the latest orders (never a card or an email)
|
|
132
|
+
* homie-studio shop refund <order> [--reason …] [--note "<why>"] ASKS the owner (a one-tap link)
|
|
133
|
+
* homie-studio shop statements [--period YYYY-MM] [--send] signed referral statements this studio owes
|
|
120
134
|
* homie-studio agents sit <game> [--server <id>] [--pass hap_…] [--label Claude]
|
|
121
135
|
* (an AI guide's seat from this terminal: then lines of `do <goal> {args}`,
|
|
122
136
|
* `say <line> {args}`, `look`, `stand`)
|
|
@@ -219,8 +233,10 @@ import { STUDIO_VERSION } from '../lib/version.mjs';
|
|
|
219
233
|
import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
|
|
220
234
|
import { playersOwner, playersShow } from '../lib/players.mjs';
|
|
221
235
|
import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
|
|
236
|
+
import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords } from '../lib/chat-cli.mjs';
|
|
222
237
|
import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
|
|
223
238
|
import { AgentSeat } from '../lib/agent-seat.mjs';
|
|
239
|
+
import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
|
|
224
240
|
import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
|
|
225
241
|
import { formatStatus, setupStatus } from '../lib/doctor.mjs';
|
|
226
242
|
import { codexTarget, newCodex, writeCodexPage } from '../lib/codex.mjs';
|
|
@@ -233,7 +249,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
233
249
|
|
|
234
250
|
const argv = process.argv.slice(2);
|
|
235
251
|
const flags = new Map();
|
|
236
|
-
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged'];
|
|
252
|
+
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send'];
|
|
237
253
|
const positional = [];
|
|
238
254
|
for (let i = 0; i < argv.length; i++) {
|
|
239
255
|
const a = argv[i];
|
|
@@ -426,6 +442,30 @@ function print(result) {
|
|
|
426
442
|
case 'servers':
|
|
427
443
|
lines.push(...serversLines(result));
|
|
428
444
|
break;
|
|
445
|
+
case 'chat':
|
|
446
|
+
lines.push(...chatLines(result));
|
|
447
|
+
break;
|
|
448
|
+
case 'chat words':
|
|
449
|
+
for (const [k, v] of Object.entries(result.groups)) lines.push(`${k}: ${v.join(', ')}`);
|
|
450
|
+
lines.push(result.note);
|
|
451
|
+
break;
|
|
452
|
+
case 'chat remove':
|
|
453
|
+
lines.push(result.message);
|
|
454
|
+
break;
|
|
455
|
+
case 'shop':
|
|
456
|
+
case 'shop orders':
|
|
457
|
+
case 'shop check':
|
|
458
|
+
case 'shop init':
|
|
459
|
+
case 'shop statements':
|
|
460
|
+
case 'shop connect':
|
|
461
|
+
case 'shop disconnect':
|
|
462
|
+
lines.push(...shopLines(result));
|
|
463
|
+
break;
|
|
464
|
+
case 'chat rules':
|
|
465
|
+
case 'chat budget':
|
|
466
|
+
case 'shop refund':
|
|
467
|
+
lines.push(result.asked ? `Asked: ${result.what}` : result.message, ...(result.asked ? [`Owner's one-tap link (until the ask ends, 15 min): ${result.link}`, result.use] : []));
|
|
468
|
+
break;
|
|
429
469
|
case 'servers new':
|
|
430
470
|
lines.push(result.message, ...(result.notes ?? []).map((n) => ` ${n}`));
|
|
431
471
|
break;
|
|
@@ -590,6 +630,8 @@ async function main() {
|
|
|
590
630
|
if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
|
|
591
631
|
if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template') });
|
|
592
632
|
if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
|
|
633
|
+
// The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
|
|
634
|
+
if (cmd === 'chat' && sub === 'words') return chatWords();
|
|
593
635
|
if (cmd === 'demo') return demoGames();
|
|
594
636
|
if (cmd === 'mcp') {
|
|
595
637
|
// stdout carries MCP messages only from here on (lib/mcp.mjs); every word for a person goes to stderr.
|
|
@@ -706,6 +748,16 @@ async function main() {
|
|
|
706
748
|
if (sub === 'close') return officeClose(root, positional[2], positional[3], { url, minutes: flags.get('minutes'), reopen: flags.has('reopen') });
|
|
707
749
|
if (sub === 'revoke') return officeRevoke(root, { url });
|
|
708
750
|
}
|
|
751
|
+
if (cmd === 'chat') {
|
|
752
|
+
// Room chat (0.23.0, NETPLAY.md section 19): rules, the room's last minutes, reports, the review's budget.
|
|
753
|
+
const url = flags.get('url');
|
|
754
|
+
if (sub === 'words') return chatWords();
|
|
755
|
+
if (!sub) return chatShow(root, { url, game: flags.get('game') });
|
|
756
|
+
if (sub === 'rules') return chatRulesSet(root, positional[2], flags, { url });
|
|
757
|
+
if (sub === 'remove') return chatRemove(root, positional[2], positional[3], positional[4], { url, all: flags.has('all') });
|
|
758
|
+
if (sub === 'budget') return chatBudget(root, positional[2], { url });
|
|
759
|
+
return { ok: false, command: 'chat', why: `unknown: chat ${sub} (rules, remove, budget, words)` };
|
|
760
|
+
}
|
|
709
761
|
if (cmd === 'servers') {
|
|
710
762
|
const url = flags.get('url');
|
|
711
763
|
if (!sub || sub === 'list') return serversList(root, { url, game: flags.get('game') });
|
|
@@ -716,6 +768,18 @@ async function main() {
|
|
|
716
768
|
if (sub === 'member') return serversMember(root, positional[2], positional[3], positional[4], { url, role: flags.get('role'), remove: flags.has('remove') });
|
|
717
769
|
return { ok: false, command: 'servers', why: `unknown: servers ${sub} (list, new, set, close, level, member)` };
|
|
718
770
|
}
|
|
771
|
+
if (cmd === 'shop') {
|
|
772
|
+
const url = flags.get('url');
|
|
773
|
+
if (!sub) return shopStatus(root, { url });
|
|
774
|
+
if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), currency: flags.get('currency') ?? 'usd', price: flags.get('price') ?? 500, managed: flags.has('managed') });
|
|
775
|
+
if (sub === 'check') return shopCheck(root);
|
|
776
|
+
if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), log });
|
|
777
|
+
if (sub === 'disconnect') return shopDisconnect(root);
|
|
778
|
+
if (sub === 'orders') return shopOrders(root, { url });
|
|
779
|
+
if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note') });
|
|
780
|
+
if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), send: flags.has('send') });
|
|
781
|
+
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, orders, refund, statements)` };
|
|
782
|
+
}
|
|
719
783
|
if (cmd === 'agents') {
|
|
720
784
|
const url = flags.get('url');
|
|
721
785
|
if (sub === 'pass') return agentsPass(root, positional[2], { url, label: flags.get('label'), server: flags.get('server'), hands: flags.get('hands'), role: flags.get('role'), days: flags.get('days') });
|
|
@@ -928,7 +992,9 @@ async function dev(root) {
|
|
|
928
992
|
log(`Local site: http://127.0.0.1:${port}/ (each game: http://127.0.0.1:${port}/<id>/play — open it in two browsers)`);
|
|
929
993
|
const ai = devConfig(root, flags.has('remote-ai'));
|
|
930
994
|
if (ai.note) log(ai.note);
|
|
931
|
-
|
|
995
|
+
// --remote-ai: Wrangler's --local turns every remote binding off ("not supported"), so a dev with the real Workers AI
|
|
996
|
+
// (the guides' brains, room chat's review) runs without it; everything else stays local all the same.
|
|
997
|
+
const child = spawn(bin, ['dev', ...(flags.has('remote-ai') ? [] : ['--local']), '--ip', '127.0.0.1', '--port', port, ...ai.args], { cwd: workerDir(root), env, stdio: 'inherit' });
|
|
932
998
|
mkdirSync(dirname(devFile(root)), { recursive: true });
|
|
933
999
|
writeFileSync(devFile(root), `${JSON.stringify({ pid: process.pid, child: child.pid, port: Number(port), at: new Date().toISOString() })}\n`);
|
|
934
1000
|
log(`Stop it with: npx --no-install homie-studio dev --stop (this studio's dev server only)`);
|
package/chat/CHAT.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Room chat
|
|
2
|
+
|
|
3
|
+
Every studio game has room chat from `@homie-rocks/studio` 0.23.0: reactions that float up every
|
|
4
|
+
screen in the room (the players', the watchers', the big screen's), the game's quick lines, and
|
|
5
|
+
typed messages where the room's rules allow them. It feels like the Homie app's live rooms and
|
|
6
|
+
homie.rocks's room chat because it is the same thing: the same five reactions in the same order,
|
|
7
|
+
the same message shapes, the same float on a television.
|
|
8
|
+
|
|
9
|
+
The wire, the rules and the helper are in [NETPLAY.md section 19](../netplay/NETPLAY.md). This
|
|
10
|
+
page is the short version for a game's author, and why the review uses what it uses. For what a
|
|
11
|
+
studio's owner is responsible for, read [OWNERS.md](OWNERS.md).
|
|
12
|
+
|
|
13
|
+
## What a game gets with no work
|
|
14
|
+
|
|
15
|
+
- **The Chat pill** on the play page, beside the room button: the room's last minutes, the
|
|
16
|
+
reactions, the quick lines, typing where allowed, Report on a line, and two switches ("Show my
|
|
17
|
+
messages over my character", "Show chat on this screen").
|
|
18
|
+
- **The float:** every reaction rises up every screen in the room.
|
|
19
|
+
- **The ticker:** a new line shows for a moment where the play page's status chip sits.
|
|
20
|
+
- **The big screen** (`/<game>/tv`) shows the room's lines in a corner; the **watch page** has a
|
|
21
|
+
Chat button; **homie.rocks** shows the room's chat on its card when the studio allows it.
|
|
22
|
+
|
|
23
|
+
## What a game can add
|
|
24
|
+
|
|
25
|
+
**Its rules, in game.json** (the owner can change every one in the office):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
"chat": {
|
|
29
|
+
"mode": "text",
|
|
30
|
+
"who": "signed-in",
|
|
31
|
+
"react": "anyone",
|
|
32
|
+
"slow": 2,
|
|
33
|
+
"max": 140,
|
|
34
|
+
"emoji": { "gem": "💎" },
|
|
35
|
+
"lines": { "gg": "Good game!", "gem": "Grab that gem!", "help": "Help me!", "wait": "Wait for me!" }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Field | | Default |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `mode` | `off`, `emoji` (reactions only), `lines` (and quick lines), `text` (and typing) | `text` |
|
|
42
|
+
| `who` | who may type: `anyone`, `signed-in` (a player account with a passkey), `members` (of the room's server) | `signed-in` |
|
|
43
|
+
| `react` | who may send reactions and quick lines (the same three) | `anyone` |
|
|
44
|
+
| `slow`, `max` | seconds between one person's lines; a typed line's length | 2, 140 |
|
|
45
|
+
| `links`, `swears` | `block` or `allow` (a link is never clickable; slurs, sexual words, threats and contact details are always held) | `block` |
|
|
46
|
+
| `ai` | review typed messages with the studio's own Workers AI | `true` |
|
|
47
|
+
| `bubbles`, `watchers`, `hub` | over the speaker's character; watchers may send; homie.rocks may show it | `true` |
|
|
48
|
+
| `emoji`, `lines` | up to 3 more reactions; up to 12 quick lines (each passes the chat floor at build) | the five; 8 friendly lines |
|
|
49
|
+
| `block`, `allow` | the studio's own words to hold (`word*` inside words) or to let through | none |
|
|
50
|
+
|
|
51
|
+
`"chat": false` turns it off. A kids server, and every beginner server, keeps chat to emoji and
|
|
52
|
+
quick lines whatever the game says. Where new lines show for a moment: game.json
|
|
53
|
+
`"screen": { "chat": "bottom-right" }` (any corner, `top-center` or `bottom-center`), or `false`
|
|
54
|
+
for only the pill's count.
|
|
55
|
+
|
|
56
|
+
**Speech bubbles over characters**, with the port kit (the starters draw them):
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createBubbles, paintBubbles, BUBBLE_FONT } from '@homie-rocks/studio/port';
|
|
60
|
+
const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; } });
|
|
61
|
+
net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
|
|
62
|
+
net.on('unchat', (e) => e.ids.forEach((id) => bubbles.remove(id)));
|
|
63
|
+
// each frame, after the names: the anchor is just over each speaking body's name
|
|
64
|
+
paintBubbles(ctx, bubbles.place(speakers.map((s) => ({ key: s.seat, x: s.x, y: s.nameTop, self: s.mine }))));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A 3D game draws them the same way on its HUD canvas (Gem Rush 3D does), or reads `place()`'s boxes
|
|
68
|
+
and draws its own. **The game's own chat UI** (a key that opens a wheel of quick lines, say):
|
|
69
|
+
`net.sayLine('gg')`, `net.react('fire')`, `net.say(text)`; `net.on('chat')` for a log of your own.
|
|
70
|
+
|
|
71
|
+
## Moderation: what it uses, and why
|
|
72
|
+
|
|
73
|
+
The owner asked for "Cloudflare's new decisions API". That is **Clef**, Cloudflare's decision
|
|
74
|
+
models, launched on Workers AI on 2026-10-01 (Birthday Week): `@cf/cloudflare/clef-flash` (9B) and
|
|
75
|
+
`@cf/cloudflare/clef` (27B). They do not write text: you send a `state` (here, the message) and
|
|
76
|
+
typed questions, and they return a probability for every allowed answer. Cloudflare names "trust
|
|
77
|
+
and safety: score user submissions against your own policy rubric" as a use.
|
|
78
|
+
|
|
79
|
+
| | Clef-flash (the default) | Clef | Llama Guard 3 8B | AI Gateway Guardrails |
|
|
80
|
+
|---|---|---|---|---|
|
|
81
|
+
| What | a decision model: our own question, a probability per answer | the same, larger | Meta's fixed safety categories (S1–S14) | Llama Guard run on prompts and responses of model calls through a gateway |
|
|
82
|
+
| Price | $0.09 per M input tokens, no output charge: measured about 2.3 neurons a short message (the question is most of it) | $0.24 per M: about 6 neurons | $0.48 per M in, $0.03 out: up to about 10 neurons a message with its policy prompt | Llama Guard's price, plus the model call it guards |
|
|
83
|
+
| Speed | about 40 ms median, 120 ms p95 (Cloudflare's numbers) | about 210 ms median | about 500 ms (Cloudflare's guardrails notes) | a model call plus about 500 ms |
|
|
84
|
+
| Fits game chat | our categories: insults, hate, sexual, grooming, harm, spam | the same, more accurate | no insults or bullying category; most game-chat abuse reads as "safe" | not for arbitrary text: it guards model traffic |
|
|
85
|
+
|
|
86
|
+
Clef-flash is the default because it is the cheapest per message by about five times, the fastest
|
|
87
|
+
by far (a message waits for it before anyone sees it), and lets the studio ask exactly what game
|
|
88
|
+
chat needs. Honest caveats: it is one day old as this ships, Cloudflare publishes no moderation
|
|
89
|
+
accuracy numbers for it, and the message it reads is a player's, so a player can try to steer it
|
|
90
|
+
(less of a risk than with a model that writes, since it only scores fixed answers). That is why
|
|
91
|
+
**the word list always runs first and decides alone** whenever the review cannot (no binding, the
|
|
92
|
+
day's budget used, an error, a slow answer). `HOMIE_CHAT_MODEL` switches to Clef or Llama Guard.
|
|
93
|
+
Firewall for AI (now "AI Security for Apps") was ruled out: it reads HTTP requests at the zone, not
|
|
94
|
+
messages inside a Durable Object, and its detections are an Enterprise add-on.
|
|
95
|
+
|
|
96
|
+
**What it costs a studio.** Workers AI gives every account 10,000 neurons a day free, on the free
|
|
97
|
+
Workers plan too, with no payment method. The review's day is 2,000 neurons by default (about
|
|
98
|
+
850 typed messages, measured), leaving the AI guides their 8,000; `homie-studio chat budget <n>` changes it. On the free plan, past the allocation
|
|
99
|
+
Workers AI refuses (the floor decides); on Workers Paid, past it is $0.011 per 1,000 neurons, which
|
|
100
|
+
is why the review stops at its own budget. `homie-studio deploy` binds Workers AI when a game lets
|
|
101
|
+
players type; `homie-studio dev --remote-ai` uses the real one (billed to the signed-in account).
|
package/chat/OWNERS.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Room chat: a note for studio owners
|
|
2
|
+
|
|
3
|
+
**This is not legal advice.** It says what Homie's tooling does and does not do, so you can decide
|
|
4
|
+
what your studio needs. Laws about online chat and children's data differ by country and change;
|
|
5
|
+
if your games are for children, or many children play them, talk to someone who advises on that
|
|
6
|
+
where you and your players are.
|
|
7
|
+
|
|
8
|
+
## Your studio, your rooms, your responsibility
|
|
9
|
+
|
|
10
|
+
A Homie studio runs on **your own Cloudflare account**: your Worker, your Durable Objects, your D1
|
|
11
|
+
database, your Workers AI. Homie (and homie.rocks) does not run your rooms, does not see your
|
|
12
|
+
players' chat, and does not moderate it for you. You choose the rules, you hold the owner's
|
|
13
|
+
controls, and you are the one responsible for what happens in your rooms, as the operator of the
|
|
14
|
+
service. Homie gives you the tools to do that well; it cannot do it for you.
|
|
15
|
+
|
|
16
|
+
What that means in practice:
|
|
17
|
+
|
|
18
|
+
- **Choose rules that fit your players.** Each game sets its defaults in game.json, and you change
|
|
19
|
+
them per game and per server in the office (`/_studio/office`) or with `homie-studio chat rules`.
|
|
20
|
+
If your players are young, keep chat to emoji and quick lines, or put them on a kids server (a
|
|
21
|
+
beginner server with `kids` on): typing is then off whatever the game says.
|
|
22
|
+
- **Watch your rooms, and act.** The office shows every live room's last minutes. Remove a message,
|
|
23
|
+
mute or kick its sender (their messages come down with them), and read what players report.
|
|
24
|
+
- **Answer reports.** Players can report a message. You see it in the office and with
|
|
25
|
+
`homie-studio chat`; dismiss it once you have dealt with it.
|
|
26
|
+
- **Say who your game is for, and what chat it has,** on your game's page and in your own terms or
|
|
27
|
+
rules, if you have them.
|
|
28
|
+
|
|
29
|
+
## What the tooling does
|
|
30
|
+
|
|
31
|
+
| | |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **Who can say what** | By default: everyone can send emoji and the game's quick lines; only **signed-in players** (an account with a passkey on your studio) can type. You can open typing to guests, keep it to members of a server, keep a room to emoji or quick lines, or turn chat off. |
|
|
34
|
+
| **Before anyone sees a typed message** | 1) a built-in word list and patterns (slurs, sexual words, telling someone to hurt themselves, asking a player's age or where they live, asking for pictures, moving a player to another app, email addresses and phone numbers, links, swears), plus your own words; 2) a review on your own Workers AI (Cloudflare's Clef decision model) that holds insults, hate, sexual content, grooming, threats and spam. The list always runs; the review runs within a daily budget. Neither is perfect: both are English-first, and a determined person can get past any filter. |
|
|
35
|
+
| **Emoji and quick lines** | Fixed words you (the game) chose. They are never reviewed and reach every screen at once. |
|
|
36
|
+
| **Children** | A kids server keeps chat to emoji and quick lines, gives players handles instead of names, and never asks anyone's age. Homie never asks for a player's age, birthday or address anywhere. |
|
|
37
|
+
| **AI players** | An AI never types in room chat. |
|
|
38
|
+
| **Slow mode and limits** | One line every 2 s by default (you choose), and message length, repeats and floods are capped. |
|
|
39
|
+
|
|
40
|
+
## What is stored, and for how long
|
|
41
|
+
|
|
42
|
+
| What | Where | How long |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| The chat itself (every message and reaction) | Only in the room's memory, on your Cloudflare account | The last 50 messages of the last 15 minutes, for someone who just opened the room. **Never written to storage.** An empty room forgets them a minute after the last person leaves. |
|
|
45
|
+
| Who said what | The same memory, for your office's Mute and Kick | As long as the message is in that memory. Never an IP address. |
|
|
46
|
+
| A **report** | Your D1 (`chat_reports`) | The one message reported (its words, its sender's name and account id if they had one, the room, when, the reason picked), for **30 days** or until you dismiss it. Never who reported it. |
|
|
47
|
+
| Your chat rules and your words | Your D1 (`chat_rules`) | Until you change them. |
|
|
48
|
+
| Counts (messages sent, held and why, reviews) | Your D1 (`stats_daily`) | Daily numbers, never a message and never a person. |
|
|
49
|
+
| Player accounts | Your D1 (the players tables, `saves/SAVES.md`) | A random id, a display name, a passkey's public key. No email unless a player adds one; players can download and delete everything of theirs at `/account/`. |
|
|
50
|
+
|
|
51
|
+
**The review** sends a typed message's words (and nothing about who wrote it) to Workers AI on your
|
|
52
|
+
own Cloudflare account. Cloudflare says it does not store or train on Clef's requests or responses.
|
|
53
|
+
**homie.rocks** stores nothing of your chat: when your rules allow it, a visitor's own browser opens
|
|
54
|
+
your room's socket to show its chat on your room's card there. Turn that off with `hub` off.
|
|
55
|
+
|
|
56
|
+
## Children's data, in plain words
|
|
57
|
+
|
|
58
|
+
Laws in many places (for example COPPA in the United States, GDPR and the UK's Children's Code in
|
|
59
|
+
Europe and the UK) set rules for online services that collect personal information from children,
|
|
60
|
+
or that are directed at children. A typed message can contain personal information. What helps:
|
|
61
|
+
|
|
62
|
+
- **Keep chat to emoji and quick lines for games aimed at children** (or a kids server). Then no
|
|
63
|
+
child types anything, and there is nothing personal in your chat to keep or lose.
|
|
64
|
+
- **Typing for signed-in players only** (the default) means a person behind each typed line has an
|
|
65
|
+
account you can act on.
|
|
66
|
+
- **Little is stored**, and only briefly (see above). Reports are the exception, kept 30 days.
|
|
67
|
+
- **You can delete**: a report with Dismiss, a message with Remove, a player's whole account in
|
|
68
|
+
their own account page (or `players` in the office).
|
|
69
|
+
|
|
70
|
+
Homie does not know your players' ages and does not verify them. Whether your game is "directed at
|
|
71
|
+
children", and what that requires of you, is for you to decide with advice where you need it.
|
|
72
|
+
|
|
73
|
+
## What the tooling cannot promise
|
|
74
|
+
|
|
75
|
+
No filter catches everything, and a review model can be wrong both ways. The word list is English;
|
|
76
|
+
other languages rely on the review. A studio past its daily review budget, or whose Worker has no
|
|
77
|
+
Workers AI binding yet, relies on the word list alone until the next day (the office says so). Your
|
|
78
|
+
attention, your rules and your owner's controls are the real safety net.
|
package/lib/build.mjs
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* (games/<id>/assets/manifest.json; lib/asset-manifest.mjs servedAssets)
|
|
10
10
|
* site/dist/games/<id>/agents.json the AI guides' vocabulary (games/<id>/agents.json, checked; NETPLAY.md
|
|
11
11
|
* section 18): the only goals and lines an AI in its rooms has
|
|
12
|
-
* site/dist/games.json { studio, games[], songs[], videos[], posts[], site } from studio.json,
|
|
12
|
+
* site/dist/games.json { studio, games[], songs[], videos[], posts[], site, shop } from studio.json,
|
|
13
13
|
* game.json files, the music/ and videos/ manifests (media/MEDIA.md),
|
|
14
|
-
* posts/*.md
|
|
14
|
+
* posts/*.md, the studio's site/ folder (site/SITE.md) and shop.json (checked
|
|
15
|
+
* against the kit's rules: a shop that breaks them stops the build)
|
|
15
16
|
* site/dist/games/<id>/_landing/... what the game's landing shows (hero footage, its cover, licence texts)
|
|
16
17
|
* site/dist/_site/ posts.json (the posts' HTML), the studio's own pages (site/pages)
|
|
17
18
|
* site/dist/_studio/codex/<id>/ each game's Game Codex (games/<id>/CODEX.md, lib/codex.mjs): served only to
|
|
@@ -45,7 +46,10 @@ import { SEAT_MAX } from '../worker/seats.mjs';
|
|
|
45
46
|
import { STUDIO_VERSION } from './version.mjs';
|
|
46
47
|
import { licenseOf, remixRow } from '../worker/license.mjs';
|
|
47
48
|
import { SERVER_LIMITS, serverOf } from '../worker/servers.mjs';
|
|
49
|
+
import { chatProblems } from '../worker/chat.mjs';
|
|
48
50
|
import { vocabularyOf } from '../worker/brain.mjs';
|
|
51
|
+
import { shopForBuild } from './shop.mjs';
|
|
52
|
+
import { audienceOf } from '../worker/shop-rules.mjs';
|
|
49
53
|
|
|
50
54
|
const SOURCE_SKIP = new Set(['node_modules', 'dist', '.git', '.wrangler', '.port']);
|
|
51
55
|
/** Never copied into a static game's served folder. */
|
|
@@ -299,6 +303,8 @@ export async function buildGameFiles(esbuild, root, g, out, { maps = false, sour
|
|
|
299
303
|
export async function build(root, { only = null, log = () => {}, deploy = process.env.WORKERS_CI === '1', maps = false } = {}) {
|
|
300
304
|
const esbuild = await studioEsbuild(root);
|
|
301
305
|
const studio = readStudio(root);
|
|
306
|
+
// The shop first (shop/SHOP.md): a shop.json that breaks the kit's rules stops the build before anything is built.
|
|
307
|
+
const shop = shopForBuild(root, { log });
|
|
302
308
|
const dist = join(root, 'site', 'dist');
|
|
303
309
|
const games = listGames(root).filter((g) => !only || g.id === only);
|
|
304
310
|
if (only && !games.length) throw new Error(`no game "${only}" in games/`);
|
|
@@ -312,6 +318,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
312
318
|
const { mode, warnings } = await buildGameFiles(esbuild, root, g, out, { maps, cache, log });
|
|
313
319
|
// The guides' vocabulary (NETPLAY.md section 18): checked here, so a room never meets a line it cannot say.
|
|
314
320
|
vocabFor(g, out);
|
|
321
|
+
// Room chat (NETPLAY.md section 19): game.json "chat", checked here (its quick lines pass the chat floor too).
|
|
322
|
+
const chatBad = chatProblems(g.chat);
|
|
323
|
+
if (chatBad.length) throw new Error(`games/${g.id}/game.json: ${chatBad.join('; ')}`);
|
|
315
324
|
// The game's own source, for other studios to remix (game.json "share": { "source": false } keeps it private).
|
|
316
325
|
if (g.share?.source !== false) writeFileSync(join(out, 'source.json'), `${JSON.stringify(sourceOf(g.dir, g.id, { studio: studio.name ?? null, game: g.name ?? g.id, license: g.license }))}\n`);
|
|
317
326
|
// Its assets' licences, and the address and SHA-256 of each one a remix may carry (`game remix` fetches them).
|
|
@@ -356,6 +365,9 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
356
365
|
// game.json `"watch"` (NETPLAY.md section 16): watchers see any player's view (the default), only the whole room
|
|
357
366
|
// ("overview": hidden hands or roles), or nothing (false: no watch door).
|
|
358
367
|
...(g.watch === 'overview' ? { watch: 'overview' } : g.watch === false || g.watch === 'off' ? { watch: 'off' } : {}),
|
|
368
|
+
// game.json `"chat"` (0.23.0, NETPLAY.md section 19): the room chat's defaults for this game (the owner's office
|
|
369
|
+
// overrides them), or false for none.
|
|
370
|
+
...(g.chat === false ? { chat: false } : g.chat && typeof g.chat === 'object' ? { chat: g.chat } : {}),
|
|
359
371
|
// Servers (0.16.0): the seeds game.json ships, the netplay revision its build speaks (the office warns when it
|
|
360
372
|
// predates servers), and game.json "agents": { "vote": "game" | false } (the game draws its own vote card, or none).
|
|
361
373
|
...(seeds.length ? { servers: seeds } : {}),
|
|
@@ -395,6 +407,10 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
395
407
|
: studio.homie?.directory === false || studio.homie?.directory === null ? null : String(studio.homie?.directory ?? 'https://homie.rocks'),
|
|
396
408
|
// A copy of the public template that nobody has named yet shows the name typed in Cloudflare's form.
|
|
397
409
|
...(studio.template === true ? { template: true } : {}),
|
|
410
|
+
// studio.json "audience": "kids" (a studio made for children: no shop at all) or "teens"; and "referrals": false
|
|
411
|
+
// (this studio takes no referral statements as a referrer).
|
|
412
|
+
...(audienceOf(studio) !== 'general' ? { audience: audienceOf(studio) } : {}),
|
|
413
|
+
...(studio.referrals === false ? { referrals: false } : {}),
|
|
398
414
|
build: buildInfo(root),
|
|
399
415
|
},
|
|
400
416
|
games: rows,
|
|
@@ -403,6 +419,8 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
403
419
|
// Summaries only; each post's HTML is in _site/posts.json.
|
|
404
420
|
posts: posts.map(({ html, record, ...p }) => p),
|
|
405
421
|
site: { pages: site.pages, partials: site.partials, ...(site.css ? { css: site.css } : {}) },
|
|
422
|
+
// shop.json, checked (nothing secret is ever in it): the Worker sells from this and checks it again.
|
|
423
|
+
...(shop ? { shop } : {}),
|
|
406
424
|
};
|
|
407
425
|
writeFileSync(join(dist, 'games.json'), `${JSON.stringify(catalogue, null, 2)}\n`);
|
|
408
426
|
return {
|