@homie-rocks/studio 0.32.0 → 0.33.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 +85 -2
- package/README.md +34 -9
- package/agents/agents.ts +57 -24
- package/bin/homie-studio.mjs +13 -4
- package/lib/build.mjs +64 -9
- package/lib/cf-usage.mjs +76 -0
- package/lib/cloudflare.mjs +25 -2
- package/lib/dev.mjs +45 -4
- package/lib/lab-check.mjs +3 -2
- package/lib/mcp-tools.mjs +1 -1
- package/lib/mcp.mjs +1 -1
- package/lib/office.mjs +7 -0
- package/lib/perf.mjs +8 -3
- package/lib/player-image.mjs +40 -0
- package/lib/rules-build.mjs +184 -0
- package/lib/rules-guard.mjs +770 -0
- package/lib/scaffold.mjs +43 -16
- package/lib/shop-catalog.mjs +10 -9
- package/lib/shop.mjs +32 -15
- package/lib/standalone-cli.mjs +30 -2
- package/lib/standalone-tools.mjs +20 -2
- package/lib/standalone.mjs +215 -9
- package/lib/studio.mjs +21 -1
- package/lib/template-history.json +90 -0
- package/lib/upgrade.mjs +19 -15
- package/lib/version.mjs +2 -7
- package/netplay/NETPLAY.md +249 -4
- package/netplay/netplay.ts +55 -13
- package/package.json +23 -1
- package/rules/core.ts +1250 -0
- package/rules/guard.ts +419 -0
- package/rules/host.ts +707 -0
- package/rules/math.ts +266 -0
- package/rules/pack.ts +292 -0
- package/rules/rules.ts +544 -0
- package/rules/view.ts +447 -0
- package/shop/SELLING.md +14 -16
- package/shop/SHOP.md +119 -30
- package/shop/shop.ts +6 -8
- package/site/SITE.md +119 -2
- package/standalone/STANDALONE.md +61 -6
- package/starters/coin-dash/game.json +11 -0
- package/starters/coin-dash/index.html +17 -0
- package/starters/coin-dash/map/main.json +16 -0
- package/starters/coin-dash/src/move.ts +27 -0
- package/starters/coin-dash/src/rules.ts +86 -0
- package/starters/coin-dash/src/view.ts +106 -0
- package/starters/coin-dash/tunables.json +5 -0
- package/starters/ember-vale/index.html +11 -0
- package/worker/brain.mjs +8 -7
- package/worker/chat-page.mjs +1 -1
- package/worker/embed.mjs +135 -0
- package/worker/hosted.mjs +56 -0
- package/worker/index.mjs +413 -22
- package/worker/office-page.mjs +1 -0
- package/worker/office.mjs +24 -7
- package/worker/pages.mjs +189 -43
- package/worker/players.mjs +10 -6
- package/worker/referrals.mjs +192 -57
- package/worker/room-store.mjs +54 -0
- package/worker/room.mjs +294 -119
- package/worker/saves-shell.mjs +7 -1
- package/worker/schema.mjs +4 -3
- package/worker/shop-page.mjs +103 -37
- package/worker/shop-rules.mjs +156 -117
- package/worker/shop-store.mjs +70 -9
- package/worker/shop.mjs +275 -143
- package/worker/site.mjs +26 -5
- package/worker/stripe.mjs +9 -6
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,92 @@ 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.33.0] - 2026-10-08
|
|
21
|
+
|
|
22
|
+
**Plugin 0.34.0** · [#57](https://github.com/homie-rocks/homie/pull/57)
|
|
23
|
+
|
|
24
|
+
A game's rules can run on your own Cloudflare instead of in a player's browser. This is the first piece of it: the format, the server that runs it, and one example game. Your existing games are not touched.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Public games can offer X a picture with a play button: its phone apps open the game full screen inside X; its website opens the posted link in a new tab. X decides what it shows, and its written card policy is for audio/video, so this may change. Studio and game switches turn it off.
|
|
29
|
+
- The player joins a public room without sign-in or storage and works as a page of its own and framed; opened as its own page, a reload keeps the room, seat and guest name. Account and purchase links leave a frame, and no purchase starts in the player.
|
|
30
|
+
- The build measures the card's picture and offers the card for any picture X's reference says will render (JPG, PNG, WEBP or GIF, at least 68,600 pixels, under 5 MB), whatever its shape; a refused picture is a warning naming the file and the rule. `site.twitterSite` is optional.
|
|
31
|
+
- Ember Vale's new-hero card fits a short screen: a phone on its side, or a small frame.
|
|
32
|
+
- **A room survives restarts and updates.** Server-hosted games save the whole match, including seats, scores, AI companions and pending decisions. Players reconnect to the same room after a restart; with the default settings, at most one second of play is replayed from the last successful save. If storage is unavailable, play continues and the office reports that recovery may lose more.
|
|
33
|
+
- **Updates keep a running match when its saved state still fits.** Players and watchers load the new game and reclaim their seats. A change to the shape of the saved state starts a fresh match. A game that is slow to load after an update is given up to a minute. A load that fails is tried again a few times, the page says what it is doing while it waits, and it asks you to reload if it has to give up. Older browser-hosted games still update at the round break.
|
|
34
|
+
- **A game written as rules plus view.** The rules (`src/rules.ts`, with the movement code in `src/move.ts`) say what is true in the game: who took which coin, every score, when a round ends. The view (`src/view.ts`) draws what it is told and sends what the player presses. `@homie-rocks/studio/rules` is the format (`defineRules`, `defineMove`, `f` for the type of every piece of state) and `@homie-rocks/studio/rules/view` is what a view uses (`openRoom()`).
|
|
35
|
+
- **The server is the room's host.** The rules of such a game run in the room's own server object on your Cloudflare. No player's browser is in charge, so a changed browser cannot change a score, a pickup or a round, and closing any one tab does not interrupt the others. Seats, chat, votes, watchers and the owner's controls work as before.
|
|
36
|
+
- **Homie checks the rules before they go anywhere.** The build refuses rules that use anything outside a short list of safe operations and names the line (no clock, no network, no `Math.sin` that two machines may round differently, no state outside the declared fields). It then adds a guard, so a rule that never ends or grabs memory is stopped while the room carries on, and a room whose rules keep failing is closed and named in the log.
|
|
37
|
+
- **Every room feature works with the server in charge.** Seats and coming back into the same body, waiting for a seat, watching, chat, votes, your owner controls, servers and their policies, AI seats with a pass, the skill dial, and a game's own decisions (`world.ask`) all behave in a server-hosted room as they do in a browser-hosted one. On a hybrid or beginner server the AI companions keep their reserved seats and their own names, and a guide in such a room follows the goals and lines in your `agents.json`.
|
|
38
|
+
- **`coin-dash`, an example.** Copy it with `homie-studio game new <id> --from coin-dash` to see the two halves. It is an example, not yet a starter the chat offers: see the upgrade notes.
|
|
39
|
+
- **The deploy plan says it.** For each such game: that its rules run on Cloudflare, and roughly what an hour of play in one full room uses of the free plan's daily allowances.
|
|
40
|
+
- **The netplay contract is revision 10** (`netplay/NETPLAY.md`, section 29): the server as a host, input as steps stamped with the room's tick, and the life of such a room. Nothing a game written before sends or receives has changed.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Your shop, your prices.** Studios choose their own shop prices, tips, catalog size, item kinds, durations, refunds and referral terms. Homie no longer sets sales or spending ceilings. Optional spending caps and refund windows belong to the studio; Stripe currency requirements still apply, including currencies without cents.
|
|
45
|
+
- **Your shop policies are editable.** Shop policies are readable and editable presets: protective, adults-only and custom. New shops start protective, including the existing protections for younger players. Existing shops keep their written settings and protective policies unless they choose otherwise. Purchase flood protection now checks both the account and the address, with separate rates the studio can change.
|
|
46
|
+
- **Any name is yours to use in the directory.** The directory no longer keeps a list of Homie's own games' names. A studio, game, song, video or part is listed under whatever plain-text name you gave it, and `studio_request_grant` and `studio_grant_status` are gone from the Homie MCP server.
|
|
47
|
+
- **The toolkit comes from npm only.** homie.rocks no longer serves `homie-studio-<version>.tgz`. A studio pins the npm registry's exact version, as every studio made since 0.10.0 already does.
|
|
48
|
+
- A room of such a game pauses when the last person leaves and ends a minute later if nobody returns. An empty room uses nothing.
|
|
49
|
+
- `homie-studio perf` accepts a room with no browser host.
|
|
50
|
+
- A new studio's `site/src/worker.mjs` hands the Worker the rules of its server-hosted games (`site/src/rules/`, written by every build and committed with the studio), and its `wrangler.jsonc` turns off code made from text while the Worker starts.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- A goal, a line, an ask or an argument named after something every JavaScript object has (`constructor`, `toString` and the like) is no longer taken for one of your vocabulary's. Before, an AI guide could be made to "say" such a line, which threw an error in every player's browser, in browser-hosted games too.
|
|
55
|
+
- Only the room's host can speak as a guide. A player or a watcher sending a line marked as the guide's is ignored.
|
|
56
|
+
- An AI pass opened twice keeps one seat: the newer connection takes it, and reconnecting is paced so a copied pass cannot be used to flood a room.
|
|
57
|
+
|
|
58
|
+
### Upgrade notes
|
|
59
|
+
|
|
60
|
+
- **Check settings you left out of shop.json.** Written settings keep working; no limit is set unless the studio sets one. Omitted settings no longer receive the toolkit's old defaults. To keep those old choices, write `"capPerPlayerMonth": 5000` (monthly spending), `"refundDays": 14` (self-service refunds), `"min": 200, "max": 5000` on each tip, and `"windowDays": 30, "capPerPlayer": 1000, "holdDays": 30, "minimumInvoice": 2500` inside `referrals`. Without them there is no monthly spending cap, no self-service refund window (buyers ask the studio), no tip range beyond provider currency units, no referral window or per-player referral cap, no hold and no minimum invoice. Older `shop init` wrote these amounts explicitly; hand-edited files may have omitted them. All amounts are minor units of your currency.
|
|
61
|
+
- **Write whole amounts.** Fractional minor-unit amounts now stop the build with the setting's name and an example, before publication. For example, replace `"capPerPlayerMonth": 2500.7` with the whole amount you intend, such as `2500`. Every amount setting follows this rule; none is silently rounded. Decimal numeric strings remain accepted; padded strings, hexadecimal strings, lists and booleans now stop the build. Fractional `days` and `refundDays` retain their fraction, rounded to milliseconds, instead of being floored to whole days.
|
|
62
|
+
- **Flood protection allows shared addresses.** `purchaseAttemptsPerMinute` is the account rate (6 by default); `purchaseAttemptsPerAddressPerMinute` is the separate address rate (600 by default, to allow households, schools and venues). Checkout, parent-link and parent-payment attempts have separate counts. Rotating addresses does not bypass the account rate. Referral statements retain a global rate of 30 per minute as well as 30 per address; both are settings in the site configuration; invalid values produce a named error. New guest buyers have a configurable address rate, `guestBuyersPerAddressPerHour`, defaulting to 600 an hour so venues and schools can share a connection; guest buyers and ordinary new players have separate counters.
|
|
63
|
+
- **The withdrawal acknowledgement stays.** Existing checkout and refund-page wording is restored. `policy.withdrawalAcknowledgement` is true in every preset; edit it only as a considered studio policy. Account and younger-player protections remain in the protective preset. `policy.childAge` and `policy.adultAge` default to 13 and 18 and can be edited.
|
|
64
|
+
- **Large shops and referral books use pages.** The shop, in-game sheet and office offer navigation. The office lists referrers with exact totals once each; sales page inside each referrer. `shop statements --send` and the office button send all statements in one action, saving immutable pages and progress in D1. Keep the command or tab open; restart after closing to resume safely. Failed pages retry three times with backoff; persistent failures name the referrer and the rest continue. One CLI office key is renewed during a run. Signed continuation pages contain lines without repeating totals; a new statement replaces the old pages for that period and currency. Mark paid selects one currency and does not change an edition already being sent. Read-only statement views allocate no editions. Sends use Workers-compatible manual redirects and refuse every redirect response.
|
|
65
|
+
- **The optional spending cap retains unresolved payments.** Sessionless rows age out after 32 minutes; sessions past expiry and bank payments still processing count until Stripe confirms payment, expiry or failure. Shop GETs never call Stripe. Only a cap-refused purchase waits for up to three parallel reads with two-second timeouts; successful purchases continue in the background. Atomic claims and per-row intervals (one minute, five for processing payments, growing with age to an hour) bound successful reads; failed reads retry after about one minute and log the order and reason. More than three lost rows can require another attempt. Paid rows use Stripe's charge time; pending, missing payment time or unreachable rows keep counting. Stripe's [resource_missing](https://docs.stripe.com/error-codes#resource-missing) means this key's account has no such session; it frees the reservation and shows the missing row in the office. The office also offers a release recorded with the confirming owner's account (or a fingerprint of the office sign-in session) and time, and asks Stripe to [expire an open Checkout Session](https://docs.stripe.com/api/checkout/sessions/expire). Neither mark prevents a verified payment from granting the item, counting toward spending again and being refundable, even if the freed space has been spent. Internal states and notes stay out of player account data. Late events are idempotent; test rows never count toward live spending. Free orders remain available after an owner lowers a cap. Partial refunds keep the full price counted; full refunds release it. No cap means no spending SUM. Deploy applies `0010_shop_reservations.sql` and `0011_shop_statements.sql`; missing schema reports not ready with the migration names.
|
|
66
|
+
- **Guest checkouts can answer the age question.** A valid answer creates a guest session when the studio permits guests. A refused Stripe checkout returns the guest cookie so retrying uses the same account. Idle guest cleanup keeps owners, including future entitlements, and continues removing other unused guests.
|
|
67
|
+
- Allowed odds and countdown fields that are not published now produce a warning telling the studio to put buyer-facing terms in `blurb`. Three-decimal amounts using the smallest minor unit warn, without an undocumented divisible-by-ten refusal; Stripe's error is shown if it refuses them, with the failure note in the office order row. Free orders can be taken back without contacting Stripe. Stripe settlement minimums remain warnings.
|
|
68
|
+
- A studio made before 0.10.0 pins an address on homie.rocks that no longer answers, so `npm install` in it fails until it is upgraded. Run `npx -y @homie-rocks/studio@latest upgrade --apply`, then `npm install`: the upgrade replaces the address with the registry's exact version. Nothing else about the studio changes.
|
|
69
|
+
- **Nothing to do for the games you have.** A game with no `src/rules.ts` builds and runs exactly as before, in a player's browser. The build says so in one line. Asking for the server on such a game (`"room": { "host": "server" }`) stops the build with the reason.
|
|
70
|
+
- **Do not put a game of your own on this yet.** A server-hosted room has no save in this version: a restart of the room, or any deploy of your site, starts its match again, where today's rooms carry on. That arrives in a later version, and `coin-dash` is offered as a starter only then.
|
|
71
|
+
- In this version a player's own character still moves as today: their browser says where it is and the server holds it to its top speed. A changed browser cannot move faster than that, but it can walk through a wall. Scores and pickups are decided on the server either way.
|
|
72
|
+
- Rules with 3D bodies, rules hosted by a player's browser, offline play of a rules game, and AI guides driven by the server are not in this version.
|
|
73
|
+
- An existing studio gets the new `site/src/worker.mjs` and the empty `site/src/rules/index.mjs` from `homie-studio upgrade --apply` (a Worker file you changed yourself is kept, and the upgrade shows the three lines to add). The next deploy writes the new `wrangler.jsonc`.
|
|
74
|
+
- This version has been run on a local Cloudflare runtime only. It has not been run or measured on Cloudflare itself.
|
|
75
|
+
|
|
76
|
+
## [0.32.1] - 2026-10-07
|
|
77
|
+
|
|
78
|
+
**Plugin 0.33.1** · [#55](https://github.com/homie-rocks/homie/pull/55) · [release-2026-10-07-studio-0.32.1](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-07-studio-0.32.1)
|
|
79
|
+
|
|
80
|
+
A standalone game can be put on your own iPhone or iPad with one command, and the guide says what has now been run on a real phone and corrects what an older site means for a copy.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- **`homie-studio standalone run <game> --for ios --device`** builds the game for the one iPhone or iPad plugged in to your Mac, signs it for your Apple team, installs it over the cable, starts it, and then looks for it among the phone's running programs. It says the game is running only when it found it there. In chat it is `game_standalone` with `device: true`.
|
|
85
|
+
- **`--device` is your yes to what that changes outside your computer.** It adds the phone to your Apple team's list of development devices (a team may register a limited number a year, and a device stays on the list until the membership year renews) and lets Xcode make a development profile for the app. The app is signed for that phone only: a build to try, never one for the store.
|
|
86
|
+
- Every way it can stop is an answer with what to do: no phone connected, more than one (it lists them by model and builds nothing), Developer Mode off (on the phone: Settings, Privacy & Security, Developer Mode; the phone restarts), a phone that does not trust the Mac, a locked phone (it waits a minute for you to unlock it), no Apple account signed in to Xcode, a team with no registered phone, a build that failed. It never says or returns the phone's id, its name or your team's id.
|
|
87
|
+
- The team is `HOMIE_APPLE_TEAM`, else the one team among your keychain's development identities; it says which it used. With none, or more than one, it stops and asks for `HOMIE_APPLE_TEAM`.
|
|
88
|
+
- `setup status` and `standalone plan` have a row for a phone to try a build on: which model is connected and whether its Developer Mode is on, or "could not check".
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **What an older site means for a copy was said too broadly.** Only **Quick play** needs your live site on 0.32.0 or later: it asks the site which room to join, and an older site does not let an app read the answer. A room made in the app or joined by its code connects through an older site too. We saw that against a site on 0.31.0; it is not promised for every older version. The plan, the build, the chat tool, the skill and the guide now say this.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- The guide's "What has been run and what has not" now has what was run on 2026-10-07: an iOS build on a real iPhone, signed for a development team and installed over the cable, playing offline with its bots, and an online private room from that phone through a studio's live site. The new command built, signed and installed a game on that iPhone; the phone was locked, so its last two steps (starting the game and finding it running) have been run against stand-ins only. Still never run: anything under Steam, Developer ID signing and notarization, the iOS archive for the App Store, the GitHub workflow, the Windows and Linux builds started on any computer, a real Android phone, and Quick play online from a phone.
|
|
97
|
+
|
|
98
|
+
### Upgrade notes
|
|
99
|
+
|
|
100
|
+
- A run on a phone needs a Mac with Xcode signed in to your Apple account, and the phone unlocked, trusting the Mac, with Developer Mode on.
|
|
101
|
+
- There is no `--device` for an Android phone yet: `standalone run <game> --for android` hands over to Capacitor, which asks which phone or emulator. That has been run on an emulator only.
|
|
102
|
+
|
|
20
103
|
## [0.32.0] - 2026-10-06
|
|
21
104
|
|
|
22
|
-
**Plugin 0.33.0** · [#54](https://github.com/homie-rocks/homie/pull/54)
|
|
105
|
+
**Plugin 0.33.0** · [#54](https://github.com/homie-rocks/homie/pull/54) · [release-2026-10-06-studio-0.32.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-06-studio-0.32.0)
|
|
23
106
|
|
|
24
107
|
A game can now be an app of its own: a desktop app for macOS, Windows and Linux and a phone app for iOS and Android, the same web game in a thin shell that still plays through your studio's own site.
|
|
25
108
|
|
|
@@ -46,7 +129,7 @@ A game can now be an app of its own: a desktop app for macOS, Windows and Linux
|
|
|
46
129
|
|
|
47
130
|
### Upgrade notes
|
|
48
131
|
|
|
49
|
-
- **Upgrade and deploy before
|
|
132
|
+
- **Upgrade and deploy before Quick play can find a room in a standalone copy.** Quick play asks your live site which room to join, and only a site deployed with 0.32.0 or later lets an app read the answer. Until then Quick play is offline with the game's bots. (Corrected in 0.32.1: a room made or joined by its code connects through an older site too.)
|
|
50
133
|
- The address of your site is built into every copy and cannot be changed in a copy that has shipped. Put the studio on its own domain before you ship: a workers.dev address belongs to the Cloudflare account.
|
|
51
134
|
- Before you ship, name the game's revision in `game.json` (`"netplay": { "version": "1" }`) and raise it with every change an older copy cannot play with. Older copies then keep playing with each other, and say "Update <Name> to play online with everyone." when they meet a newer one.
|
|
52
135
|
- In an app the game's code is trusted with everything the app's page has, and on phones with Capacitor's native bridge. That is fine for your own code; do not load scripts from the network in a game you ship as an app.
|
package/README.md
CHANGED
|
@@ -207,6 +207,24 @@ the owner's session), so `localStorage` throws inside it: a setting or a persona
|
|
|
207
207
|
`net.link` says whether the browser is in its room, reconnecting, or playing alone because the room
|
|
208
208
|
never answered; `guardGestures()` stops a long press on a phone from selecting text in a touch game.
|
|
209
209
|
|
|
210
|
+
## A game's rules on the server (an example, 0.33.0)
|
|
211
|
+
|
|
212
|
+
A game can be written as **rules plus view**: `src/rules.ts` and `src/move.ts` say what is true in the game and run
|
|
213
|
+
in the room's own server object on the studio's Cloudflare; `src/view.ts` draws what it is told and sends what the
|
|
214
|
+
player presses. No player's browser is the room's host, so a changed browser cannot change a score, a pickup or a
|
|
215
|
+
round. `homie-studio build` checks the rules first and refuses anything outside a short list of safe operations,
|
|
216
|
+
naming the line, then guards them so a rule that never ends is stopped while the room carries on
|
|
217
|
+
(`netplay/NETPLAY.md` section 29).
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
npx --no-install homie-studio game new coin-dash --from coin-dash # the example
|
|
221
|
+
npx --no-install homie-studio dev # its rules run in the local room object
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`coin-dash` is an example, not yet a starter to build your own game on: a server-hosted room has no save in this
|
|
225
|
+
version, so a deploy of the site restarts its match. A game with no `src/rules.ts` is not touched by any of this and
|
|
226
|
+
runs in a player's browser exactly as before. So far this has run on a local Cloudflare runtime only.
|
|
227
|
+
|
|
210
228
|
## Player accounts and cloud saves
|
|
211
229
|
|
|
212
230
|
A game whose progress must last (a character that levels up for days, unlocks, a collection, a hardcore mode)
|
|
@@ -433,13 +451,14 @@ npx homie-studio lounge mod <player id> # a mode
|
|
|
433
451
|
|
|
434
452
|
A studio sells items for its games (0.24.0; Stripe's own agent tools since 0.24.3) with **its own Stripe account**: the studio is the seller, money goes
|
|
435
453
|
straight from players to its Stripe, and homie.rocks never sees, holds or moves it (Homie takes no cut; no shared
|
|
436
|
-
currency; not a Connect platform).
|
|
437
|
-
|
|
438
|
-
|
|
454
|
+
currency; not a Connect platform). The studio chooses its prices, item kinds and optional spending and refund
|
|
455
|
+
settings, with no Homie ceiling. New shops start with the editable `protective` policy: accounts and an age
|
|
456
|
+
question, no purchases for under-13s, parent checkout for teens, no shop on kids servers, and no paid randomness.
|
|
457
|
+
The studio can inspect and change each rule in shop.json; [shop/SHOP.md](shop/SHOP.md) explains every preset.
|
|
439
458
|
|
|
440
459
|
```sh
|
|
441
460
|
npx homie-studio shop init --supporter # shop.json with a US$5 Supporter pack, and SELLING.md (the owner's plain words)
|
|
442
|
-
npx homie-studio shop check # the
|
|
461
|
+
npx homie-studio shop check # the studio settings and provider requirements; every build checks them too
|
|
443
462
|
npx homie-studio shop catalog [--have <file>] # the items as Products in Stripe: the read, then the exact writes (Stripe's MCP)
|
|
444
463
|
npx homie-studio shop connect # a page on THIS computer: the owner pastes one restricted key; it makes the webhook
|
|
445
464
|
npx homie-studio shop # open (test or live) or what is missing
|
|
@@ -823,12 +842,19 @@ every other target says "built, not started on this computer".
|
|
|
823
842
|
|
|
824
843
|
**What the standalone game does not have (v1):** player accounts and sign-in, cloud saves (saves stay on the
|
|
825
844
|
device and are lost when the app is uninstalled), the shop, room chat, watching and the big screen, servers
|
|
826
|
-
other than the public one, automatic updates, and a signed Windows build.
|
|
827
|
-
**and deployed
|
|
845
|
+
other than the public one, automatic updates, and a signed Windows build. For Quick play to find a room, a
|
|
846
|
+
studio must be on 0.32.0 or later **and deployed**; a room made or joined by its code works with an older site
|
|
847
|
+
too (seen against 0.31.0, not promised for every older version).
|
|
848
|
+
|
|
849
|
+
**On your own iPhone or iPad:** `standalone run <id> --for ios --device` builds the game for the one phone
|
|
850
|
+
plugged in, signs it for your Apple team, installs it over the cable, starts it and looks for it among the
|
|
851
|
+
phone's running programs. `--device` is your yes to what it changes: it adds the phone to your Apple team's list
|
|
852
|
+
of development devices.
|
|
828
853
|
|
|
829
854
|
**What has not been run:** the Windows and Linux builds have never been started; nothing has been started by
|
|
830
855
|
Steam (its overlay, which Electron apps often do not show, included); a Developer ID signature, notarization and
|
|
831
|
-
the iOS archive have never been made; nothing has run on a real phone
|
|
856
|
+
the iOS archive have never been made; nothing has run on a real Android phone, and Quick play has not been seen
|
|
857
|
+
online from a phone. The guide lists it all.
|
|
832
858
|
|
|
833
859
|
## How it feels: the Game Lab (`homie-studio lab`)
|
|
834
860
|
|
|
@@ -1193,8 +1219,7 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
|
|
|
1193
1219
|
Versions are immutable: a published version never changes, so a studio that pinned one
|
|
1194
1220
|
never changes by surprise. A change ships as a new `version` on the npm registry as
|
|
1195
1221
|
`@homie-rocks/studio`, which is what `homie-studio new` pins (the exact version; package-lock.json keeps its
|
|
1196
|
-
integrity). The registry is what Workers Builds and a Claude Code cloud session reach by default.
|
|
1197
|
-
pinned a tarball at `https://homie.rocks/npm/homie-studio-<version>.tgz`, which stays.
|
|
1222
|
+
integrity). The registry is what Workers Builds and a Claude Code cloud session reach by default.
|
|
1198
1223
|
|
|
1199
1224
|
## Beta
|
|
1200
1225
|
|
package/agents/agents.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* vocabulary line with arguments that fit, on a server whose AI may talk).
|
|
25
25
|
* =============================================================================
|
|
26
26
|
*/
|
|
27
|
-
import { aiName, stripAi, type Netplay, type NetEvent, type Policy, type Slot } from '../netplay/netplay';
|
|
27
|
+
import { aiName, stripAi, type Netplay, type NetEvent, type Policy, type Slot } from '../netplay/netplay.ts';
|
|
28
28
|
|
|
29
29
|
/* ------------------------------------------------------------------ the vocabulary (agents.json) */
|
|
30
30
|
|
|
@@ -56,6 +56,13 @@ export interface GoalEvent { slot: number; goal: Goal; prev: Goal | null; askAt:
|
|
|
56
56
|
export interface AskButton { k: string; args: Record<string, unknown>; text: string }
|
|
57
57
|
|
|
58
58
|
export interface AgentsOptions {
|
|
59
|
+
/** Runtime integration: a room clock and an explicit beat replace the browser timer. */
|
|
60
|
+
now?: () => number;
|
|
61
|
+
manual?: boolean;
|
|
62
|
+
/** Keep an asked-for goal through scripted floor decisions until completion or sixty seconds. */
|
|
63
|
+
carryFloor?: boolean;
|
|
64
|
+
restore?: AgentsSaved;
|
|
65
|
+
|
|
59
66
|
/** Host: what a guide in this slot sees. Game state only: never an account, an address or typed text. Under 2 KB. */
|
|
60
67
|
view: (slot: number) => Record<string, unknown>;
|
|
61
68
|
/** The scripted floor: runs with no AI, over budget, and between AI decisions. Synchronous; never waits. */
|
|
@@ -70,7 +77,14 @@ export interface AgentsOptions {
|
|
|
70
77
|
|
|
71
78
|
export interface AgentsStats { views: number; dos: number; says: number; asks: number; drops: number; floors: number; viewBytes: number }
|
|
72
79
|
|
|
80
|
+
export interface AgentsSaved { goals: [number, Goal][]; asks: [number, Ask[]][]; avoid: [number, [number, number][]][]; sayAt: [number, number][]; viewAt: [number, number][]; floorAt: [number, number][]; askAt: [number, number][] }
|
|
73
81
|
export interface Agents {
|
|
82
|
+
tick(): void;
|
|
83
|
+
save(): AgentsSaved;
|
|
84
|
+
forget(slot: number): void;
|
|
85
|
+
/** Drop one slot's goal so its floor may run at once. Its pacing, its asks and who asked to be left alone all stay. */
|
|
86
|
+
dropGoal(slot: number): void;
|
|
87
|
+
|
|
74
88
|
readonly vocab: Vocabulary;
|
|
75
89
|
/** Whether this server's AI may talk (its brain is workers-ai or owner-key, and speech is not off). */
|
|
76
90
|
readonly talking: boolean;
|
|
@@ -95,6 +109,7 @@ export interface Agents {
|
|
|
95
109
|
stop(): void;
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
const own = <T>(table: Record<string, T> | undefined, key: unknown): T | undefined => table && typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
98
113
|
const TALK = ['workers-ai', 'owner-key'];
|
|
99
114
|
export const AGENT_RULES = Object.freeze({ viewMs: 2050, floorMs: 1000, quietMs: 8000, leaveMs: 10 * 60_000, askMs: 30_000, holdMs: 45_000, askWaitMs: 4000, carryMs: 60_000, viewBytes: 1900 });
|
|
100
115
|
const VIEW_ARG = /^view\.([A-Za-z][A-Za-z0-9_]{0,23})$/;
|
|
@@ -127,7 +142,7 @@ export function argsWhy(types: Record<string, ArgType> | undefined, args: unknow
|
|
|
127
142
|
return null;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
export const labelOf = (vocab: Vocabulary, value: unknown): string => vocab.labels
|
|
145
|
+
export const labelOf = (vocab: Vocabulary, value: unknown): string => own(vocab.labels, String(value)) ?? String(value ?? '').replace(/[-_]+/g, ' ');
|
|
131
146
|
|
|
132
147
|
/* ------------------------------------------------------------------ useAgents */
|
|
133
148
|
|
|
@@ -145,7 +160,15 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
145
160
|
const askAt = new Map<number, number>();
|
|
146
161
|
const handlers = { say: new Set<(e: SayEvent) => void>(), goal: new Set<(e: GoalEvent) => void>(), ask: new Set<(e: Ask & { slot: number }) => void>() };
|
|
147
162
|
const st: AgentsStats = { views: 0, dos: 0, says: 0, asks: 0, drops: 0, floors: 0, viewBytes: 0 };
|
|
148
|
-
const wall = ()
|
|
163
|
+
const wall = opts.now ?? (() => Date.now());
|
|
164
|
+
if (opts.restore) {
|
|
165
|
+
const entries = (v: unknown): [number, any][] => Array.isArray(v) ? v.slice(0, 256).filter((e) => Array.isArray(e) && e.length === 2 && Number.isSafeInteger(e[0]) && e[0] >= 0) : [];
|
|
166
|
+
const stamp = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= wall();
|
|
167
|
+
for (const [k, v] of entries(opts.restore.goals)) if (v && own(vocab.goals, v.goal) && stamp(v.at) && v.args && typeof v.args === 'object' && !Array.isArray(v.args)) goals.set(k, { ...v });
|
|
168
|
+
for (const [k, v] of entries(opts.restore.asks)) if (Array.isArray(v)) asks.set(k, v.slice(0, 3).filter((a) => a && own(vocab.asks, a.k) && stamp(a.at) && Number.isSafeInteger(a.from) && a.from >= 0 && a.args && typeof a.args === 'object' && !Array.isArray(a.args)));
|
|
169
|
+
for (const [k, v] of entries(opts.restore.avoid)) avoid.set(k, new Map(entries(v).filter(([, until]) => typeof until === 'number' && Number.isFinite(until) && until > wall() && until <= wall() + AGENT_RULES.leaveMs)));
|
|
170
|
+
for (const [dst, src] of [[sayAt, opts.restore.sayAt], [viewAt, opts.restore.viewAt], [floorAt, opts.restore.floorAt], [askAt, opts.restore.askAt]] as const) for (const [k, v] of entries(src)) if (stamp(v)) dst.set(k, v);
|
|
171
|
+
}
|
|
149
172
|
|
|
150
173
|
const guides = (): Slot[] => (net.slots ?? []).filter((s) => s.agent && roles.includes(s.agent.role as 'guide' | 'party'));
|
|
151
174
|
const slotOf = (slot: number): Slot | undefined => guides().find((s) => s.slot === slot);
|
|
@@ -171,7 +194,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
171
194
|
};
|
|
172
195
|
|
|
173
196
|
function render(id: string, args: Record<string, unknown>, slot: number | null, kind: 'lines' | 'asks' = 'lines'): string | null {
|
|
174
|
-
const def = kind === 'asks' ? vocab.asks
|
|
197
|
+
const def = kind === 'asks' ? own(vocab.asks, id) : own(vocab.lines, id);
|
|
175
198
|
if (!def) return null;
|
|
176
199
|
const s = slot === null ? undefined : (net.slots ?? []).find((x) => x.slot === slot);
|
|
177
200
|
const me = s ? stripAi(s.name) : 'Guide';
|
|
@@ -180,7 +203,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
180
203
|
|
|
181
204
|
/** A line the host lets a guide say: from the vocabulary, its arguments fit, not within 8 s of its last. */
|
|
182
205
|
function say(slot: number, line: string, args: Record<string, unknown>): boolean {
|
|
183
|
-
const def = vocab.lines
|
|
206
|
+
const def = own(vocab.lines, line);
|
|
184
207
|
const now = wall();
|
|
185
208
|
if (!def || !talksOn(net.policy) || now - (sayAt.get(slot) ?? -Infinity) < AGENT_RULES.quietMs) { st.drops += 1; return false; }
|
|
186
209
|
if (argsWhy(def.args, args, lastView.get(slot) ?? null, people()) || touchesAvoided(def.args, args, slot)) { st.drops += 1; return false; }
|
|
@@ -197,7 +220,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
197
220
|
|
|
198
221
|
function setGoal(slot: number, d: Decision, from: 'brain' | 'floor', answering = true): boolean {
|
|
199
222
|
if (!d.goal) return false;
|
|
200
|
-
const def = vocab.goals
|
|
223
|
+
const def = own(vocab.goals, d.goal);
|
|
201
224
|
const args = (d.args ?? {}) as Record<string, unknown>;
|
|
202
225
|
if (!def || argsWhy(def.args, args, lastView.get(slot) ?? opts.view(slot), people()) || touchesAvoided(def.args, args, slot)) { st.drops += 1; return false; }
|
|
203
226
|
const prev = goals.get(slot) ?? null;
|
|
@@ -216,13 +239,13 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
216
239
|
// A person's ask is answered the way agents.json says (its `goal`): a brain's decision that does something else
|
|
217
240
|
// leaves the ask open, and the floor answers it once the brain has had askWaitMs (a fixed rule, not the model's).
|
|
218
241
|
const open = askAt.has(slot) ? (asks.get(slot) ?? []).at(-1) ?? null : null;
|
|
219
|
-
const want = open ? vocab.asks
|
|
220
|
-
const answering = !open ||
|
|
242
|
+
const want = open ? own(vocab.asks, open.k)?.goal : undefined;
|
|
243
|
+
const answering = !open || !want || (want === d.goal && Object.entries(open.args).every(([k, v]) => !own(own(vocab.goals, want)?.args, k) || d.args?.[k] === v));
|
|
221
244
|
// What a person asked for is carried through until it is done (or 60 s): a brain's other goal waits.
|
|
222
245
|
const cur = goals.get(slot);
|
|
223
|
-
if (!open && from === 'brain' && cur && cur.asked && cur.state === 'active' && wall() - cur.at < AGENT_RULES.carryMs && d.goal !== cur.goal) { st.drops += 1; if (speak && d.say) say(slot, d.say, (d.sayArgs ?? {}) as Record<string, unknown>); return; }
|
|
246
|
+
if (!open && from === 'brain' && cur && cur.asked && cur.state === 'active' && wall() - cur.at < AGENT_RULES.carryMs && (d.goal !== cur.goal || JSON.stringify(d.args ?? {}) !== JSON.stringify(cur.args))) { st.drops += 1; if (speak && d.say) say(slot, d.say, (d.sayArgs ?? {}) as Record<string, unknown>); return; }
|
|
224
247
|
setGoal(slot, d, from, answering);
|
|
225
|
-
if (open && answering) { askAt.delete(slot); asks.set(slot, []); const g = goals.get(slot); if (g && d.goal === g.goal) g.asked = true; }
|
|
248
|
+
if (open && answering && goals.get(slot)?.goal === d.goal && JSON.stringify(goals.get(slot)?.args) === JSON.stringify(d.args ?? {})) { askAt.delete(slot); asks.set(slot, []); const g = goals.get(slot); if (g && d.goal === g.goal) { g.asked = true; g.at = wall(); } }
|
|
226
249
|
if (speak && d.say) say(slot, d.say, (d.sayArgs ?? {}) as Record<string, unknown>);
|
|
227
250
|
}
|
|
228
251
|
|
|
@@ -242,21 +265,23 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
242
265
|
|
|
243
266
|
/** The floor for one slot: the game's own decide, on its view. */
|
|
244
267
|
function floor(slot: number, held: boolean): void {
|
|
245
|
-
if (!opts.decide) return;
|
|
268
|
+
if (!opts.decide) { floorAt.set(slot, wall()); askAt.delete(slot); asks.set(slot, []); return; }
|
|
246
269
|
const v = viewOf(slot);
|
|
247
270
|
lastView.set(slot, v);
|
|
248
271
|
let d: Decision | null = null;
|
|
249
272
|
try { d = opts.decide(v, { slot, goal: goals.get(slot) ?? null, asks: asksFor(slot) }); } catch { d = null; }
|
|
250
273
|
floorAt.set(slot, wall());
|
|
251
|
-
if
|
|
274
|
+
// One floor attempt settles an ask, even if it declines or fails.
|
|
275
|
+
if (!d) { askAt.delete(slot); asks.set(slot, []); return; }
|
|
252
276
|
st.floors += 1;
|
|
253
277
|
// The floor speaks only for a guide no AI holds (a held guide's own brain speaks for it).
|
|
254
278
|
apply(slot, d, 'floor', !held);
|
|
279
|
+
askAt.delete(slot); asks.set(slot, []);
|
|
255
280
|
}
|
|
256
281
|
|
|
257
282
|
/** A person's ask reached the host (theirs, or its own player's). */
|
|
258
283
|
function onAsk(slot: number, k: string, args: Record<string, unknown>, from: number): void {
|
|
259
|
-
const def = vocab.asks
|
|
284
|
+
const def = own(vocab.asks, k);
|
|
260
285
|
const s = slotOf(slot);
|
|
261
286
|
if (!def || !s || argsWhy(def.args, args, lastView.get(slot) ?? viewOf(slot), null)) { st.drops += 1; return; }
|
|
262
287
|
const now = wall();
|
|
@@ -267,7 +292,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
267
292
|
emit('ask', { slot, k, args, from, at: now });
|
|
268
293
|
const held = s.agent?.seat !== null && s.agent?.seat !== undefined;
|
|
269
294
|
// No AI holds this guide: the floor answers at once. Held: its brain has askWaitMs to answer first.
|
|
270
|
-
if (!held) floor(slot, false);
|
|
295
|
+
if (!held) { if (opts.manual) floorAt.delete(slot); else floor(slot, false); }
|
|
271
296
|
}
|
|
272
297
|
|
|
273
298
|
const offEvent = net.on('event', (e: NetEvent) => {
|
|
@@ -286,12 +311,13 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
286
311
|
if (ask && e.from !== null && !net.isAgent(e.from) && Number.isInteger(d['slot'])) { onAsk(d['slot'] as number, ask, (d['args'] ?? {}) as Record<string, unknown>, e.from); return; }
|
|
287
312
|
return;
|
|
288
313
|
}
|
|
314
|
+
if (k === 'agent:offer' && e.from === net.host?.seat && Number.isInteger(d['slot']) && d['view'] && typeof d['view'] === 'object') { lastView.set(d['slot'] as number, d['view'] as Record<string, unknown>); return; }
|
|
289
315
|
// Every other browser: a guide's line the host relayed (Quiet AI already dropped it in the helper). Its words are
|
|
290
316
|
// this browser's own copy of the vocabulary; an argument is a seat, a value of the line's list, or an id.
|
|
291
317
|
const line = /^say:([a-z][a-z0-9_]{0,31})$/.exec(k)?.[1];
|
|
292
|
-
if (line && d['ai'] === true && Number.isInteger(d['slot'])) {
|
|
318
|
+
if (e.from === net.host?.seat && line && d['ai'] === true && Number.isInteger(d['slot'])) {
|
|
293
319
|
const args = (d['args'] ?? {}) as Record<string, unknown>;
|
|
294
|
-
const def = vocab.lines
|
|
320
|
+
const def = own(vocab.lines, line);
|
|
295
321
|
const ids = Object.fromEntries(Object.entries(def?.args ?? {}).map(([n, t]) => [n, t]).filter(([, t]) => typeof t === 'string' && VIEW_ARG.test(t)).map(([n, t]) => [VIEW_ARG.exec(t as string)![1], /^[a-z0-9][a-z0-9_-]{0,39}$/.test(String(args[n as string] ?? '')) ? [args[n as string]] : []]));
|
|
296
322
|
if (!def || argsWhy(def.args, args, ids, null)) { st.drops += 1; return; }
|
|
297
323
|
const text = render(line, args, d['slot'] as number);
|
|
@@ -300,7 +326,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
300
326
|
});
|
|
301
327
|
|
|
302
328
|
// The host's beat: views to the guides an AI holds (at most one every 2 s each), the floor for the rest.
|
|
303
|
-
const
|
|
329
|
+
const beat = (): void => {
|
|
304
330
|
if (!net.isHost || net.offline) return;
|
|
305
331
|
const now = wall();
|
|
306
332
|
for (const s of guides()) {
|
|
@@ -322,21 +348,27 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
322
348
|
const waiting = held && askAt.has(s.slot) && now - (askAt.get(s.slot) as number) < askWaitMs;
|
|
323
349
|
// The floor decides for a guide with no AI's decision in force, at most once a second; an ask an AI left
|
|
324
350
|
// unanswered for askWaitMs is answered by the floor (silently: a held guide's lines are its brain's).
|
|
325
|
-
|
|
351
|
+
const carried = opts.carryFloor && g?.asked && g.state === 'active' && now - g.at < AGENT_RULES.carryMs && !askAt.has(s.slot);
|
|
352
|
+
if (!carried && ((!brainFresh && !waiting && now - (floorAt.get(s.slot) ?? -Infinity) >= AGENT_RULES.floorMs) || (held && askAt.has(s.slot) && !waiting))) floor(s.slot, held);
|
|
326
353
|
}
|
|
327
354
|
// Slots no longer an AI's: forget them.
|
|
328
355
|
const live = new Set(guides().map((s) => s.slot));
|
|
329
|
-
for (const k of [...
|
|
330
|
-
}
|
|
356
|
+
for (const map of [goals, asks, avoid, sayAt, viewAt, floorAt, askAt, lastView]) for (const k of [...map.keys()]) if (!live.has(k)) map.delete(k);
|
|
357
|
+
};
|
|
358
|
+
const timer = opts.manual ? null : setInterval(beat, 250);
|
|
331
359
|
|
|
332
360
|
const agents: Agents = {
|
|
361
|
+
tick: beat,
|
|
362
|
+
forget: (slot) => { for (const map of [goals, asks, avoid, sayAt, viewAt, floorAt, askAt, lastView]) map.delete(slot); },
|
|
363
|
+
dropGoal: (slot) => { goals.delete(slot); floorAt.delete(slot); },
|
|
364
|
+
save: () => ({ goals: [...goals], asks: [...asks], avoid: [...avoid].map(([k, v]) => [k, [...v]]), sayAt: [...sayAt], viewAt: [...viewAt], floorAt: [...floorAt], askAt: [...askAt] }),
|
|
333
365
|
vocab,
|
|
334
366
|
get talking() { return talksOn(net.policy); },
|
|
335
367
|
guides,
|
|
336
368
|
goalOf: (slot) => goals.get(slot) ?? null,
|
|
337
369
|
asksFor,
|
|
338
370
|
ask(slot, k, args = {}) {
|
|
339
|
-
const def = vocab.asks
|
|
371
|
+
const def = own(vocab.asks, k);
|
|
340
372
|
const s = slotOf(slot);
|
|
341
373
|
if (!def || !s) return false;
|
|
342
374
|
const seat = s.agent?.seat ?? null;
|
|
@@ -353,6 +385,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
353
385
|
return true;
|
|
354
386
|
},
|
|
355
387
|
askButtons(slot, offer = {}) {
|
|
388
|
+
if (!slotOf(slot)) return [];
|
|
356
389
|
const out: AskButton[] = [];
|
|
357
390
|
const view = { ...(lastView.get(slot) ?? {}), ...offer };
|
|
358
391
|
for (const [k, def] of Object.entries(vocab.asks ?? {})) {
|
|
@@ -362,7 +395,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
362
395
|
const name = names[0] as string;
|
|
363
396
|
const type = (def.args as Record<string, ArgType>)[name];
|
|
364
397
|
const m = typeof type === 'string' ? VIEW_ARG.exec(type) : null;
|
|
365
|
-
const values = Array.isArray(type) ? [...type] : m ? (Array.isArray(view[m[1] as string]) ? (view[m[1] as string] as unknown[]) : []) : [];
|
|
398
|
+
const values = Array.isArray(type) ? [...type] : type === 'player' ? (net.seat === null ? [] : [net.seat]) : m ? (Array.isArray(view[m[1] as string]) ? (view[m[1] as string] as unknown[]) : []) : [];
|
|
366
399
|
for (const v of values.slice(0, 4)) {
|
|
367
400
|
const value = v && typeof v === 'object' ? (v as { id?: unknown }).id : v;
|
|
368
401
|
const text = render(k, { [name]: value }, slot, 'asks');
|
|
@@ -378,7 +411,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
378
411
|
g.at = wall();
|
|
379
412
|
const s = slotOf(slot);
|
|
380
413
|
// A guide no AI holds decides again at once; one an AI holds hears it in its next view.
|
|
381
|
-
if (s && (s.agent?.seat ?? null) === null) floor(slot, false);
|
|
414
|
+
if (s && (s.agent?.seat ?? null) === null) { if (opts.manual) floorAt.delete(slot); else floor(slot, false); }
|
|
382
415
|
},
|
|
383
416
|
render,
|
|
384
417
|
on(kind: 'say' | 'goal' | 'ask', fn: (e: never) => void) {
|
|
@@ -386,7 +419,7 @@ export function useAgents(net: Netplay<any, any, any>, vocab: Vocabulary, opts:
|
|
|
386
419
|
return () => { (handlers[kind] as Set<unknown>).delete(fn); };
|
|
387
420
|
},
|
|
388
421
|
stats: () => ({ ...st }),
|
|
389
|
-
stop() { clearInterval(timer); offEvent(); },
|
|
422
|
+
stop() { if (timer) clearInterval(timer); offEvent(); },
|
|
390
423
|
} as Agents;
|
|
391
424
|
return agents;
|
|
392
425
|
}
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
* spent from the guides' day; --model compares a model before setting it)
|
|
170
170
|
* homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
|
|
171
171
|
* homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
|
|
172
|
-
* homie-studio shop check shop.json against
|
|
172
|
+
* homie-studio shop check shop.json against studio settings and provider requirements
|
|
173
173
|
* homie-studio shop connect [--managed] [--live] a page on THIS computer where the owner pastes the studio's
|
|
174
174
|
* restricted Stripe key; with it this makes the webhook (0.24.3), and the key
|
|
175
175
|
* and the webhook's secret go straight to the Worker secrets, never a chat or a
|
|
@@ -295,6 +295,13 @@
|
|
|
295
295
|
* id, and an unsigned release is said as UNSIGNED and is not a success); run:
|
|
296
296
|
* the built copy, here; steam: Steam's build file; ci: a GitHub workflow that
|
|
297
297
|
* builds all five. Nothing is ever uploaded to Steam or a store
|
|
298
|
+
* homie-studio standalone run <game> --for ios --device
|
|
299
|
+
* the game on the ONE iPhone or iPad plugged in to this Mac: built, signed for
|
|
300
|
+
* your Apple team (HOMIE_APPLE_TEAM, else the keychain's only one), installed
|
|
301
|
+
* over the cable, started, and then looked for among the phone's running
|
|
302
|
+
* programs. --device is your yes to what it changes outside this computer: it
|
|
303
|
+
* adds the phone to your Apple team's list of development devices. A phone with
|
|
304
|
+
* Developer Mode off, locked, or not trusting this Mac is said with its fix
|
|
298
305
|
*
|
|
299
306
|
* homie-studio statusline the current build in one line (what Claude Code's status line shows)
|
|
300
307
|
* homie-studio statusline --install [--project <folder>]
|
|
@@ -361,7 +368,7 @@ import { standaloneDeployNotes } from '../lib/standalone.mjs';
|
|
|
361
368
|
|
|
362
369
|
const argv = process.argv.slice(2);
|
|
363
370
|
const flags = new Map();
|
|
364
|
-
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', 'no-rig', 'supporter', 'managed', 'live', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release'];
|
|
371
|
+
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', 'no-rig', 'supporter', 'managed', 'live', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
|
|
365
372
|
const positional = [];
|
|
366
373
|
for (let i = 0; i < argv.length; i++) {
|
|
367
374
|
const a = argv[i];
|
|
@@ -803,9 +810,11 @@ const OUTWARD_FLAGS = {
|
|
|
803
810
|
'lounge mod': ['url', 'remove'], 'lounge remove': ['url', 'all'],
|
|
804
811
|
'chat remove': ['url', 'all'], 'chat budget': ['url'],
|
|
805
812
|
'servers close': ['url', 'reopen'], 'servers level': ['url'], 'servers member': ['url', 'role', 'remove'],
|
|
806
|
-
'shop connect': ['url', 'managed', 'live'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note'], 'shop statements': ['url', 'period', 'send'],
|
|
813
|
+
'shop connect': ['url', 'managed', 'live'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note'], 'shop statements': ['url', 'period', 'send', 'cursor'],
|
|
807
814
|
'agents pass': ['url', 'label', 'server', 'hands', 'role', 'days'], 'agents revoke': ['url'], 'agents brain': ['url', 'budget', 'remove'],
|
|
808
815
|
'agents sit': ['url', 'server', 'pass', 'label', 'brain'],
|
|
816
|
+
// With --device it adds a phone to the person's Apple team and installs on it: a flag it does not know stops it.
|
|
817
|
+
'standalone run': ['for', 'device', 'site'],
|
|
809
818
|
};
|
|
810
819
|
/** Every command takes these: how it answers, never what it does. */
|
|
811
820
|
const ANY_COMMAND = ['json', 'help', 'version'];
|
|
@@ -1046,7 +1055,7 @@ async function main() {
|
|
|
1046
1055
|
}
|
|
1047
1056
|
if (sub === 'orders') return shopOrders(root, { url });
|
|
1048
1057
|
if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note') });
|
|
1049
|
-
if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), send: flags.has('send') });
|
|
1058
|
+
if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), cursor: flags.get('cursor') ?? '', send: flags.has('send') });
|
|
1050
1059
|
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, catalog, orders, refund, statements)` };
|
|
1051
1060
|
}
|
|
1052
1061
|
if (cmd === 'agents') {
|