summer-engine 4.3.0 → 4.5.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.
Files changed (218) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/AGENTS.md +2 -2
  7. package/CHANGELOG.md +87 -0
  8. package/README.md +67 -27
  9. package/commands/start.md +19 -15
  10. package/commands/summer.md +1 -0
  11. package/dist/cli/command-usage.d.ts +3 -1
  12. package/dist/cli/command-usage.js +10 -4
  13. package/dist/cli/commands/create.d.ts +1 -25
  14. package/dist/cli/commands/create.js +10 -138
  15. package/dist/cli/commands/dev.d.ts +66 -0
  16. package/dist/cli/commands/dev.js +234 -0
  17. package/dist/cli/commands/install.d.ts +15 -58
  18. package/dist/cli/commands/install.js +31 -481
  19. package/dist/cli/commands/login.js +1 -1
  20. package/dist/cli/commands/ops.d.ts +17 -1
  21. package/dist/cli/commands/ops.js +97 -9
  22. package/dist/cli/commands/preview.d.ts +58 -0
  23. package/dist/cli/commands/preview.js +181 -0
  24. package/dist/cli/commands/run.d.ts +1 -17
  25. package/dist/cli/commands/run.js +17 -206
  26. package/dist/cli/index.js +5 -0
  27. package/dist/core/agent-host.d.ts +3 -0
  28. package/dist/core/agent-host.js +1 -1
  29. package/dist/core/api-client.d.ts +10 -2
  30. package/dist/core/api-client.js +13 -8
  31. package/dist/core/capabilities/agent-playbook.js +1 -0
  32. package/dist/core/capabilities/apps.d.ts +9 -9
  33. package/dist/core/capabilities/asset-import.d.ts +4 -1
  34. package/dist/core/capabilities/asset-import.js +15 -6
  35. package/dist/core/capabilities/capture-gameplay.js +1 -1
  36. package/dist/core/capabilities/connect-signal.d.ts +2 -2
  37. package/dist/core/capabilities/content-make/index.d.ts +81 -0
  38. package/dist/core/capabilities/content-make/index.js +706 -0
  39. package/dist/core/capabilities/content-make/media.d.ts +57 -0
  40. package/dist/core/capabilities/content-make/media.js +172 -0
  41. package/dist/core/capabilities/content-make/plan.d.ts +130 -0
  42. package/dist/core/capabilities/content-make/plan.js +433 -0
  43. package/dist/core/capabilities/content-make/render.d.ts +64 -0
  44. package/dist/core/capabilities/content-make/render.js +370 -0
  45. package/dist/core/capabilities/content-make/study.d.ts +13 -0
  46. package/dist/core/capabilities/content-make/study.js +165 -0
  47. package/dist/core/capabilities/content-make/types.d.ts +173 -0
  48. package/dist/core/capabilities/content-make/types.js +6 -0
  49. package/dist/core/capabilities/content-make/voice.d.ts +7 -0
  50. package/dist/core/capabilities/content-make/voice.js +27 -0
  51. package/dist/core/capabilities/events.d.ts +2 -2
  52. package/dist/core/capabilities/export-game.d.ts +4 -1
  53. package/dist/core/capabilities/export-game.js +4 -3
  54. package/dist/core/capabilities/export-presets.d.ts +2 -1
  55. package/dist/core/capabilities/export-presets.js +2 -1
  56. package/dist/core/capabilities/go-live.d.ts +14 -0
  57. package/dist/core/capabilities/go-live.js +408 -0
  58. package/dist/core/capabilities/image-generation.d.ts +6 -6
  59. package/dist/core/capabilities/item-manifest.d.ts +92 -0
  60. package/dist/core/capabilities/item-manifest.js +247 -0
  61. package/dist/core/capabilities/placement.d.ts +7 -7
  62. package/dist/core/capabilities/projects-local.d.ts +68 -0
  63. package/dist/core/capabilities/projects-local.js +172 -0
  64. package/dist/core/capabilities/publish-build.d.ts +13 -2
  65. package/dist/core/capabilities/publish-build.js +87 -21
  66. package/dist/core/capabilities/publish-download.js +1 -1
  67. package/dist/core/capabilities/runtime-control.d.ts +7 -2
  68. package/dist/core/capabilities/runtime-control.js +27 -1
  69. package/dist/core/capabilities/seeing/args.d.ts +2 -2
  70. package/dist/core/capabilities/store-checks.d.ts +45 -0
  71. package/dist/core/capabilities/store-checks.js +85 -0
  72. package/dist/core/capabilities/store-details.d.ts +37 -0
  73. package/dist/core/capabilities/store-details.js +94 -0
  74. package/dist/core/capabilities/summer-bundle.d.ts +21 -2
  75. package/dist/core/capabilities/summer-bundle.js +54 -7
  76. package/dist/core/capabilities/sync-items.d.ts +89 -0
  77. package/dist/core/capabilities/sync-items.js +352 -0
  78. package/dist/core/capabilities/tool-dispatch.js +66 -6
  79. package/dist/core/capabilities/video-ogv.d.ts +49 -0
  80. package/dist/core/capabilities/video-ogv.js +260 -0
  81. package/dist/core/capability-skew.js +2 -2
  82. package/dist/core/cli-usage-beacon.js +26 -2
  83. package/dist/core/cli-usage.d.ts +5 -2
  84. package/dist/core/cli-usage.js +8 -2
  85. package/dist/core/engine-install.d.ts +53 -0
  86. package/dist/core/engine-install.js +93 -0
  87. package/dist/core/engine-installer.d.ts +134 -0
  88. package/dist/core/engine-installer.js +760 -0
  89. package/dist/core/engine-launch.d.ts +126 -0
  90. package/dist/core/engine-launch.js +398 -0
  91. package/dist/core/engine-self-start.d.ts +63 -0
  92. package/dist/core/engine-self-start.js +282 -0
  93. package/dist/core/engine.d.ts +7 -1
  94. package/dist/core/engine.js +9 -2
  95. package/dist/core/error-codes.d.ts +4 -1
  96. package/dist/core/error-codes.js +7 -3
  97. package/dist/core/library-read.d.ts +2 -2
  98. package/dist/core/library-search.d.ts +2 -2
  99. package/dist/core/local-project.d.ts +50 -0
  100. package/dist/core/local-project.js +234 -0
  101. package/dist/core/ops/areas.js +9 -3
  102. package/dist/core/ops/catalog.d.ts +9 -0
  103. package/dist/core/ops/catalog.js +56 -4
  104. package/dist/core/ops/engine-answer.d.ts +4 -1
  105. package/dist/core/ops/engine-answer.js +16 -3
  106. package/dist/core/ops/engine.js +4 -4
  107. package/dist/core/ops/save-files.d.ts +6 -1
  108. package/dist/core/ops/save-files.js +67 -50
  109. package/dist/core/ops/search.js +9 -2
  110. package/dist/core/preview/input.d.ts +35 -0
  111. package/dist/core/preview/input.js +105 -0
  112. package/dist/core/preview/launch-config.d.ts +52 -0
  113. package/dist/core/preview/launch-config.js +137 -0
  114. package/dist/core/preview/page.d.ts +22 -0
  115. package/dist/core/preview/page.js +232 -0
  116. package/dist/core/preview/preview-source.d.ts +179 -0
  117. package/dist/core/preview/preview-source.js +350 -0
  118. package/dist/core/preview/server.d.ts +26 -0
  119. package/dist/core/preview/server.js +218 -0
  120. package/dist/core/telemetry.d.ts +20 -16
  121. package/dist/core/telemetry.js +92 -16
  122. package/dist/core/templates.d.ts +20 -0
  123. package/dist/core/templates.js +300 -3
  124. package/dist/core/tool-profiles.d.ts +2 -2
  125. package/dist/core/tool-profiles.js +1 -0
  126. package/dist/core/usage-fields.d.ts +41 -0
  127. package/dist/core/usage-fields.js +150 -0
  128. package/dist/core/util/sleep.d.ts +3 -0
  129. package/dist/core/util/sleep.js +12 -0
  130. package/dist/mcp/ops-surface.js +28 -2
  131. package/dist/mcp/server.d.ts +14 -4
  132. package/dist/mcp/server.js +42 -8
  133. package/dist/mcp/tool-surface.d.ts +8 -0
  134. package/dist/mcp/tool-surface.js +19 -2
  135. package/dist/mcp/tools/creator-tools.js +92 -14
  136. package/dist/mcp/tools/debug-tools.js +2 -0
  137. package/dist/mcp/tools/local-project-tools.d.ts +6 -0
  138. package/dist/mcp/tools/local-project-tools.js +33 -0
  139. package/dist/mcp/tools/navigation-tools.js +2 -1
  140. package/dist/mcp/tools/project-tools.js +5 -8
  141. package/dist/mcp/tools/seeing-tools.js +1 -1
  142. package/dist/mcp/tools/visual-tools.js +1 -1
  143. package/dist/mcp/tools/with-engine.js +1 -1
  144. package/gemini-extension.json +3 -1
  145. package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
  146. package/library/skills/README.md +12 -8
  147. package/library/skills/animated-loop/SKILL.md +10 -16
  148. package/library/skills/cinematic-cutscene/SKILL.md +7 -18
  149. package/library/skills/headless-scripting/SKILL.md +7 -4
  150. package/library/skills/in-game-purchases/SKILL.md +25 -8
  151. package/library/skills/in-game-purchases/resource.yaml +2 -1
  152. package/library/skills/launch-ready/SKILL.md +351 -0
  153. package/library/skills/launch-ready/resource.yaml +45 -0
  154. package/library/skills/make-content/SKILL.md +202 -0
  155. package/library/skills/make-content/resource.yaml +32 -0
  156. package/library/skills/make-game/SKILL.md +2 -0
  157. package/library/skills/make-game/resource.yaml +2 -1
  158. package/library/skills/navigate-summer/SKILL.md +4 -2
  159. package/library/skills/new-project/SKILL.md +15 -22
  160. package/library/skills/new-project/resource.yaml +2 -1
  161. package/library/skills/painted-25d-world/SKILL.md +366 -0
  162. package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
  163. package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
  164. package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
  165. package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
  166. package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
  167. package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
  168. package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
  169. package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
  170. package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
  171. package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
  172. package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
  173. package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
  174. package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
  175. package/library/skills/painted-25d-world/resource.yaml +39 -0
  176. package/library/skills/publish-your-game/SKILL.md +13 -14
  177. package/library/skills/publish-your-game/resource.yaml +4 -3
  178. package/library/skills/release-checklist/SKILL.md +188 -0
  179. package/library/skills/release-checklist/resource.yaml +40 -0
  180. package/library/skills/release-readiness/SKILL.md +1 -1
  181. package/library/skills/release-readiness/resource.yaml +1 -1
  182. package/library/skills/store-art/SKILL.md +1 -1
  183. package/library/skills/store-art/resource.yaml +1 -1
  184. package/library/skills/store-listing/SKILL.md +1 -1
  185. package/library/skills/store-listing/resource.yaml +1 -1
  186. package/library/skills/summer-cli/SKILL.md +2 -0
  187. package/library/skills/summer-store/SKILL.md +295 -39
  188. package/library/skills/summer-store/resource.yaml +9 -3
  189. package/library/skills/trailer-shot/SKILL.md +3 -3
  190. package/library/templates/README.md +5 -0
  191. package/library/tools/content-make/resource.yaml +132 -0
  192. package/library/tools/frame-shot/GUIDE.md +1 -1
  193. package/library/tools/go-live/resource.yaml +67 -0
  194. package/library/tools/import-from-url/GUIDE.md +1 -0
  195. package/library/tools/import-from-url-batch/GUIDE.md +1 -1
  196. package/library/tools/play/GUIDE.md +2 -0
  197. package/library/tools/projects-create-local/resource.yaml +55 -0
  198. package/library/tools/projects-open-local/resource.yaml +51 -0
  199. package/library/tools/publish-build/resource.yaml +3 -3
  200. package/library/tools/screenshot/GUIDE.md +1 -1
  201. package/library/tools/store-set-details/resource.yaml +114 -0
  202. package/library/tools/sync-items/resource.yaml +72 -0
  203. package/package.json +1 -1
  204. package/registry/generated/aliases.json +6 -0
  205. package/registry/generated/counts.json +3 -3
  206. package/registry/generated/gemini-extension.json +3 -1
  207. package/registry/generated/index.json +463 -37
  208. package/registry/generated/marketplace.claude.json +1 -1
  209. package/registry/generated/plugin.claude-directory.json +4 -2
  210. package/registry/generated/plugin.claude.json +3 -1
  211. package/registry/generated/plugin.codex.json +5 -3
  212. package/registry/generated/plugin.cursor.json +4 -2
  213. package/registry/generated/plugin.factory.json +4 -2
  214. package/registry/generated/server.json +2 -2
  215. package/registry/generated/skills-index.md +12 -8
  216. package/registry/generated/skills-registry.json +62 -5
  217. package/registry/operations/legacy-names.json +40 -0
  218. package/registry/operations/public-catalog.json +10144 -387
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: summer-store
3
- description: "Sell Summer items for Sparks in a multiplayer game: shop, checkout, owned items, authority entitlement checks."
3
+ description: "Sell Summer items for Sparks: shop, checkout, item slugs, bundles and sales, owned items, summer.items.json sync, authority checks."
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: multiplayer-and-networking
7
7
  user-invocable: true
8
- allowed-tools: Read Grep Edit Write summer_get_script_errors summer_project_setting summer_play summer_stop summer_get_diagnostics
9
- paths: ["**/*.gd", ".summer/test-users.json"]
8
+ allowed-tools: Read Grep Edit Write summer_get_script_errors summer_project_setting summer_play summer_stop summer_get_diagnostics summer_run_script summer_sync_items
9
+ paths: ["**/*.gd", ".summer/test-users.json", "summer.items.json"]
10
10
  ---
11
11
 
12
12
  # /summer-store — sell items, check what players own
@@ -22,12 +22,13 @@ balances or approval:
22
22
  4. Summer charges the Sparks and issues the item into the player's **inventory**.
23
23
  5. The game sees the item in `Summer.client.items` (client) and `Summer.authority.items` (authority).
24
24
 
25
- Three ids, kept apart:
25
+ The ids, kept apart:
26
26
 
27
27
  | Id | Where it comes from | Use it for |
28
28
  | --- | --- | --- |
29
+ | Slug ("Item key" in Studio) | You choose it, e.g. `party_hat`; the same in staging and production | Recognizing an item in game code |
30
+ | Definition id | Summer assigns it (`item_…`); **differs between staging and production** | The same, on Summer versions without the slug getters |
29
31
  | Offer id | `item.price.offer_id` in the store listing | `SummerCheckoutLine.create(offer_id, quantity)` |
30
- | Definition id | Your item definition in the Summer catalog | "Does this player own a hat?" |
31
32
  | Owned item id | One owned copy (`item.get_owned_item_id()`) | Pointing at a specific owned copy |
32
33
 
33
34
  The rule that makes it cheat-proof: **the authority decides what a player may
@@ -39,15 +40,154 @@ proves nothing.
39
40
  | Surface | Store and checkout | Inventory |
40
41
  | --- | --- | --- |
41
42
  | Hosted | Need the Summer app's `player.store.read@1` / `player.store.checkout@1` | Needs `player.items@1` |
42
- | Local Play | `unavailable` | Works from a fixture: empty unless `.summer/test-users.json` lists item templates (step 5) |
43
+ | Local Play | `unavailable` | Works from a fixture: empty unless `.summer/test-users.json` lists item templates (step 7) |
43
44
  | Guests, older Summer apps | Readiness reports `unavailable` | Readiness reports `unavailable` |
44
45
 
45
- Items and offers are created in your game's Studio pages (`studio.open` target
46
- `game`). Code never creates items.
47
-
48
46
  This skill extends the Courtyard game from `multiplayer-project` and
49
47
  `multiplayer-state`.
50
48
 
49
+ ## Where items come from
50
+
51
+ **Game code never creates or grants items at runtime.** The creator defines
52
+ them, Summer reviews each one, and the owner activates it. Two ways in:
53
+
54
+ | Way | How | Who writes |
55
+ | --- | --- | --- |
56
+ | Studio | The game's item pages (`studio.open` target `game`): create, Item key, price, submit for review | The owner |
57
+ | `summer.items.json` | A file in the project root, synced with `summer_sync_items` (CLI `summer economy sync-items`) | An agent **proposes**; the owner approves in Studio, and Studio applies it |
58
+
59
+ An agent never writes an item or a price itself. Bundles and sales (step 5)
60
+ are not in the file; they belong to the store page draft, which the owner
61
+ publishes.
62
+
63
+ ### How an item becomes buyable
64
+
65
+ Today an item goes on sale only after all of these:
66
+
67
+ 1. **Defined.** In Studio, or from a sync plan the owner approved (Studio
68
+ applies it). Approving the plan does not put anything on sale.
69
+ 2. **Reviewed.** Each item is submitted for Summer's review. Its page shows
70
+ the state; "Check verification status" reads the result.
71
+ 3. **Activated, one item at a time.** Once Summer approves an item, the owner
72
+ presses **Review activation** on that item's page in Studio and confirms.
73
+ Nothing activates it for them: not the sync, not an agent.
74
+ 4. **The Game is live.** An active item is buyable only while its Game's
75
+ store page is live. It stops selling while the Game is unlisted and
76
+ resumes when the Game is listed again. An item can be activated before
77
+ the Game goes live; it sells from the moment the Game is live.
78
+
79
+ **Editing a live item makes a new revision.** In Studio that is "Review new
80
+ revision"; in a sync plan it is an `update` with `"revision": "new_draft"`.
81
+ Players keep buying the live revision while the new one is reviewed, and
82
+ the new one replaces it only after the owner's Review activation of it.
83
+
84
+ **Coming:** Summer has decided that items will go on sale without waiting for
85
+ Summer's review. Staff will review an item after it goes live and can take
86
+ it down. The store change that does this is being built and has not shipped.
87
+ Until it ships, follow the four steps above. When it ships, steps 2 and 3
88
+ no longer hold up the sale; a live Game is still required.
89
+
90
+ ### `summer.items.json`
91
+
92
+ ```json
93
+ {
94
+ "schema": "summer.items/v1",
95
+ "items": [
96
+ {
97
+ "slug": "party_hat",
98
+ "name": "Party hat",
99
+ "description": "A paper crown for the courtyard.",
100
+ "class": "COSMETIC",
101
+ "cosmetic": {"familyId": "hat", "variantKey": "party"},
102
+ "ownershipMode": "INSTANCE",
103
+ "ownership": "OWNED",
104
+ "priceSparks": 150,
105
+ "media": ["art/items/party_hat.png"]
106
+ },
107
+ {
108
+ "slug": "confetti",
109
+ "name": "Confetti",
110
+ "description": "One burst of confetti.",
111
+ "ownershipMode": "QUANTITY",
112
+ "consumable": true,
113
+ "priceSparks": 10,
114
+ "media": ["art/items/confetti.png"]
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ | Field | Rule |
121
+ | --- | --- |
122
+ | `slug` | Required, unique in the file: 1–64 lowercase letters and digits, words joined by one `.`, `_` or `-`, starting with a letter |
123
+ | `name`, `description` | Required; at most 120 and 5000 characters |
124
+ | `class` | `STANDARD` (default) or `COSMETIC`; a cosmetic needs `cosmetic.familyId` and `variantKey` |
125
+ | `ownershipMode` | `INSTANCE` (default; one copy is one owned item) or `QUANTITY` (a stack). Class and mode never change later |
126
+ | `maxOwned` | Optional cap on copies per player (step 4) |
127
+ | `ownership` | `SOULBOUND`, `OWNED` (default) or `MARKETABLE`; `consumable` is true or false |
128
+ | `supply` | Optional lifetime limit; omit for unlimited |
129
+ | `priceSparks` | Whole Sparks. Omitted: a new item is not sold, an existing price stays |
130
+ | `media` | Project-relative `.png`, `.jpg`, `.webp` or `.glb`, in store order. **A new item needs at least one image** |
131
+
132
+ ### Sync it
133
+
134
+ 1. **Plan (dry run).** `summer_sync_items {"gameId": "game_..."}` reads the
135
+ file and the Game's items by slug, and changes nothing. It answers create,
136
+ update (with the changed fields), unchanged, blocked (with the reason),
137
+ and the Game's items the file does not name (listed, never changed).
138
+ 2. **Show the plan to the user** and ask before sending it.
139
+ 3. **Confirm.** `summer_sync_items {"gameId": "game_...", "confirm": true}`
140
+ uploads the changed images and sends the plan to the owner. The answer is
141
+ `awaiting_owner_approval` with an approval link: give it to the owner.
142
+ Nothing changes until they approve in Studio. Then each item is created
143
+ or revised, priced and submitted for Summer's review. Each one still
144
+ needs Summer's approval and the owner's Review activation before players
145
+ can buy it ("How an item becomes buyable").
146
+ 4. **One approval takes at most 50 items.** A larger plan lists `batches`;
147
+ confirm each one with `"slugs": [...]` and pass on each approval link.
148
+
149
+ Activating an item (Review activation), retiring it, and the Game's own
150
+ price are not part of the sync. The owner does those in Studio. A Passport
151
+ collectible (a stamp, pin or badge) with the same slug is **blocked**:
152
+ Passport items have their own lifecycle and are never synced; give the
153
+ store item another slug.
154
+
155
+ **Staging and production.** One file serves both, because items match by
156
+ slug. Game ids differ, so take `gameId` from that environment's game list.
157
+ The machine holds one store sign-in. To switch, set `SUMMER_GATEWAY_URL` and
158
+ `SUMMER_MCP_RESOURCE_URL` for the other environment, then run
159
+ `summer login --store --force`. Until then the sync stops with
160
+ `store_login_other_environment`.
161
+
162
+ ### Slugs
163
+
164
+ - Set in Studio ("Item key", on create or on the item page) or from the file.
165
+ - Unique within the Game, and the same in staging and production.
166
+ - **Frozen once the item's first publication is claimed**, even if that
167
+ publication fails or the item is retired. Before that it can change, and an
168
+ item without a slug can get one at any time.
169
+ - Never reused by another item of the Game, so game code and the authority
170
+ can safely name items by slug.
171
+
172
+ **Availability: the platform sends slugs; the game needs the getters.**
173
+ Since 2026-10-16 Summer sends every item's slug to games: the store listing
174
+ carries `StoreItem.slug` and the inventory `InventoryItem.definition_slug`.
175
+ Game code reads them with `SummerStoreItem.get_slug()`,
176
+ `SummerInventoryItem.get_slug()`, `SummerClientItems.has_owned_slug()` and
177
+ `SummerItemSelection.allowed_slugs`. Those getters are not in Summer 0.7.3;
178
+ they come with the next Summer Engine release that carries them (0.7.4, in
179
+ progress). Check the version the game runs on before relying on them. Local
180
+ Play fixtures carry slugs too (step 7).
181
+
182
+ | Check | Result |
183
+ | --- | --- |
184
+ | `summer_run_script {"scenePath": "res://main.tscn", "source": "func run(ctx):\n\tctx.report(\"slugs\", ClassDB.class_has_method(\"SummerClientItems\", \"has_owned_slug\"))"}` | `slugs: true`: this Summer version has the getters |
185
+ | No MCP: write the slug code, then `summer_get_script_errors` | A "not found" error on `has_owned_slug` or `get_slug`: this version lacks them |
186
+ | Hosted: `get_slug()` returns `""` | That item has no Item key yet: set one in Studio or in `summer.items.json` |
187
+
188
+ Without the getters, use the definition-id variants in steps 1–3, with each
189
+ environment's definition ids.
190
+
51
191
  ## Steps
52
192
 
53
193
  ### 1. The client shop
@@ -60,7 +200,7 @@ extends Node
60
200
  ## A shop for Summer items, paid in Sparks. Summer shows its own purchase
61
201
  ## sheet; the game never sees money and never grants an item itself.
62
202
 
63
- signal shelf_changed(rows: Array) # [{name, sparks, offer_id}]
203
+ signal shelf_changed(rows: Array) # [{name, sparks, offer_id, slug, owned}]
64
204
  signal checkouts_changed(busy: bool, rows: Array)
65
205
  signal status(text: String)
66
206
 
@@ -96,7 +236,8 @@ func open() -> void:
96
236
  var rows := []
97
237
  for item in page.items:
98
238
  if item.price != null and item.price.offer_id != "":
99
- rows.append({"name": item.name, "sparks": item.price.sparks_amount, "offer_id": item.price.offer_id})
239
+ rows.append({"name": item.name, "sparks": item.price.sparks_amount, "offer_id": item.price.offer_id,
240
+ "slug": item.slug, "owned": owns(item.slug)})
100
241
  shelf_changed.emit(rows)
101
242
  if items.check_refresh_readiness().ok:
102
243
  items.refresh()
@@ -122,22 +263,36 @@ func _render_checkouts(operation: SummerCheckoutOperation = null) -> void:
122
263
  for checkout in view.statuses:
123
264
  rows.append({"id": checkout.request_id, "state": checkout.state, "error": checkout.error_code})
124
265
  checkouts_changed.emit(view.has_pending_checkouts(), rows)
125
- if view.submission_failure != null:
126
- status.emit("The purchase did not start: %s" % view.submission_failure.code)
266
+ var failure := view.submission_failure
267
+ if failure != null:
268
+ var reason: StringName = failure.service_error.reason if failure.service_error != null else failure.code
269
+ if reason == &"already_owned":
270
+ status.emit("You already own this.") # show the row as Owned; nothing was charged
271
+ else:
272
+ status.emit("The purchase did not start: %s" % reason)
127
273
 
128
274
 
129
275
  ## Ownership comes only from the inventory, never from a checkout result.
130
- func owns(definition_id: String) -> bool:
131
- return items.is_projection_loaded() and items.has_owned_definition(definition_id)
276
+ func owns(slug: String) -> bool:
277
+ return items.is_projection_loaded() and items.has_owned_slug(slug)
132
278
 
133
279
 
134
280
  func _on_items_changed() -> void:
135
281
  status.emit("Inventory updated.")
136
282
  ```
137
283
 
284
+ On a Summer version without slugs, drop `"slug"` from the rows, pass the
285
+ item's per-environment definition id, and use:
286
+
287
+ ```gdscript
288
+ func owns(definition_id: String) -> bool:
289
+ return items.is_projection_loaded() and items.has_owned_definition(definition_id)
290
+ ```
291
+
138
292
  Bind `checkouts_changed` to the Buy buttons: disable them while `busy` is
139
- true. Update rows by `id`; a status can be delivered again after a crash, so
140
- rendering must be idempotent. Show each checkout state:
293
+ true, and for rows with `owned` true. Update rows by `id`; a status can be
294
+ delivered again after a crash, so rendering must be idempotent. Show each
295
+ checkout state:
141
296
 
142
297
  | `state` | Show |
143
298
  | --- | --- |
@@ -150,11 +305,13 @@ rendering must be idempotent. Show each checkout state:
150
305
 
151
306
  ### 2. Unlock from the inventory only
152
307
 
153
- Unlock cosmetics in menus with `owns(definition_id)`, and refresh the menu on
308
+ Unlock cosmetics in menus with `owns(slug)`, and refresh the menu on
154
309
  `refreshed`, `item_replaced` and `item_removed`. Never unlock from a
155
310
  `purchased` status: the receipt proves the debit, the inventory proves
156
- ownership, including after a reinstall or on another device. An empty inventory
157
- before `is_projection_loaded()` is "still loading", not "owns nothing".
311
+ ownership, including after a reinstall or on another device. An empty
312
+ inventory before `is_projection_loaded()` is "still loading", not "owns
313
+ nothing". Match by slug, never by item name, and never hard-code one
314
+ environment's definition ids.
158
315
 
159
316
  ### 3. The authority checks entitlement
160
317
 
@@ -164,13 +321,20 @@ the authority reads that Session's inventory. Add `"equip"` to the `match` in
164
321
 
165
322
  ```gdscript
166
323
  "equip":
167
- _equip(request, str(command.get("definition", "")))
324
+ _equip(request, str(command.get("hat", "")))
168
325
  ```
169
326
 
170
327
  ```gdscript
328
+ ## The hats this game knows, by slug: the same in staging and production.
329
+ const HATS := ["party_hat", "top_hat"]
330
+
331
+
171
332
  ## A player asks to wear a hat. The authority reads that Session's inventory
172
333
  ## itself; a client saying "I own it" proves nothing.
173
- func _equip(request: SummerNetworkCommandRequest, definition_id: String) -> void:
334
+ func _equip(request: SummerNetworkCommandRequest, slug: String) -> void:
335
+ if not HATS.has(slug):
336
+ request.refuse(&"unknown_item")
337
+ return
174
338
  var session := request.get_session()
175
339
  var read := Summer.authority.items.inventory_for_session(session)
176
340
  var result: SummerResult = await read.get_result_or_completed_signal()
@@ -178,39 +342,105 @@ func _equip(request: SummerNetworkCommandRequest, definition_id: String) -> void
178
342
  request.refuse(&"inventory_unavailable") # retryable: try again later
179
343
  return
180
344
  for item in read.items:
181
- if item.is_owned() and item.get_definition_id() == definition_id:
345
+ if item.is_owned() and item.get_slug() == slug:
182
346
  var hats: Dictionary = match_doc.get("hats", {})
183
- hats[session.player.user_id] = definition_id
347
+ hats[session.player.user_id] = slug
184
348
  match_doc["hats"] = hats
185
349
  match_group.reset(Game.pack(match_doc))
186
- request.accept({}, Game.pack({"equipped": definition_id}))
350
+ request.accept({}, Game.pack({"equipped": slug}))
187
351
  return
188
352
  request.refuse(&"not_owned")
189
353
  ```
190
354
 
191
- The client sends `await command({"c": "equip", "definition": definition_id})`
192
- and every client draws hats from `match_doc.hats`. Other players see the hat
193
- only because the authority published it.
355
+ The client sends `await command({"c": "equip", "hat": slug})` and every client
356
+ draws hats from `match_doc.hats`. Other players see the hat only because the
357
+ authority published it.
358
+
359
+ **Without slugs** (Summer versions without the getters, or an item with no Item key):
360
+ keep each environment's definition ids, send the definition id, and compare
361
+ `item.get_definition_id() == definition_id` instead of `item.get_slug() == slug`.
362
+
363
+ For an equipment slot, `SummerItemSelection` does this check for you: set
364
+ `selection.allowed_slugs = PackedStringArray(["party_hat"])`, then call
365
+ `configure_slot` with an empty definition allowlist. An item whose inventory
366
+ carries no slug never matches, so the slot fails closed until it has one.
194
367
 
195
368
  `Summer.authority.items.inventory_changed(session, change)` hints that a
196
369
  player's inventory changed during the match. To drop an item that was
197
370
  refunded mid-match, re-read with `inventory_for_session` (or
198
371
  `refresh_inventory`) on that hint.
199
372
 
200
- ### 4. Rules and why
373
+ ### 4. Owned items and `maxOwned`
201
374
 
202
- - **Offer ids and quantities only.** Never send a price, an amount or a buyer. The platform prices everything, and a client-side price is what a cheater edits.
375
+ | Item | Can a player buy another? |
376
+ | --- | --- |
377
+ | Unique (`INSTANCE`, not consumable) | No, once they own one |
378
+ | `maxOwned` N (not stackable, not consumable) | Until they own N |
379
+ | Stackable (`QUANTITY`) or consumable | Yes; unaffected |
380
+
381
+ Summer counts owned copies plus purchases still settling. A checkout with no
382
+ room left is refused with **`409 already_owned`** before anything is charged,
383
+ and of two simultaneous purchases of the last copy, one is accepted. In game
384
+ code that is a `submission_failure` whose reason is `already_owned` (step 1):
385
+ show "Owned", not an error. The Summer apps' shops already show **Owned**
386
+ instead of Buy; in your own shop, set `owned` from the inventory and disable Buy.
387
+
388
+ ### 5. Bundles and timed sales
389
+
390
+ They live on the store page draft; saving never publishes. Players see them
391
+ once the owner publishes the store page again (`store.publish`, owner-approved).
392
+
393
+ **In Studio** (`studio.open` target `/studio/store/:gameId`, the **Store page** tab):
394
+
395
+ - **Bundles** card: **Add bundle**, name it (the key comes from the name and is fixed once published), pick the Items (one without a price can't be picked; a stackable takes a quantity), set the Sparks price. The line under the price shows the Items total and "Save N%". Optional: a minimum price, and "On sale from" / "Until" dates. **Save bundles**. Removing a bundle removes its sales too.
396
+ - **Sales** card: **Add sale**, pick an Item or a bundle, the percent off and the start and end dates; it shows the result ("400 → 280 Sparks"). **Save sales**. A sale that has ended is dropped on save.
397
+ - **Sold only in bundles:** the checkbox on an Item's row in a bundle, or the toggle on the Item's page. A refusal shows at the field to fix.
398
+
399
+ **As an agent:** ask the owner before saving, then use `store.set_bundles` and `store.set_sales`.
400
+ `store.get_listing` returns `itemStore`: the bundles with `itemsTotalSparks` and `savePercent`, the sales with `listSparks` and `saleSparks`, `bundleOnlyItemIds`, and the Game's Items with ids and Sparks prices.
401
+ Each call **replaces the whole list**, so send every bundle or sale to keep; `[]` clears it. A bundle without a key keeps the saved key of the same name; `bundleOnlyItemIds` on `store.set_bundles` sets the bundle-only Items. A refusal names the field, e.g. `storeBundles[0].priceSparks`.
402
+
403
+ | | Rules |
404
+ | --- | --- |
405
+ | **Bundle** | A fixed list of 1–16 of the Game's items, 2–64 units in all (1 of a unique item, up to its stack for a stackable). A stable key, a name of at most 40 characters, up to 20 per Game. A purchased bundle never changes |
406
+ | Bundle price | The creator's Sparks price: at least 1 Spark per unit, below what its items cost alone. Shown with the items' total crossed out and "Save N%". Optional minimum price, and an optional available-from/until window (a limited-time bundle) |
407
+ | **Sale** | 1–90% off one item or one bundle, between a start and an end at most 90 days apart. The old price shows crossed out, with the end time |
408
+ | **Bundle-only item** | Sold only inside its bundles, never listed or bought alone (`409 not_sold_alone`) |
409
+
410
+ **A player who owns some of a bundle's unique items** gets those left out,
411
+ and the price drops by their share of the regular base prices:
412
+ `bundle price × base of the items not owned ÷ base of all its items`. It is
413
+ computed exactly, held at least at the minimum and at 1 Spark per unit, then
414
+ rounded up once, and never costs more than buying the remaining items
415
+ separately. Stackable and consumable items always count. Owning everything
416
+ shows **Owned**. Only the bundle total is promised; each item's share on the
417
+ receipt just splits it.
418
+
419
+ **Where players see them:** the Game shop in the Summer apps (website,
420
+ desktop, iPhone, Android) shows a Bundles row, "Save N%", "You own 2 of 5",
421
+ "Ends in …", and "This offer ended" (nothing charged) when a sale or bundle
422
+ window closes before the purchase.
423
+
424
+ **In your in-game shop:** `list_items` shows a running sale's price as the
425
+ item's price (no crossed-out price) and leaves bundle-only items out. Bundles
426
+ are not sold in game yet; point players to the Game shop in the Summer app.
427
+
428
+ ### 6. Rules and why
429
+
430
+ - **Offer ids and quantities only.** Never send a price, an amount or a buyer. The platform prices everything, sales and bundles included, and a client-side price is what a cheater edits.
203
431
  - **One `request_checkout` is one purchase.** Each call gets a new identity. Never call it again because a result is slow; `is_outcome_unknown()` means it may already be charged, and the status will still arrive.
204
432
  - **Connect `checkout_status_changed` normally, before reading statuses.** A synchronous handler is how the SDK knows the result was delivered; a deferred-only handler leaves it undelivered.
205
433
  - **`pending` and `manual_review` are not failures.** Don't show success or failure until a terminal state.
206
434
  - **Carts:** 1–16 different offers, at most 64 units, as one atomic purchase.
207
435
  - **Hide what's unavailable.** `check_browse_readiness()` and `check_checkout_readiness()` return a `SummerResult`; show the shop only when they're `ok`.
208
436
 
209
- ### 5. Test
437
+ ### 7. Test
210
438
 
211
439
  **Local Play.** The store and checkout report `unavailable`, so the hidden-shop
212
440
  path is what you test. Inventory and the authority check work from a fixture:
213
- create `.summer/test-users.json` with item templates.
441
+ create `.summer/test-users.json` with item templates. Give them slugs with
442
+ `catalog_item_slugs`, only on a Summer version with slug getters; older ones
443
+ refuse the field.
214
444
 
215
445
  ```json
216
446
  {
@@ -221,6 +451,7 @@ create `.summer/test-users.json` with item templates.
221
451
  "world_definition": "world.json",
222
452
  "service_outcomes": [],
223
453
  "catalog_item_templates": ["hat"],
454
+ "catalog_item_slugs": [{"template": "hat", "slug": "party_hat"}],
224
455
  "personas": [
225
456
  {"persona_id": "alice", "display_name": "Alice", "participant_role": "player", "eligible_for_outcome": true, "session_lifetime": "match_bound"},
226
457
  {"persona_id": "bob", "display_name": "Bob", "participant_role": "player", "eligible_for_outcome": true, "session_lifetime": "match_bound"}
@@ -231,29 +462,48 @@ create `.summer/test-users.json` with item templates.
231
462
  What the fixture gives you:
232
463
 
233
464
  - Every persona owns one item per template.
234
- - Ids are local stand-ins. Bob's hat has owned item id `local-item:bob:0`, definition id `local-definition:courtyard:0`, and `item_template_version_id` `"hat"`.
235
- - A check against your real catalog definition id is therefore `false` in Local Play. To exercise the authority path locally, equip with the local definition id the client reads from `items.get_owned_items()`.
236
-
237
- **Staging.** Test real purchases there; Summer staging uses sandbox payments.
465
+ - Each item has the slug you gave its template: Bob's hat is `party_hat`, so slug code behaves as it will once slugs are delivered.
466
+ - Other ids are local stand-ins: owned item id `local-item:bob:0`, definition id `local-definition:courtyard:0`, `item_template_version_id` `"hat"`. Without slugs, equip with the local definition id the client reads from `items.get_owned_items()`.
467
+
468
+ **Staging.** Summer staging is a separate environment: its own sign-in,
469
+ Games and definition ids (one `summer.items.json` still serves both). Your
470
+ production account and Games are not in it; if you have no staging account,
471
+ ask Summer for access. Purchases there use test payments: Sparks bought in
472
+ staging are sandbox Sparks, nothing real is charged, and they earn the
473
+ creator nothing. Test a purchase with a staging account that bought Sparks
474
+ in the staging Summer app.
475
+
476
+ Before testing, the item must be buyable in staging: reviewed, activated by
477
+ the owner, and the staging Game live ("How an item becomes buyable").
238
478
  Walk through every outcome:
239
479
 
240
480
  - decline the sheet;
241
481
  - buy with too few Sparks;
482
+ - buy a unique item you already own: `already_owned`, nothing charged;
242
483
  - buy, then kill the game while the purchase is pending and relaunch: the result must arrive exactly once.
243
484
 
244
485
  ## Checklist
245
486
 
246
487
  - [ ] Local Play: the shop button is hidden, and there are no script errors.
247
488
  - [ ] Local Play with the fixture: `items.refresh()` succeeds, the owned item is listed, `equip` is accepted for it and refused with `not_owned` for anything else.
248
- - [ ] No code path sends a price or grants an item from a checkout status.
249
- - [ ] Buy is disabled while `has_pending_checkouts()` is true.
250
- - [ ] `manual_review` shows "confirming", not success or failure.
489
+ - [ ] Game code names items by slug, or by each environment's definition ids where slugs are not available.
490
+ - [ ] No code path creates an item, sends a price, or grants an item from a checkout status.
491
+ - [ ] Buy is disabled while `has_pending_checkouts()` is true, and for owned unique items.
492
+ - [ ] `manual_review` shows "confirming", and `already_owned` shows "Owned".
493
+ - [ ] Every `summer.items.json` sync: the dry-run plan was shown, then confirmed, then the owner got the approval link.
494
+ - [ ] Every new or changed item: Summer approved it, the owner pressed Review activation, and the Game is live.
251
495
  - [ ] Staging: a purchase shows up in the inventory, and the equipped item appears for every player.
252
496
 
253
497
  ## Common mistakes
254
498
 
255
499
  | Don't | Do | Why |
256
500
  | --- | --- | --- |
501
+ | Create or grant items from game code | Define them in Studio, or in `summer.items.json` synced with `summer_sync_items` | Items are reviewed, and the owner approves every change |
502
+ | Hard-code definition ids | Match by slug | Definition ids differ between staging and production |
503
+ | Assume slug getters work everywhere | Check the Summer version; fall back to definition ids without them | 0.7.3 and older lack them; an item without an Item key reads `""` |
504
+ | Tell the owner an approved sync plan is on sale | Say each item still needs Summer's review, Review activation, and a live Game | Approving the plan only creates or revises the items |
505
+ | Confirm a sync without showing the plan | Dry run, show it, then `confirm` | The plan is what the owner approves |
506
+ | Treat `already_owned` as an error | Show "Owned" | A unique item is one per player |
257
507
  | Unlock on `purchased` | Unlock from `Summer.client.items` | Ownership lives in the inventory; receipts can replay |
258
508
  | Let the client tell the authority what it owns | `Summer.authority.items.inventory_for_session(session)` | Client claims are what cheaters forge |
259
509
  | Send prices or amounts | Offer id + quantity only | The platform prices every purchase |
@@ -277,10 +527,16 @@ No Summer MCP tools in this session? Run each one as a command; when `summer --v
277
527
 
278
528
  | In this skill | Command |
279
529
  |---|---|
530
+ | `summer_sync_items` | `summer economy sync-items` |
280
531
  | `summer_get_diagnostics` | `summer engine get-diagnostics` |
281
532
  | `summer_get_script_errors` | `summer engine get-script-errors` |
282
533
  | `summer_play` | `summer engine play` |
283
534
  | `summer_project_setting` | `summer engine project-setting` |
535
+ | `summer_run_script` | `summer engine run-script` |
284
536
  | `summer_stop` | `summer engine stop` |
537
+ | `store.get_listing` | `summer store get-listing` |
538
+ | `store.publish` | `summer store publish` |
539
+ | `store.set_bundles` | `summer store set-bundles` |
540
+ | `store.set_sales` | `summer store set-sales` |
285
541
  | `studio.open` | `summer studio open` |
286
542
  <!-- /shell-commands -->
@@ -1,11 +1,14 @@
1
1
  id: skill/summer-store
2
2
  kind: skill
3
- version: 1.0.0
4
- summary: "Sell Summer items for Sparks in a multiplayer game: shop, checkout, owned items, authority entitlement checks."
3
+ version: 1.2.0
4
+ summary: "Sell Summer items for Sparks: shop, checkout, item slugs, bundles and sales, owned items, summer.items.json sync, authority checks."
5
5
  use_when:
6
6
  - "adding a shop, in-game purchases, cosmetics or skins sold for Sparks to a Summer game"
7
7
  - "checking on the authority that a player really owns an item before letting them use or equip it"
8
- - "checkout shows pending, manual_review, insufficient_funds or unavailable, or bought items don't appear"
8
+ - "checkout shows pending, manual_review, insufficient_funds, already_owned or unavailable, or bought items don't appear"
9
+ - "recognizing items in game code by slug (item key) so one build works in staging and production"
10
+ - "defining items in summer.items.json and syncing them to the store for the owner's approval"
11
+ - "store bundles, Save %, timed sales, bundle-only items, or Owned instead of Buy"
9
12
  facets:
10
13
  lifecycle:
11
14
  - build
@@ -17,7 +20,10 @@ facets:
17
20
  compatibility:
18
21
  engine: ">=4.6"
19
22
  related:
23
+ tools:
24
+ - tool/sync-items
20
25
  skills:
26
+ - skill/in-game-purchases
21
27
  - skill/multiplayer
22
28
  - skill/multiplayer-state
23
29
  - skill/multiplayer-testing
@@ -114,11 +114,11 @@ If "regenerate", revise *one* axis (camera move, lighting, or framing) — don't
114
114
 
115
115
  Trailer footage usually lives on the user's local disk and goes straight into the NLE, where the generated `.mp4` is exactly what you want.
116
116
 
117
- For shots that double as **in-engine** splash content, transcode first — Summer Engine has no MP4 or WebM loader, and `.ogv` (Ogg Theora) is the only video container it can open. `summer_import_from_url` runs the engine's import pipeline and rejects the file if it never becomes a loadable resource, so importing the `.mp4` fails outright rather than half-working.
117
+ For shots that double as **in-engine** splash content, import the `.mp4` as it is. Summer Engine plays video only as Ogg Theora (`.ogv`), so the import converts it and saves it at the path with `.ogv`; the answer names it in `videoConversions`:
118
118
 
119
119
  ```
120
- ffmpeg -i hero_boss_reveal.mp4 -c:v libtheora -q:v 8 -an hero_boss_reveal.ogv
121
- summer_import_from_url(url="<hosted .ogv url>", path="res://assets/video/trailers/hero_boss_reveal.ogv")
120
+ summer_import_from_url(url="<the shot's fileUrl>", path="res://assets/video/trailers/hero_boss_reveal.mp4")
121
+ → res://assets/video/trailers/hero_boss_reveal.ogv
122
122
  ```
123
123
 
124
124
  For pure trailer use (DaVinci / Premiere), the user can grab the `fileUrl` and download directly.
@@ -18,6 +18,11 @@ runtime (`src/core/templates.ts`).
18
18
  2. Recompute `tree_digest` with the formula below and **refuse** on mismatch — the
19
19
  directory is removed and nothing is written.
20
20
  3. `git checkout <commit>` (detached). `.git` is removed unless `--keep-git`.
21
+
22
+ Without `--keep-git`, a github.com pin is first tried without git: the codeload
23
+ tarball of exactly `commit` is unpacked beside the target, the same digest is
24
+ computed from the archive (blob ids, bytewise path order, git's default path
25
+ quoting) and must match strictly; any failure falls back to the git steps above.
21
26
  3. **Built-in template** (`builtin: true`, no pin): generated in-process by
22
27
  `BUILTIN_GENERATORS` in `src/cli/commands/create.ts` — no download.
23
28
  4. **Record** the pin into the project's `.summer/project.json`