summer-engine 3.5.1 → 3.6.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 (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -2
  3. package/.codex-plugin/plugin.json +4 -3
  4. package/.cursor-plugin/plugin.json +3 -2
  5. package/.factory-plugin/plugin.json +3 -2
  6. package/CHANGELOG.md +22 -1
  7. package/commands/summer.md +1 -0
  8. package/dist/core/capabilities/sfx-choices.d.ts +91 -0
  9. package/dist/core/capabilities/sfx-choices.js +291 -0
  10. package/dist/core/capabilities/tool-dispatch.js +35 -0
  11. package/dist/mcp/hosted-mount.js +2 -1
  12. package/dist/mcp/provider-safe-schema.d.ts +10 -0
  13. package/dist/mcp/provider-safe-schema.js +96 -0
  14. package/dist/mcp/server.js +3 -0
  15. package/dist/mcp/tools/generate-tools.js +70 -3
  16. package/gemini-extension.json +2 -1
  17. package/library/references/mcp-tools-reference/mcp-tools-reference.md +5 -3
  18. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  19. package/library/skills/README.md +4 -3
  20. package/library/skills/ambient-bed/SKILL.md +3 -1
  21. package/library/skills/ambient-bed/resource.yaml +1 -1
  22. package/library/skills/audio-direction/SKILL.md +5 -5
  23. package/library/skills/audio-direction/resource.yaml +1 -1
  24. package/library/skills/export-and-ship/SKILL.md +4 -0
  25. package/library/skills/make-game/SKILL.md +21 -2
  26. package/library/skills/make-game/resource.yaml +1 -0
  27. package/library/skills/multiplayer/SKILL.md +3 -1
  28. package/library/skills/multiplayer/resource.yaml +1 -0
  29. package/library/skills/multiplayer-movement/SKILL.md +1 -1
  30. package/library/skills/multiplayer-movement/files.md +2 -3
  31. package/library/skills/multiplayer-project/SKILL.md +20 -4
  32. package/library/skills/multiplayer-project/files.md +2 -3
  33. package/library/skills/multiplayer-publish/SKILL.md +90 -5
  34. package/library/skills/multiplayer-publish/resource.yaml +1 -0
  35. package/library/skills/multiplayer-state/SKILL.md +1 -1
  36. package/library/skills/multiplayer-state/files.md +2 -3
  37. package/library/skills/multiplayer-testing/SKILL.md +13 -0
  38. package/library/skills/multiplayer-testing/resource.yaml +1 -0
  39. package/library/skills/publish-your-game/SKILL.md +2 -0
  40. package/library/skills/release-readiness/SKILL.md +437 -0
  41. package/library/skills/release-readiness/resource.yaml +44 -0
  42. package/library/skills/sound-effect/SKILL.md +83 -158
  43. package/library/skills/sound-effect/resource.yaml +8 -2
  44. package/library/skills/summer-parties/SKILL.md +1 -1
  45. package/library/skills/ui-basics/SKILL.md +1 -0
  46. package/library/tools/generate-audio/resource.yaml +6 -2
  47. package/library/tools/generate-sfx-choices/resource.yaml +69 -0
  48. package/library/tools/get-choices/resource.yaml +50 -0
  49. package/package.json +1 -1
  50. package/registry/generated/aliases.json +2 -0
  51. package/registry/generated/counts.json +3 -3
  52. package/registry/generated/gemini-extension.json +2 -1
  53. package/registry/generated/index.json +163 -24
  54. package/registry/generated/marketplace.claude.json +2 -2
  55. package/registry/generated/plugin.claude.json +3 -2
  56. package/registry/generated/plugin.codex.json +4 -3
  57. package/registry/generated/plugin.cursor.json +3 -2
  58. package/registry/generated/plugin.factory.json +3 -2
  59. package/registry/generated/skills-index.md +4 -3
  60. package/registry/generated/skills-registry.json +15 -1
@@ -0,0 +1,96 @@
1
+ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2
+ /** String formats OpenAI strict mode accepts; other providers accept no more. */
3
+ export const STRICT_PROVIDER_FORMATS = new Set(["date-time", "time", "date", "duration", "email", "hostname", "ipv4", "ipv6", "uuid"]);
4
+ // Where a schema holds subschemas: one, a list, or a name -> schema map.
5
+ const SCHEMA_KEYS = new Set(["items", "additionalItems", "additionalProperties", "contains", "not", "if", "then", "else", "propertyNames", "unevaluatedItems", "unevaluatedProperties"]);
6
+ const SCHEMA_LIST_KEYS = new Set(["anyOf", "oneOf", "allOf", "prefixItems"]);
7
+ const SCHEMA_MAP_KEYS = new Set(["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"]);
8
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
+ function resolvePointer(root, ref) {
10
+ let node = root;
11
+ for (const raw of ref.slice(2).split("/")) {
12
+ if (raw === "")
13
+ continue;
14
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
15
+ if (Array.isArray(node))
16
+ node = node[Number(key)];
17
+ else if (isObject(node))
18
+ node = node[key];
19
+ else
20
+ return undefined;
21
+ }
22
+ return node;
23
+ }
24
+ function uniqueSchemas(schemas) {
25
+ const seen = new Map();
26
+ for (const schema of schemas)
27
+ seen.set(JSON.stringify(schema), schema);
28
+ return [...seen.values()];
29
+ }
30
+ function rewrite(schema, root, expanding) {
31
+ if (!isObject(schema))
32
+ return schema;
33
+ const child = (value) => rewrite(value, root, expanding);
34
+ const { $ref, ...rest } = schema;
35
+ if (typeof $ref === "string" && ($ref === "#" || $ref.startsWith("#/"))) {
36
+ // A recursive shape cannot be inlined; accept any value there and let zod validate it.
37
+ const target = expanding.includes($ref) ? {} : rewrite(resolvePointer(root, $ref), root, [...expanding, $ref]);
38
+ return { ...(isObject(target) ? target : {}), ...child(rest) };
39
+ }
40
+ const out = {};
41
+ for (const [key, value] of Object.entries(schema)) {
42
+ if (key === "additionalItems" || key === "prefixItems")
43
+ continue;
44
+ if (SCHEMA_KEYS.has(key))
45
+ out[key] = Array.isArray(value) ? value.map(child) : child(value);
46
+ else if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value))
47
+ out[key] = value.map(child);
48
+ else if (SCHEMA_MAP_KEYS.has(key) && isObject(value))
49
+ out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, child(sub)]));
50
+ else
51
+ out[key] = value;
52
+ }
53
+ if (Array.isArray(schema.type) && !("anyOf" in out)) {
54
+ delete out.type;
55
+ out.anyOf = schema.type.map((type) => ({ type }));
56
+ }
57
+ if (typeof out.format === "string" && !STRICT_PROVIDER_FORMATS.has(out.format))
58
+ delete out.format;
59
+ if (isObject(out.propertyNames) && Object.keys(out.propertyNames).length === 1 && out.propertyNames.type === "string")
60
+ delete out.propertyNames;
61
+ const tuple = Array.isArray(schema.prefixItems) ? schema.prefixItems : Array.isArray(schema.items) ? schema.items : null;
62
+ if (tuple) {
63
+ const extra = Array.isArray(schema.prefixItems) ? schema.items : schema.additionalItems;
64
+ const open = extra !== undefined && extra !== false;
65
+ const members = uniqueSchemas([...tuple, ...(isObject(extra) ? [extra] : [])].map(child));
66
+ out.items = members.length === 1 ? members[0] : members.length === 0 ? {} : { anyOf: members };
67
+ out.minItems ??= tuple.length;
68
+ if (!open)
69
+ out.maxItems ??= tuple.length;
70
+ }
71
+ return out;
72
+ }
73
+ export function providerSafeSchema(schema) {
74
+ const out = rewrite(schema, schema, []);
75
+ if (isObject(out)) {
76
+ // Every local $ref is inlined, so the definition tables are dead weight.
77
+ delete out.definitions;
78
+ delete out.$defs;
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * Wrap the SDK's tools/list so every tool's inputSchema leaves the server in
84
+ * the provider-safe form. Call after the first registerTool (the SDK installs
85
+ * its tools/list handler then).
86
+ */
87
+ export function serveProviderSafeToolSchemas(server) {
88
+ const handlers = server.server._requestHandlers;
89
+ const listTools = handlers.get("tools/list");
90
+ if (!listTools)
91
+ return;
92
+ server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
93
+ const result = await listTools(request, extra);
94
+ return { ...result, tools: result.tools.map((tool) => ({ ...tool, inputSchema: providerSafeSchema(tool.inputSchema) })) };
95
+ });
96
+ }
@@ -27,6 +27,7 @@ import { registerLibraryTools } from "./tools/library-tools.js";
27
27
  import { getCachedBootDriftNotice as getCachedNotice, setCachedBootDriftNotice, } from "./boot-notice.js";
28
28
  import { appendMcpLogEvent } from "../core/mcp-log.js";
29
29
  import { prepareHostedMount } from "./hosted-mount.js";
30
+ import { serveProviderSafeToolSchemas } from "./provider-safe-schema.js";
30
31
  import { buildBootDriftNotice, fetchLatestRegistryVersion, } from "../installer/version-check.js";
31
32
  import { TOOLKIT_VERSION as version } from "../core/version.js";
32
33
  let processDiagnosticsInstalled = false;
@@ -329,6 +330,8 @@ export function createMcpServer() {
329
330
  // The playbook is also an MCP prompt so prompt-surfacing hosts get it
330
331
  // natively (same content as the summer_get_agent_playbook tool).
331
332
  registerPlaybookPrompt(server);
333
+ // Strict providers (Kimi, OpenAI strict) reject tuple and $ref schemas.
334
+ serveProviderSafeToolSchemas(server);
332
335
  return { server, getRegisteredToolCount };
333
336
  }
334
337
  export async function startMcpServer(options = {}) {
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { BOARD_ENDPOINT, boardArgsSchema, boardContent, boardQuery } from "../../core/capabilities/board.js";
3
3
  import { IMAGE_GENERATION_TIMEOUT_MS, imageGenerationArgsSchema, saveGeneratedImage } from "../../core/capabilities/image-generation.js";
4
4
  import { buildMotionRequestBody, motionErrorHint, motionGenerationArgsSchema, motionJobFailureHint, } from "../../core/capabilities/motion-generation.js";
5
+ import { MAX_CHOICES_WAIT_SECONDS, defaultCanOpenBrowser, generateSfxChoices, getChoices, getChoicesArgsSchema, openInStudioTab, sfxChoicesArgsSchema, SFX_CHOICES_ENDPOINT, } from "../../core/capabilities/sfx-choices.js";
6
+ import open from "open";
5
7
  import { getAuthToken } from "../../core/auth.js";
6
8
  import { resolveGatewayUrl } from "../../core/config.js";
7
9
  import { readJsonResponse } from "../../core/util/http.js";
@@ -10,6 +12,7 @@ import { TOOLKIT_VERSION as CLI_VERSION } from "../../core/version.js";
10
12
  const TOOL_BY_ENDPOINT = {
11
13
  "/api/mcp/generate/image": "summer_generate_image",
12
14
  "/api/mcp/generate/audio": "summer_generate_audio",
15
+ [SFX_CHOICES_ENDPOINT]: "summer_generate_sfx_choices",
13
16
  "/api/mcp/generate/3d": "summer_generate_3d",
14
17
  "/api/mcp/generate/video": "summer_generate_video",
15
18
  "/api/mcp/generate/motion": "summer_generate_motion",
@@ -356,10 +359,21 @@ Requires authentication: run 'npx -y summer-engine@latest login' first.`, {
356
359
 
357
360
  Capabilities:
358
361
  - "text_to_speech" — Convert text to spoken audio. Requires 'text'. Optional 'voiceId'.
359
- - "sound_effects" — Generate sound effects from description. Requires 'text'. Optional 'durationSeconds'.
362
+ - "sound_effects" — Generate sound effects from description. Requires 'text'. Optional 'durationSeconds', 'modelId', 'loop'.
360
363
  - "music" — Generate background music. Requires 'prompt'. Optional 'durationSeconds'.
361
364
  - "text_to_dialogue" — Multi-voice dialogue. Requires 'inputs' array of {text, voiceId} objects.
362
365
 
366
+ Sound-effect models (modelId; omit it to use the person's approved default,
367
+ else ElevenLabs):
368
+ - "eleven_text_to_sound_v2" — ElevenLabs SFX v2. Strong all-rounder, 0.5-30 s,
369
+ the only one that makes seamless loops (loop: true).
370
+ - "sonilo-sfx-1.1" — Sonilo SFX 1.1. Strong all-rounder, >= 0.5 s.
371
+ - "stable-audio-3-sfx" — Stable Audio 3 SFX. Sometimes best on fast movement
372
+ (swings, whooshes), less consistent; >= 1 s.
373
+ The best model changes a lot from one sound to the next. For the first sound
374
+ effect in a chat, offer the person three versions to pick from by ear:
375
+ summer_generate_sfx_choices.
376
+
363
377
  The 'options' object is spread into the ElevenLabs SDK call at the TOP level, so
364
378
  only keys the SDK accepts there have any effect. Voice tuning is NOT top-level and
365
379
  is NOT snake_case: pass it as options.voiceSettings with camelCase keys —
@@ -388,11 +402,16 @@ Requires authentication: run 'npx -y summer-engine@latest login' first.`, {
388
402
  modelId: z
389
403
  .string()
390
404
  .optional()
391
- .describe("ElevenLabs model ID (e.g. 'eleven_multilingual_v2')"),
405
+ .describe("Model id. text_to_speech: an ElevenLabs model (e.g. 'eleven_multilingual_v2'). " +
406
+ "sound_effects: eleven_text_to_sound_v2, sonilo-sfx-1.1 or stable-audio-3-sfx; omit to use the person's approved default"),
392
407
  durationSeconds: z
393
408
  .number()
394
409
  .optional()
395
410
  .describe("Duration in seconds for SFX or music"),
411
+ loop: z
412
+ .boolean()
413
+ .optional()
414
+ .describe("sound_effects only: ask for a seamless loop (eleven_text_to_sound_v2 only)"),
396
415
  inputs: z
397
416
  .array(z.object({ text: z.string(), voiceId: z.string() }))
398
417
  .optional()
@@ -403,7 +422,7 @@ Requires authentication: run 'npx -y summer-engine@latest login' first.`, {
403
422
  .describe("Provider params, spread at the SDK's top level. Voice tuning goes in " +
404
423
  "options.voiceSettings with camelCase keys (stability, similarityBoost, " +
405
424
  "style, speed, useSpeakerBoost); flat snake_case is silently ignored."),
406
- }, async ({ capability, text, prompt, voiceId, modelId, durationSeconds, inputs, options }) => {
425
+ }, async ({ capability, text, prompt, voiceId, modelId, durationSeconds, loop, inputs, options }) => {
407
426
  const body = { capability };
408
427
  if (text)
409
428
  body.text = text;
@@ -415,6 +434,8 @@ Requires authentication: run 'npx -y summer-engine@latest login' first.`, {
415
434
  body.modelId = modelId;
416
435
  if (durationSeconds)
417
436
  body.durationSeconds = durationSeconds;
437
+ if (loop !== undefined)
438
+ body.loop = loop;
418
439
  if (inputs)
419
440
  body.inputs = inputs;
420
441
  if (options)
@@ -426,6 +447,52 @@ Requires authentication: run 'npx -y summer-engine@latest login' first.`, {
426
447
  return successResult(result.data);
427
448
  });
428
449
  // ========================================================================
450
+ // summer_generate_sfx_choices + summer_get_choices
451
+ // Shared implementation: core/capabilities/sfx-choices.ts
452
+ // ========================================================================
453
+ const sfxChoicesDeps = {
454
+ post: (endpoint, body, timeoutMs) => mcpGenerate(endpoint, body, timeoutMs),
455
+ get: (endpoint, params, timeoutMs) => mcpGet(endpoint, params, timeoutMs),
456
+ studioOpen: (path, reviewUrl) => openInStudioTab(path, reviewUrl),
457
+ openUrl: (url) => open(url),
458
+ canOpenBrowser: () => defaultCanOpenBrowser(),
459
+ };
460
+ server.tool("summer_generate_sfx_choices", `Make one sound effect with several models (default three) and show the
461
+ versions to the person on a Summer Studio page, where they listen and approve
462
+ one or more. The approved model becomes their default for sound effects.
463
+
464
+ Use it for the first sound effect in a chat after the person said yes to
465
+ "three versions to pick from", or whenever they want to compare. The page
466
+ opens in their open Summer Studio tab when there is one (needs the store
467
+ sign-in), else in the browser; open:false only returns reviewUrl.
468
+
469
+ Then tell the person the returned "say" text and call summer_get_choices
470
+ with the choiceSetId. Do not pick for them.
471
+
472
+ Models: eleven_text_to_sound_v2 (0.5-30 s, loops), sonilo-sfx-1.1 (>= 0.5 s),
473
+ stable-audio-3-sfx (>= 1 s; give durationSeconds >= 1 when it is included).
474
+ Each version is a billed generation.
475
+
476
+ Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
477
+ Requires authentication: run 'npx -y summer-engine@latest login' first.`, sfxChoicesArgsSchema.shape, async (args) => {
478
+ const result = await generateSfxChoices(sfxChoicesArgsSchema.parse(args), sfxChoicesDeps);
479
+ return result.ok ? successResult(result) : errorResult(String(result.error), result);
480
+ });
481
+ server.tool("summer_get_choices", `Read a sound-effect choice set from summer_generate_sfx_choices: whether the
482
+ person decided ("pending" or "decided"), which versions they approved (with
483
+ audioUrl, assetId and their note) and their default sound-effect model.
484
+
485
+ waitSeconds (max ${MAX_CHOICES_WAIT_SECONDS}) waits for the decision before answering; call again
486
+ while status is "pending". Import each approved clip with
487
+ summer_import_from_url (url: audioUrl, path: res://audio/sfx/<name>.<fileExtension>)
488
+ or summer_import_asset_by_id (assetId), then wire it.
489
+
490
+ Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
491
+ Requires authentication: run 'npx -y summer-engine@latest login' first.`, getChoicesArgsSchema.shape, async (args) => {
492
+ const result = await getChoices(getChoicesArgsSchema.parse(args), sfxChoicesDeps);
493
+ return result.ok ? successResult(result) : errorResult(String(result.error), result);
494
+ });
495
+ // ========================================================================
429
496
  // summer_generate_3d
430
497
  // ========================================================================
431
498
  server.tool("summer_generate_3d", `Generate a 3D model via Summer Engine Studio.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/gemini — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "version": "3.5.1",
4
+ "version": "3.6.0",
5
5
  "description": "Agent tooling for Summer Engine: MCP bridge, context primer, and game-dev skills. Use with the npm CLI for skill files on disk.",
6
6
  "contextFileName": "GEMINI.md",
7
7
  "mcpServers": {
@@ -93,6 +93,7 @@
93
93
  "./library/skills/psx-retro-rendering/",
94
94
  "./library/skills/publish-your-game/",
95
95
  "./library/skills/realtime-wet-surfaces/",
96
+ "./library/skills/release-readiness/",
96
97
  "./library/skills/remote-deploy/",
97
98
  "./library/skills/retarget/",
98
99
  "./library/skills/running-in-the-cloud/",
@@ -17,7 +17,7 @@
17
17
 
18
18
  **Rule of thumb:** project reads/writes go through Summer; live hierarchy/inspector changes use scene tools; process-level work remains with the host.
19
19
 
20
- ## Tool surface (105 tools)
20
+ ## Tool surface (107 tools)
21
21
 
22
22
  ### Project files (4)
23
23
 
@@ -211,13 +211,15 @@ Preview. One fast, read-only call that walks every node of a 3D scene and lists
211
211
  | `summer_import_hdri` | Search Poly Haven's CC0 HDRIs (public API, no Summer login), import the `.hdr`/`.exr` into `res://sky/`, and get the exact `summer_run_script` snippet that wires it as the WorldEnvironment sky. |
212
212
  | `summer_slice_asset_sheet` | Detect and crop every distinct asset from a generated sheet image into named individual assets (works without the engine; import the results afterwards). |
213
213
 
214
- ### Asset generation (5 — metered)
214
+ ### Asset generation (7 — metered)
215
215
 
216
216
  | Tool | Use |
217
217
  |---|---|
218
218
  | `summer_generate_image` | AI image gen. |
219
219
  | `summer_generate_3d` | Image-to-3D. |
220
- | `summer_generate_audio` | SFX / music gen. |
220
+ | `summer_generate_audio` | SFX / music gen. Sound effects: `modelId` picks ElevenLabs SFX v2, Sonilo SFX 1.1 or Stable Audio 3 SFX; omitted = the person's approved default. |
221
+ | `summer_generate_sfx_choices` | One sound effect from several models, shown on a Summer Studio page where the person approves one or more. |
222
+ | `summer_get_choices` | Read which sound-effect versions the person approved (audioUrl, assetId, note). |
221
223
  | `summer_generate_video` | Video gen. |
222
224
  | `summer_generate_motion` | Generate/apply 3D skeletal motion from a rigged asset. |
223
225
 
@@ -1,6 +1,6 @@
1
1
  id: reference/mcp-tools-reference
2
2
  kind: reference
3
- version: 1.4.0
3
+ version: 1.4.1
4
4
  summary: Canonical reference for which Summer MCP tool to call — skills should reference tool names exactly as written here.
5
5
  use_when:
6
6
  - choosing between Summer MCP tools and host tools for an operation
@@ -1,6 +1,6 @@
1
1
  # Summer skills
2
2
 
3
- > 122 skills, grouped by their primary domain. **Generated** by `npm run generate:registry` from each skill's `resource.yaml`; `--check` fails when this file and the library disagree. Do not edit by hand.
3
+ > 123 skills, grouped by their primary domain. **Generated** by `npm run generate:registry` from each skill's `resource.yaml`; `--check` fails when this file and the library disagree. Do not edit by hand.
4
4
 
5
5
  The folder is flat on purpose (`docs/design/DECISIONS.md`, D3): a skill usually belongs to several domains, so categories live in each skill's `facets.domains` and this index is rendered from them. The first domain listed is the primary one. `summer skills list --by-domain` prints the same grouping; `summer skills info <slug>` shows one skill.
6
6
 
@@ -18,7 +18,7 @@ Status: **stable** unless marked; *preview* = not yet exercised in-engine by the
18
18
  - [character-controller](#character-controller) (2)
19
19
  - [creator](#creator) (4)
20
20
  - [debug](#debug) (1)
21
- - [deployment](#deployment) (2)
21
+ - [deployment](#deployment) (3)
22
22
  - [editor](#editor) (1)
23
23
  - [gameplay](#gameplay) (6)
24
24
  - [level-design](#level-design) (2)
@@ -117,7 +117,7 @@ Status: **stable** unless marked; *preview* = not yet exercised in-engine by the
117
117
  | [ambient-bed](./ambient-bed/SKILL.md) | Generate a long looping location ambience — forest, dungeon, city, spaceship, cave — a looping AudioStreamPlayer on the Ambient bus with a seamless loop. | assets |
118
118
  | [audio-direction](./audio-direction/SKILL.md) ★ | Define the game's sonic identity — music style, instruments, SFX vocabulary, dynamic music plan — output as an audio bible at .summer/audio-bible.md. | game-design |
119
119
  | [music-track](./music-track/SKILL.md) ★ | Generate a looped or cinematic music track — loops authored at >=30s with a marked loop point, cinematic tracks at >=60s linear. | assets |
120
- | [sound-effect](./sound-effect/SKILL.md) ★ | Generate short SFX one-shots — footsteps, weapon swings, UI clicks, hit impacts — wired as AudioStreamPlayer/2D/3D that auto-frees on finished. | assets |
120
+ | [sound-effect](./sound-effect/SKILL.md) ★ | Make SFX one-shots and timed sequences (swings, hits, whooshes, reloads, UI) - offer three model versions to pick from, then wire them as AudioStreamPlayers. | assets |
121
121
  | [voice-line](./voice-line/SKILL.md) ★ | Generate TTS voice lines — NPC barks, narrator, dialogue — with voice-id sourcing, a character-to-voice decision tree, and multi-line dialogue support. | assets |
122
122
 
123
123
  ## character-controller
@@ -147,6 +147,7 @@ Status: **stable** unless marked; *preview* = not yet exercised in-engine by the
147
147
  | skill | when to use | also |
148
148
  |---|---|---|
149
149
  | [export-and-ship](./export-and-ship/SKILL.md) ★ | Assess and prepare a game export: inventory installed templates and targets, validate release assets and config, produce supported builds after approval. | project |
150
+ | [*release-readiness*](./release-readiness/SKILL.md) (preview) | Before releasing a Summer game version, prove players can play it: server scene, online join, progress, Game ID, released Summer, packed files, app version. | multiplayer, playtest |
150
151
  | [remote-deploy](./remote-deploy/SKILL.md) ★ | Run or test the game on a real device — phone, tablet, another computer — via the Remote Deploy button, runnable export presets, and on-device debugging. | project |
151
152
 
152
153
  ## editor
@@ -103,13 +103,15 @@ If the user wants a busier bed (market, party), still prevent foreground events:
103
103
  ```
104
104
  summer_generate_audio(
105
105
  capability: "sound_effects",
106
+ modelId: "eleven_text_to_sound_v2",
107
+ loop: true,
106
108
  text: "forest at dawn, gentle wind through leaves, distant bird calls every 6 seconds, soft creaking branches, no music, no human sound, 22s seamless loop",
107
109
  durationSeconds: 22
108
110
  )
109
111
  // Then: summer_import_from_url(url: "<fileUrl>", path: "res://audio/ambient/forest_dawn.wav")
110
112
  ```
111
113
 
112
- `sound_effects` reads from **`text`**, not `prompt` — passing `prompt` is a 400 `text_required`. (`prompt` is the `music` capability's field.) The clip comes back as WAV, so a `.wav` target path is correct.
114
+ Name `modelId: "eleven_text_to_sound_v2"` with `loop: true`: it is the only sound-effect model that makes seamless loops, and without a `modelId` the person's default sound-effect model runs, which may not loop. `sound_effects` reads from **`text`**, not `prompt` — passing `prompt` is a 400 `text_required`. (`prompt` is the `music` capability's field.) The clip comes back as WAV, so a `.wav` target path is correct.
113
115
 
114
116
  22s is the SFX ceiling. Always generate at the ceiling for ambient — longer cycle means less recognized repetition.
115
117
 
@@ -1,6 +1,6 @@
1
1
  id: skill/ambient-bed
2
2
  kind: skill
3
- version: 1.0.2
3
+ version: 1.0.3
4
4
  summary: Generate a long looping location ambience — forest, dungeon, city, spaceship, cave — a looping AudioStreamPlayer on the Ambient bus with a seamless loop.
5
5
  use_when:
6
6
  - generating a long looping location ambience — forest at dawn, dungeon air, city street, spaceship hum, cave drip — non-melodic and low-energy
@@ -203,15 +203,15 @@ Ask:
203
203
 
204
204
  ### 10. Optional — generate first SFX seeds
205
205
 
206
- If the user wants to start generating, propose 3-5 SFX seeds aligned with the vocabulary. These are starting points, not finals.
206
+ If the user wants to start generating, propose 3-5 SFX seeds aligned with the vocabulary. These are starting points, not finals. Generate them with the `sound-effect` skill: the first sound effect in a chat starts with its question (three versions from three models, picked by ear in Summer Studio), and its prompt shape is the one that tested well.
207
207
 
208
208
  ```
209
- summer_generate_audio(capability="sound_effects", text="soft woody UI click, 80ms, no reverb, neutral pitch", durationSeconds=0.5)
210
- summer_generate_audio(capability="sound_effects", text="warm major-third coin pickup, 300ms, light reverb", durationSeconds=0.5)
211
- summer_generate_audio(capability="sound_effects", text="punchy low-mid player damage hit, slight detune, 200ms, no tail", durationSeconds=0.5)
209
+ summer_generate_audio(capability="sound_effects", text="Foley one-shot: a fingertip taps a small wooden button, soft woody click, no tail, close-mic, dry, no reverb, no music, no voices.", durationSeconds=0.5)
210
+ summer_generate_audio(capability="sound_effects", text="Foley one-shot: a small gold coin drops onto a wooden table, bright metallic ting with a short ring, close-mic, dry, no music, no voices.", durationSeconds=0.5)
211
+ summer_generate_audio(capability="sound_effects", text="Foley one-shot: a fist hits a padded leather vest, low-mid thump, no tail, close-mic, dry, no reverb, no music, no voices.", durationSeconds=0.5)
212
212
  ```
213
213
 
214
- `capability` is required; `sound_effects` reads from `text` (not `prompt`), and the length argument is `durationSeconds` (not `duration`) with a floor of 0.5 s. Ask for the character you want inside the text and let the player only use the front of the clip — you cannot request 80 ms.
214
+ `capability` is required; `sound_effects` reads from `text` (not `prompt`), and the length argument is `durationSeconds` (not `duration`) with a floor of 0.5 s (1 s for Stable Audio 3 SFX). Leave out `modelId` so the person's approved default model runs. Ask for the character you want inside the text and let the player only use the front of the clip — you cannot request 80 ms.
215
215
 
216
216
  Ask first:
217
217
 
@@ -1,6 +1,6 @@
1
1
  id: skill/audio-direction
2
2
  kind: skill
3
- version: 1.0.6
3
+ version: 1.0.7
4
4
  summary: Define the game's sonic identity — music style, instruments, SFX vocabulary, dynamic music plan — output as an audio bible at .summer/audio-bible.md.
5
5
  use_when:
6
6
  - defining the sonic identity — music style, instruments, SFX vocabulary, dynamic music plan
@@ -298,6 +298,10 @@ Per platform, point the user at the correct upload path:
298
298
  - **summer.games:** `summer_export_game` writes the summer.games `.zip`; `summer_publish_build` uploads it to the game's store listing after the user confirms the exact game, file and version (needs `summer login --store`). If it answers `store_auth_refused`, the user uploads the `.zip` in Studio instead.
299
299
  - Per platform, the store takes: iPhone, Android, macOS and Windows in the Summer apps = one summer.games bundle (`summer_export_game` `targets`, format `bundle`; no template, no signing; a game without a server targets `ios`/`android` only). Web = an HTML5 `.zip` on Summer's WebGPU Forward+ template (`targets:["web"]`; the project must render with Forward+ or Mobile). A macOS or Windows download for the store page = `targets:["macos"]` or `["windows"]` with format `download`. Download exports need the template: `summer_export_templates` `install` fetches it from Summer's CDN.
300
300
  - Hosted games: if `summer_export_game` fails with "… is in the source graph's authority_engine domain, which the client pack leaves out", an authority root in `source-domains.json` holds a file the client loads (often the composition `.tres`). Narrow that root to the server-only files, or move the shared file under a `shared` path, then export again. Every exporter message is in the `multiplayer-publish` skill's export-errors.md; the folder rules are in `multiplayer-project`.
301
+ - Every summer.games bundle: export with the released Summer editor from the download page, never a development build, and export again after updating Summer. Otherwise its scripts can fail to compile on players' devices and the game opens to an empty window, though every upload check passes. `release-readiness` check 7 proves it.
302
+ - Every file the game reads by path at run time (`FileAccess`, `DirAccess`, `load_from_file`) that Summer doesn't import, such as a `.bin` or `.json`, must match the **summer.games** preset's own `include_filter` (Resources > Filters to export non-resource files/folders); a filter on another preset doesn't carry over. A left-out file is no export error, it is just missing on players' devices. Never use `SystemFont` for phone text: on iPhone it draws every glyph as a box. `release-readiness` check 8 lists the pack.
303
+ - iPhone and Android bundles: the Summer Games apps start only exports on their Summer version line (same MAJOR.MINOR, patch no newer). Export with the current Summer release and export again after every Summer minor update, or players see "The game could not start" on phones while desktop works. `release-readiness` check 9.
304
+ - Hosted games, before the upload: a clean export does not prove players can join. Run `release-readiness` on the `.zip`: the bundle's `server.pck`, started with no scene, must load the authority scene (the server preset needs the `summer_authority` custom feature), and `Summer.initialize` must pass no hard-coded Game ID. Otherwise every online join fails with `world_start_timeout`.
301
305
  - Store page art: key art, covers and icons carry no text, logos or UI (the store page shows the title). Screenshots are real gameplay and may show the game's own HUD; add no captions, logos or overlays. Capture them with `summer_capture_gameplay` (offscreen, no editor needed; `resolution` `1920x1080`, or `1080x1920` for portrait; get past a title menu with the game's own flags in `args`, e.g. `["--autostart"]`, or `steps` such as `[{"press":"Play"},{"wait":3000},{"shot":true}]`), or `summer_screenshot` target `game` while the editor runs the game.
302
306
  - **Steam:** `steamcmd` + Steam Pipe (`builder_app_xxx.vdf`). Set up depots first in Steamworks.
303
307
  - **itch.io:** `butler push <path> <user>/<game>:<channel>` (e.g. `butler push build/win <user>/mygame:windows`).
@@ -47,6 +47,7 @@ These rules from the playbooks apply to every genre:
47
47
  - **2D scales to the viewport.** Set `display/window/stretch/mode = canvas_items` and `stretch/aspect = expand`; UI uses Control anchors; code-positioned nodes compute from `get_viewport_rect().size`. Never hardcode 1920x1080.
48
48
  - **Playable slice, not tech demo.** Name the core loop in one sentence; every input gets feedback (tween, sound, particle, flash, shake); progression is visible; a restart exists. The bar is "feels good for 30 seconds".
49
49
  - **Fast first draft when someone is waiting.** The first playable version uses shapes, the palette and any template art, plus at most one or two generated assets the person specifically asked for; generation takes minutes, so it never blocks the first play. Say plainly that it is a first draft to get a feel for the game, that the art is not final, and ask if this is the direction they want. Generate the rest of the art in later passes.
50
+ - **A way back to the title.** From inside play, a pause menu or a **Leave** button returns to the title screen without quitting the app.
50
51
  - **Text the default font can show.** No emoji or symbols (▲ ◀ ★ ♥ ✓) in UI text: they render as boxes with hex codes. Use words or icons; see `ui-basics`.
51
52
 
52
53
  ## The Pipeline
@@ -95,6 +96,12 @@ This phase doesn't have a dedicated skill yet — do it inline. Read `.summer/Ga
95
96
  2. **Core mechanics** — ranked, max 3 (the brief enforces this; respect it)
96
97
  3. **Cut list** — what's explicitly NOT in scope
97
98
 
99
+ If the game has online play, the plan also fixes three product defaults; each one was missed by a real release:
100
+
101
+ - **Online play is reachable by default.** A new player gets to it from the title without changing a setting. Hiding it behind a setting or a grown-ups gate is the owner's explicit decision, recorded in the plan, and it means someone joins online before every release.
102
+ - **Progress is saved by the server.** Online progress goes through authority player data (`summer-player-data`), not only the player's local files.
103
+ - **A way back to the title** from inside an online and an offline session.
104
+
98
105
  Save the plan to `.summer/build-plan.md`. Format:
99
106
 
100
107
  ```markdown
@@ -236,8 +243,10 @@ Only if `summer_get_diagnostics` flags rendering or physics hotspots. Don't opti
236
243
  1. `play` from main scene.
237
244
  2. Walk every mechanic from the plan.
238
245
  3. Hit the win condition (or lose condition).
239
- 4. `summer_get_diagnostics` clean.
240
- 5. `debug` until clean if not.
246
+ 4. Return to the title from inside play, then start again.
247
+ 5. Online games: run Local Play (`multiplayer-testing`) and reach online play from the title as a new player, with default settings.
248
+ 6. `summer_get_diagnostics` clean.
249
+ 7. `debug` until clean if not.
241
250
 
242
251
  **Don't skip this even if every mechanic worked individually.** Integration bugs hide between mechanics.
243
252
 
@@ -249,6 +258,14 @@ Skill: export-and-ship
249
258
 
250
259
  Pre-flight checklist (icon, store banners, build config) before producing release builds for the targets in the brief.
251
260
 
261
+ Before any version reaches players, run:
262
+
263
+ ```
264
+ Skill: release-readiness
265
+ ```
266
+
267
+ It checks the file you upload, not the editor: a hosted game's `server.pck` starts its authority scene, `Summer.initialize` passes no hard-coded Game ID, online play is reachable, progress is saved by the server, and players can get back to the title. Then, after publishing, the user joins online with the released version. Report each check as pass, fail, not applicable or not run. Hosted games publish through `multiplayer-publish`; store pages through `publish-your-game`.
268
+
252
269
  If the brief said "jam" or "prototype", skip this phase. If the brief said "game I want to release", run it.
253
270
 
254
271
  ## Anti-Patterns
@@ -260,6 +277,8 @@ If the brief said "jam" or "prototype", skip this phase. If the brief said "game
260
277
  | Stack art direction on top of unfinished mechanics | Art changes are reversible only if the underlying scene structure is stable. Get mechanics done first. |
261
278
  | Treat "polish" as required | If the brief says "jam in 2 days", polish is a cut item. Respect the brief. |
262
279
  | Run all phases without confirming | The user owns scope. Stop at every checkpoint. |
280
+ | Ship online play behind a setting nobody changes | Hosted join is never exercised, so it breaks without anyone noticing. Reachable by default, or the owner joins online before every release. |
281
+ | Hard-code a Game ID in `Summer.initialize` | The summer.games bundle has no `gameId`; a different ID fails on the hosted server and every online join times out. Pass no ID. |
263
282
  | Build a fully featured menu before there's a playable mechanic | Menu work is endless polish; player-loop work is the actual game. Mechanics first. |
264
283
  | Fall back to "let me try a few things" mid-pipeline | If you don't know what to do, name the phase and ask the user, don't improvise around the structure. |
265
284
 
@@ -21,6 +21,7 @@ related:
21
21
  skills:
22
22
  - skill/audio-direction
23
23
  - skill/export-and-ship
24
+ - skill/release-readiness
24
25
  - skill/design-mechanic
25
26
  - skill/tune-performance
26
27
  - skill/art-direction
@@ -84,7 +84,7 @@ the sender's verified Session. Events are fire-and-forget.
84
84
 
85
85
  | Word | Meaning |
86
86
  |---|---|
87
- | Game | Your game on Summer; its id is `gameId` in `summer.build.json` and the argument to `Summer.initialize` |
87
+ | Game | Your game on Summer; its id is `gameId` in `summer.build.json`. `Summer.initialize()` takes no ID: Summer knows which game is running |
88
88
  | World | One running match or shared place, simulated by one authority |
89
89
  | WorldDefinition | `world.json`: which scenes run the client and the authority, lifetime, persistence |
90
90
  | Queue | How players find a World; declared in `summer.build.json` |
@@ -109,6 +109,8 @@ Each skill continues the same small game, so their code fits together:
109
109
  what a passing run must show.
110
110
  5. **`multiplayer-publish`**: export the summer.games bundle and upload it as
111
111
  a Build.
112
+ 6. **`release-readiness`**: before players get a version, prove the server
113
+ starts its authority scene and a real online join works.
112
114
 
113
115
  ## Add Summer services
114
116
 
@@ -25,6 +25,7 @@ related:
25
25
  - skill/multiplayer-state
26
26
  - skill/multiplayer-testing
27
27
  - skill/multiplayer-publish
28
+ - skill/release-readiness
28
29
  - skill/summer-matchmaking
29
30
  - skill/summer-player-data
30
31
  source: official
@@ -176,7 +176,7 @@ func _ready() -> void:
176
176
  spawner.entity_spawned.connect(_on_player_spawned)
177
177
  spawner.command_received.connect(_on_command)
178
178
  world.session_left.connect(_on_session_left)
179
- var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
179
+ var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
180
180
  if not initialized.ok:
181
181
  push_error(initialized.message)
182
182
  return
@@ -26,7 +26,7 @@ func _ready() -> void:
26
26
  spawner.entity_spawned.connect(_on_player_spawned)
27
27
  spawner.command_received.connect(_on_command)
28
28
  world.session_left.connect(_on_session_left)
29
- var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
29
+ var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
30
30
  if not initialized.ok:
31
31
  push_error(initialized.message)
32
32
  return
@@ -141,7 +141,7 @@ func _ready() -> void:
141
141
  # Connect before joining, or the first values arrive unseen.
142
142
  spawner.state_group_created.connect(_on_group_created)
143
143
  spawner.entity_spawned.connect(_on_entity_spawned)
144
- var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
144
+ var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
145
145
  if not initialized.ok:
146
146
  push_error(initialized.message)
147
147
  return
@@ -440,7 +440,6 @@ occurrence_streams = Array[SummerNetworkOccurrenceStream]([SubResource("Commands
440
440
  extends RefCounted
441
441
  ## Shared by the client and the authority: ids, streams and shared rules.
442
442
 
443
- const GAME_ID := "game_courtyard"
444
443
  const QUEUE := &"courtyard"
445
444
  const MOTION := &"courtyard.motion"
446
445
  const CMD := &"courtyard.cmd"
@@ -74,7 +74,14 @@ textures/vram_compression/import_etc2_astc=true
74
74
  ```
75
75
 
76
76
  The `.summer_client` and `.summer_authority` overrides tell each exported pack
77
- which scene to start. Leave the `[summer] version` line the editor writes.
77
+ which scene to start. Summer starts the hosted server with no scene
78
+ argument, so `run/main_scene.summer_authority` is the only thing that makes
79
+ `server.pck` start the authority. It applies only when the server export
80
+ preset carries the custom feature `summer_authority` (`multiplayer-publish`).
81
+ Without it the server starts the client scene, never reports ready, and every
82
+ online join fails with `world_start_timeout`. Local Play names the scene
83
+ itself, so it works either way. Leave the `[summer] version` line the editor
84
+ writes.
78
85
 
79
86
  ## 4. The declarations
80
87
 
@@ -116,6 +123,9 @@ starts. Summer rejects unknown fields, so copy the shape exactly.
116
123
  }
117
124
  ```
118
125
 
126
+ - `gameId` is the game's id in Summer Studio, for your publishing tools. The
127
+ summer.games export leaves it out of the bundle, so game code never reads
128
+ it: both sides call `Summer.initialize()` with no ID (sections 7 and 8).
119
129
  - `queues[].minPlayers` / `maxPlayers` bound a match. `worldDefinition` names
120
130
  the `definition_id` in `world.json`.
121
131
  - `server.exportPreset` names the server export preset (`multiplayer-publish`).
@@ -250,7 +260,6 @@ on both sides:
250
260
  extends RefCounted
251
261
  ## Shared by the client and the authority: ids, streams and shared rules.
252
262
 
253
- const GAME_ID := "game_courtyard"
254
263
  const QUEUE := &"courtyard"
255
264
  const CMD := &"courtyard.cmd"
256
265
  const MAX_PLAYERS := 8
@@ -321,7 +330,7 @@ func _ready() -> void:
321
330
  spawner.entity_spawned.connect(_on_player_spawned)
322
331
  spawner.command_received.connect(_on_command)
323
332
  world.session_left.connect(_on_session_left)
324
- var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
333
+ var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
325
334
  if not initialized.ok:
326
335
  push_error(initialized.message)
327
336
  return
@@ -379,6 +388,11 @@ func _on_session_left(session: SummerSession, reason: String) -> void:
379
388
 
380
389
  `authority/main.tscn` is a plain `Node` with that script ([files.md](files.md)).
381
390
 
391
+ - `Summer.initialize()` takes no Game ID. Summer takes the game's identity
392
+ from where it runs: Local Play, the hosted server or the Summer Games app.
393
+ Never pass a literal, a placeholder or a fallback ID: any ID that differs
394
+ from the hosted one fails with `game_identity_mismatch`, and the authority
395
+ stops before it reports ready.
382
396
  - **Never** read who sent a Command from its payload: `request.get_session()`
383
397
  is verified. Don't name your own classes `SummerSession`, `SummerWorld` or
384
398
  other engine class names.
@@ -426,7 +440,7 @@ func _ready() -> void:
426
440
  spawner = Game.build_network(self, PlayerArchetype)
427
441
  # Connect signals like state_group_created here, before joining, or values
428
442
  # that arrive during the join go unseen (multiplayer-movement adds them).
429
- var initialized: SummerResult = await Summer.initialize(Game.GAME_ID).get_result_or_completed_signal()
443
+ var initialized: SummerResult = await Summer.initialize().get_result_or_completed_signal()
430
444
  if not initialized.ok:
431
445
  push_error(initialized.message)
432
446
  return
@@ -510,6 +524,8 @@ warning yourself.
510
524
  | `@rpc`, `MultiplayerSynchronizer`, `ENetMultiplayerPeer` | The Spawner, State groups and Commands | Unverified peers, no late join, no Summer hosting |
511
525
  | `if multiplayer.is_server()` in one shared scene | Separate `client/` and `authority/` scenes | The engine starts the right scene on each side |
512
526
  | Load an `authority/` file from client or shared code | Move what both sides need into `network/` | The export leaves `authority/` out of the players' pack |
527
+ | `Summer.initialize("game_…")`, or a fallback ID when `gameId` is missing | `Summer.initialize()` | The bundle has no `gameId`; a different ID fails hosted with `game_identity_mismatch` |
528
+ | Set `run/main_scene.summer_authority` and stop there | Also give the server preset the `summer_authority` feature | Without the feature the hosted server starts the client scene |
513
529
  | Build the composition differently on each side | One `network/composition.tres`, loaded by both | Different compositions refuse each other |
514
530
  | Send a Command right after `join` succeeds | Await `spawner.network_ready` first | The authority does not know the client's network until then |
515
531
  | Free a player's things on `session_disconnected` | Free them on `session_left` | A disconnected player keeps their seat and may reconnect |