summer-engine 1.3.2 → 1.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 (32) hide show
  1. package/.claude-plugin/plugin.json +59 -0
  2. package/.codex-plugin/plugin.json +38 -0
  3. package/.cursor-plugin/plugin.json +42 -0
  4. package/.mcp.json +8 -0
  5. package/.opencode/plugins/summer.js +49 -0
  6. package/README.md +13 -0
  7. package/dist/lib/skills-registry.d.ts +96 -0
  8. package/dist/lib/skills-registry.js +205 -0
  9. package/gemini-extension.json +21 -0
  10. package/hooks/hooks-cursor.json +21 -0
  11. package/hooks/hooks.json +31 -0
  12. package/hooks/pre-commit-doctor.sh +123 -0
  13. package/hooks/run-hook.cmd +23 -0
  14. package/hooks/session-start.sh +120 -0
  15. package/marketplace.json +24 -0
  16. package/package.json +9 -1
  17. package/skills/_tests/specs/design-npc.md +43 -12
  18. package/skills/_tests/specs/fps-controller.md +33 -20
  19. package/skills/_tests/specs/host-authoritative-state.md +104 -0
  20. package/skills/_tests/specs/peer-to-peer-multiplayer.md +99 -0
  21. package/skills/_tests/specs/vfx.md +56 -38
  22. package/skills/ai-and-npcs/design-npc/SKILL.md +264 -122
  23. package/skills/asset-pipeline/asset-strategy/SKILL.md +8 -0
  24. package/skills/catalog.yaml +84 -0
  25. package/skills/character-controllers/fps-controller/SKILL.md +277 -69
  26. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +332 -0
  27. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +378 -0
  28. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +20 -0
  29. package/skills/scene-and-project/make-game/SKILL.md +10 -0
  30. package/skills/scene-and-project/scene-composition/SKILL.md +8 -0
  31. package/skills/ui-and-ux/ui-basics/SKILL.md +28 -0
  32. package/skills/visual-effects/vfx/SKILL.md +233 -134
@@ -0,0 +1,378 @@
1
+ ---
2
+ name: peer-to-peer-multiplayer
3
+ description: Use when starting a multiplayer Godot game from scratch — peer-to-peer with host authority. Build the network architecture top-down so authoritative state, routing rules, and real-time rendering aren't bolted on later. Use BEFORE writing game logic, not after. Trigger on "multiplayer", "peer-to-peer", "p2p", "co-op", "host", "multiplayer architecture", "add multiplayer".
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: multiplayer-and-networking
7
+ user-invocable: true
8
+ allowed-tools: Read Grep Glob summer_get_scene_tree summer_inspect_node summer_create_scene summer_add_node summer_set_prop summer_connect_signal summer_save_scene summer_get_script_errors summer_project_setting
9
+ paths: ["**/*.gd", "**/*.tscn", "**/project.godot"]
10
+ ---
11
+
12
+ # /peer-to-peer-multiplayer — Build a P2P Game From the Ground Up
13
+
14
+ ## Overview
15
+
16
+ **Multiplayer is architecture, not a feature.** Bolting it onto a single-player game is 5× harder than starting multiplayer-first. This skill walks the four layers in order. Skip any of them and you'll spend the rest of development paying for it in cheat-vulnerable state, desync bugs, and "why is the other player teleporting" reports.
17
+
18
+ The four layers, top-down:
19
+
20
+ 1. **NetworkManager** — the autoload that owns peer IDs, connection lifecycle, message routing.
21
+ 2. **Authoritative state** — the decision matrix for *which fields the host owns vs. which the client renders*.
22
+ 3. **Three routing patterns** — host→client, client→host, client→client. Each has different rules.
23
+ 4. **Real-time rendering** — prediction + interpolation so the local player feels responsive while remote state catches up.
24
+
25
+ Build them in that order. Don't write a single piece of game logic until layers 1 + 2 exist.
26
+
27
+ ## Step 0 — Should it be P2P at all?
28
+
29
+ Ask the user before anything else:
30
+
31
+ > Are players connecting through Steam friends / Epic friends / a private invite link, or do they need matchmaking at scale (1000s of concurrent matches)?
32
+
33
+ | Answer | Architecture |
34
+ |---|---|
35
+ | Friend-invite / small lobby (≤8 players) | **P2P with host authority.** This skill. |
36
+ | Matchmaking + anti-cheat + scale | **Dedicated client-server.** Different skill (`/summer:client-server-multiplayer`). |
37
+ | MMO / persistent world | **Custom server.** Out of scope for this skill. |
38
+
39
+ If P2P is right, continue. Otherwise, stop and route to the right skill.
40
+
41
+ ## Layer 1 — NetworkManager
42
+
43
+ A single autoload that owns the network. Nothing else in the game touches `MultiplayerAPI` directly.
44
+
45
+ May I create `autoloads/network_manager.gd` and register it as a Godot autoload?
46
+
47
+ ```gdscript
48
+ class_name NetworkManager
49
+ extends Node
50
+
51
+ # ── Signals (all game systems listen here, not to MultiplayerAPI directly) ──
52
+ signal connection_succeeded
53
+ signal connection_failed(reason: String)
54
+ signal peer_joined(peer_id: int)
55
+ signal peer_left(peer_id: int)
56
+ signal session_ended
57
+
58
+ const DEFAULT_PORT := 7777
59
+ const MAX_PEERS := 8
60
+
61
+ var is_host: bool = false
62
+ var local_peer_id: int = 0
63
+ var peers: Dictionary = {} # peer_id -> { name, ready, ... }
64
+
65
+ func host(port: int = DEFAULT_PORT) -> Error:
66
+ var peer := ENetMultiplayerPeer.new()
67
+ var err := peer.create_server(port, MAX_PEERS)
68
+ if err != OK:
69
+ connection_failed.emit("create_server failed: %s" % error_string(err))
70
+ return err
71
+ multiplayer.multiplayer_peer = peer
72
+ is_host = true
73
+ local_peer_id = 1
74
+ _wire_signals()
75
+ connection_succeeded.emit()
76
+ return OK
77
+
78
+ func join(address: String, port: int = DEFAULT_PORT) -> Error:
79
+ var peer := ENetMultiplayerPeer.new()
80
+ var err := peer.create_client(address, port)
81
+ if err != OK:
82
+ connection_failed.emit("create_client failed: %s" % error_string(err))
83
+ return err
84
+ multiplayer.multiplayer_peer = peer
85
+ is_host = false
86
+ _wire_signals()
87
+ return OK
88
+
89
+ func leave() -> void:
90
+ if multiplayer.multiplayer_peer:
91
+ multiplayer.multiplayer_peer.close()
92
+ multiplayer.multiplayer_peer = null
93
+ is_host = false
94
+ local_peer_id = 0
95
+ peers.clear()
96
+ session_ended.emit()
97
+
98
+ func _wire_signals() -> void:
99
+ multiplayer.peer_connected.connect(_on_peer_connected)
100
+ multiplayer.peer_disconnected.connect(_on_peer_disconnected)
101
+ multiplayer.connected_to_server.connect(_on_connected_to_server)
102
+ multiplayer.connection_failed.connect(_on_connection_failed)
103
+ multiplayer.server_disconnected.connect(_on_server_disconnected)
104
+
105
+ func _on_peer_connected(id: int) -> void:
106
+ peers[id] = { "name": "Player %d" % id, "ready": false }
107
+ peer_joined.emit(id)
108
+
109
+ func _on_peer_disconnected(id: int) -> void:
110
+ peers.erase(id)
111
+ peer_left.emit(id)
112
+
113
+ func _on_connected_to_server() -> void:
114
+ local_peer_id = multiplayer.get_unique_id()
115
+ connection_succeeded.emit()
116
+
117
+ func _on_connection_failed() -> void:
118
+ connection_failed.emit("connection refused or timed out")
119
+
120
+ func _on_server_disconnected() -> void:
121
+ leave()
122
+ ```
123
+
124
+ Register as autoload: `summer_project_setting(key="autoload/NetworkManager", value="*res://autoloads/network_manager.gd")`. The leading `*` makes it a singleton.
125
+
126
+ **Why this shape:** every system in the game listens to `NetworkManager.peer_joined` / `peer_left`, never to `multiplayer.peer_connected` directly. When you swap transports (ENet → WebRTC → Steam P2P) later, you change one file.
127
+
128
+ ## Layer 2 — Authoritative State
129
+
130
+ For every piece of state in your game, answer one question: **if a malicious client lies about this, does it break the game?**
131
+
132
+ - **Yes** → host owns it. Clients send intent; host decides. Replicated to clients read-only.
133
+ - **No** (cosmetic / pure local feel) → client owns it. Replicated for visual sync only.
134
+
135
+ The canonical decision matrix:
136
+
137
+ | State | Owner | Reasoning |
138
+ |---|---|---|
139
+ | health, mana, stamina | **host** | damage logic must not be cheatable |
140
+ | score, kills, objectives | **host** | competitive integrity |
141
+ | inventory contents | **host** | items would dupe |
142
+ | ability cooldowns | **host** | spam-cast attack vector |
143
+ | current weapon equipped | **host** | tied to inventory |
144
+ | position (player) | **client-predicted, host-reconciled** | input latency feels awful otherwise |
145
+ | velocity | **client-predicted, host-reconciled** | same |
146
+ | look direction (camera yaw/pitch) | **client** | cosmetic, host doesn't need it |
147
+ | animation state | **client** | visual; can lead the truth |
148
+ | footstep sounds | **client** | host doesn't care |
149
+ | muzzle flash, particles | **client** | host doesn't care |
150
+ | chat messages | **host-relayed** | for moderation hooks |
151
+
152
+ Build a State Layer that codifies this. May I create `autoloads/game_state.gd`?
153
+
154
+ ```gdscript
155
+ class_name GameState
156
+ extends Node
157
+
158
+ # Host-authoritative dictionaries. Keyed by peer_id where applicable.
159
+ var health: Dictionary = {} # peer_id -> int
160
+ var score: Dictionary = {} # peer_id -> int
161
+ var inventory: Dictionary = {} # peer_id -> Array[ItemData]
162
+
163
+ # Local-only mirrors (read for rendering; never written by clients).
164
+ var local_health_view: int = 100
165
+
166
+ func _ready() -> void:
167
+ NetworkManager.peer_joined.connect(_on_peer_joined)
168
+ NetworkManager.peer_left.connect(_on_peer_left)
169
+
170
+ # ── Host-only mutators (called via RPC from client requests) ──
171
+ func _host_apply_damage(target: int, amount: int) -> void:
172
+ if not NetworkManager.is_host: return
173
+ var new_hp = max(0, health.get(target, 100) - amount)
174
+ health[target] = new_hp
175
+ _broadcast_health(target, new_hp)
176
+ if new_hp == 0:
177
+ _broadcast_death(target)
178
+
179
+ @rpc("authority", "call_remote", "reliable")
180
+ func _broadcast_health(peer: int, hp: int) -> void:
181
+ if peer == NetworkManager.local_peer_id:
182
+ local_health_view = hp
183
+
184
+ @rpc("authority", "call_remote", "reliable")
185
+ func _broadcast_death(peer: int) -> void:
186
+ pass # play death anim, etc.
187
+
188
+ # ── Client-side request (intent only — host decides) ──
189
+ func client_request_damage(target: int, amount: int) -> void:
190
+ rpc_id(1, "_host_handle_damage_request", target, amount)
191
+
192
+ @rpc("any_peer", "call_remote", "reliable")
193
+ func _host_handle_damage_request(target: int, amount: int) -> void:
194
+ if not NetworkManager.is_host: return
195
+ var sender := multiplayer.get_remote_sender_id()
196
+ # validate: did sender's hitbox actually overlap target this frame?
197
+ # if validation passes, apply. otherwise, drop silently.
198
+ if _validate_hit(sender, target):
199
+ _host_apply_damage(target, amount)
200
+
201
+ func _validate_hit(_attacker: int, _target: int) -> bool:
202
+ return true # implement: distance check, line-of-sight, cooldown, etc.
203
+
204
+ func _on_peer_joined(id: int) -> void:
205
+ if NetworkManager.is_host:
206
+ health[id] = 100
207
+ score[id] = 0
208
+ inventory[id] = []
209
+ _broadcast_health(id, 100)
210
+
211
+ func _on_peer_left(id: int) -> void:
212
+ if NetworkManager.is_host:
213
+ health.erase(id)
214
+ score.erase(id)
215
+ inventory.erase(id)
216
+ ```
217
+
218
+ **Key invariants enforced by this shape:**
219
+ - Every host-authoritative field has a single mutator function on the host.
220
+ - Clients call `client_request_*` to request a change; the host validates and broadcasts.
221
+ - Clients never directly modify `health` / `score` / `inventory` even on their own peer.
222
+ - `@rpc("authority", ...)` ensures only the host can call broadcast functions.
223
+ - `@rpc("any_peer", ...)` allows any client to call request functions, but the function checks `NetworkManager.is_host` and validates the request.
224
+
225
+ ## Layer 3 — The Three Routing Patterns
226
+
227
+ Every networked function fits one of three patterns. Pick the right one or you'll have bugs.
228
+
229
+ ### Pattern A: Host → Client (state broadcast)
230
+
231
+ State the host owns, replicated read-only to clients.
232
+
233
+ ```gdscript
234
+ @rpc("authority", "call_remote", "reliable")
235
+ func _broadcast_score(peer: int, value: int) -> void:
236
+ score[peer] = value
237
+ ```
238
+
239
+ **Rules:**
240
+ - `authority` mode = only host can call this (anyone else's call is ignored).
241
+ - `call_remote` = doesn't run on the caller (host doesn't need to call its own update).
242
+ - `reliable` = must arrive (state corruption otherwise).
243
+
244
+ ### Pattern B: Client → Host (intent / request)
245
+
246
+ Client wants something to happen. Host decides yes or no.
247
+
248
+ ```gdscript
249
+ @rpc("any_peer", "call_remote", "reliable")
250
+ func _host_handle_use_item_request(item_id: String) -> void:
251
+ if not NetworkManager.is_host: return
252
+ var sender := multiplayer.get_remote_sender_id()
253
+ if _can_use(sender, item_id):
254
+ _host_apply_use_item(sender, item_id)
255
+ ```
256
+
257
+ **Rules:**
258
+ - `any_peer` = any client can call.
259
+ - First line is always `if not NetworkManager.is_host: return`. Defensive.
260
+ - Always validate (`_can_use`). Trusting client input is the #1 multiplayer cheat vector.
261
+ - Use `multiplayer.get_remote_sender_id()` — clients can't lie about who sent the request.
262
+
263
+ ### Pattern C: Client → Client (cosmetic relay)
264
+
265
+ Visual-only events. Host relays but doesn't validate.
266
+
267
+ ```gdscript
268
+ @rpc("any_peer", "call_remote", "unreliable")
269
+ func _peer_play_emote(emote_id: String) -> void:
270
+ # everyone plays the emote on the sender's player
271
+ var sender := multiplayer.get_remote_sender_id()
272
+ _play_emote_visual(sender, emote_id)
273
+ ```
274
+
275
+ **Rules:**
276
+ - `unreliable` is fine for cosmetic stuff (animations, particles, footstep SFX).
277
+ - No validation needed — worst case is a player's emote desyncs by a frame.
278
+ - Save bandwidth: keep payloads tiny.
279
+
280
+ **Anti-pattern:** mixing patterns in one function. A function that broadcasts state AND handles client intent is going to have bugs. Split.
281
+
282
+ ## Layer 4 — Real-time Rendering
283
+
284
+ The local player needs to feel instant. Remote players need to look smooth despite network jitter. Two techniques:
285
+
286
+ ### Client-side prediction (for the local player's input)
287
+
288
+ ```gdscript
289
+ # Local player runs movement immediately on input. Host runs the SAME function
290
+ # authoritatively. If they diverge, the client snaps to host state.
291
+
292
+ func _physics_process(delta: float) -> void:
293
+ if not _is_local_authority(): return
294
+ var input := _read_input()
295
+ velocity = _compute_velocity(input, velocity, delta)
296
+ move_and_slide()
297
+ if NetworkManager.local_peer_id != 1: # not the host
298
+ _send_input_to_host(input)
299
+
300
+ @rpc("any_peer", "call_remote", "unreliable_ordered")
301
+ func _send_input_to_host(_input: InputState) -> void:
302
+ if not NetworkManager.is_host: return
303
+ # host runs the SAME _compute_velocity to validate + reconcile
304
+ pass
305
+ ```
306
+
307
+ ### Interpolation (for remote players' positions)
308
+
309
+ Don't snap remote players to incoming positions — interpolate.
310
+
311
+ ```gdscript
312
+ var _network_position_buffer: Array[Vector3] = []
313
+ var _network_position_timestamps: Array[float] = []
314
+
315
+ func _physics_process(_delta: float) -> void:
316
+ if _is_local_authority(): return # local handled by prediction
317
+ _interpolate_to_buffer()
318
+
319
+ @rpc("authority", "call_remote", "unreliable_ordered")
320
+ func _receive_remote_position(pos: Vector3, t: float) -> void:
321
+ _network_position_buffer.append(pos)
322
+ _network_position_timestamps.append(t)
323
+ while _network_position_buffer.size() > 4:
324
+ _network_position_buffer.pop_front()
325
+ _network_position_timestamps.pop_front()
326
+
327
+ func _interpolate_to_buffer() -> void:
328
+ if _network_position_buffer.size() < 2: return
329
+ var render_time := Time.get_ticks_msec() / 1000.0 - 0.1 # 100ms interp delay
330
+ # find the two buffer entries that bracket render_time, lerp between them
331
+ # (full impl: ~30 lines)
332
+ pass
333
+ ```
334
+
335
+ ## Common mistakes (anti-patterns)
336
+
337
+ | Mistake | What goes wrong | Fix |
338
+ |---|---|---|
339
+ | Putting health on the client and trusting it | Trivial cheats: edit memory, infinite HP. | Move all damage logic to host. Client requests, host decides. |
340
+ | Calling `multiplayer.peer_connected.connect()` directly in 5 different scripts | Connection logic scattered. Refactor pain. | Always go through `NetworkManager` signals. |
341
+ | `@rpc("any_peer")` with no `is_host` check inside | Client can broadcast as if host. State corruption. | First line of every `any_peer` handler is `if not is_host: return`. |
342
+ | Reliable RPC for cosmetic stuff (footsteps, particles) | Bandwidth blowup. | Use `unreliable` for cosmetic. Reserve `reliable` for state. |
343
+ | Sending position 60×/sec via reliable RPC | Network spam, dropped packets cause stutters. | `unreliable_ordered` at 20–30 Hz + interpolation buffer on receivers. |
344
+ | Adding multiplayer after the game's built | Single-player code assumed direct state writes everywhere. Massive refactor. | Use this skill BEFORE writing game logic. |
345
+ | Forgetting host-leaves handling | Host disconnects → game freezes. | `multiplayer.server_disconnected` → graceful end-session in `NetworkManager.leave()`. |
346
+
347
+ ## When to NOT use this skill
348
+
349
+ - **Single-player game with leaderboards.** No multiplayer needed.
350
+ - **Async multiplayer** (turn-based via REST API). This skill assumes real-time.
351
+ - **Dedicated server with anti-cheat.** Use `/summer:client-server-multiplayer` (when shipped).
352
+ - **Already-shipped game getting multiplayer retrofit.** Sorry — you're in for a refactor. This skill helps for the redesign but not the migration.
353
+
354
+ ## Collaborative protocol
355
+
356
+ This skill writes multiple new files (`autoloads/network_manager.gd`, `autoloads/game_state.gd`, plus updates to `project.godot` for the autoload registration). Always ask before each phase:
357
+
358
+ > May I create the NetworkManager autoload?
359
+ > May I create the GameState autoload and register both in project.godot?
360
+ > May I add the prediction/interpolation logic to your existing player scene?
361
+
362
+ Don't apply all four layers in one shot — checkpoint after each layer so the user can verify before adding the next.
363
+
364
+ See [`_shared/collaborative-protocol.md`](../../_shared/collaborative-protocol.md).
365
+
366
+ ## Want a working starter?
367
+
368
+ No template ships with this exact architecture yet — it's deliberately built fresh for each project because the State Layer's authority decisions are game-specific (a stealth game's "visibility" and a shooter's "ammo count" both want host authority but for different reasons). Run this skill from scratch.
369
+
370
+ When the `template-co-op-3d` template ships in the registry, this skill will reference it as a starter — for now it's hand-rolled.
371
+
372
+ ## See also
373
+
374
+ - [`_shared/godot-version.md`](../../_shared/godot-version.md) — Godot 4.5 multiplayer API stability notes
375
+ - [`_shared/mcp-tools-reference.md`](../../_shared/mcp-tools-reference.md) — `summer_project_setting` for autoload registration
376
+ - [`_shared/gd-style.md`](../../_shared/gd-style.md) — typed-GDScript conventions
377
+ - [`host-authoritative-state`](../host-authoritative-state/SKILL.md) — deeper dive on JUST the state ownership decision matrix
378
+ - [`setup-multiplayer`](../setup-multiplayer/SKILL.md) — lighter intro that just gets a session running (use that one if the user just wants 2 players to see each other; use this one if they want to ship a real game)
@@ -102,3 +102,23 @@ Always use `Color(r, g, b, a)` with values 0.0–1.0:
102
102
  - Cool white: `Color(0.9, 0.95, 1, 1)`
103
103
  - Orange (torch): `Color(1, 0.6, 0.2, 1)`
104
104
  - Green (alien): `Color(0.5, 1, 0.5, 1)`
105
+
106
+ ## Fallback (no MCP — edit `.tscn` directly)
107
+
108
+ If Summer MCP isn't connected, append the lighting block to your scene file:
109
+
110
+ ```
111
+ [node name="Sun" type="DirectionalLight3D" parent="World"]
112
+ transform = Transform3D(0.866, 0, -0.5, 0.354, 0.707, 0.612, 0.354, -0.707, 0.612, 0, 0, 0)
113
+ light_color = Color(1, 0.95, 0.9, 1)
114
+ light_energy = 1.0
115
+ shadow_enabled = true
116
+
117
+ [node name="WorldEnvironment" type="WorldEnvironment" parent="World"]
118
+ ```
119
+
120
+ Then create `Environment` and `Sky` resources via the inspector or load from `.tres`.
121
+
122
+ ## Collaborative protocol
123
+
124
+ This skill mutates the scene (adds lights + WorldEnvironment + tunes Environment sub-properties). Always ask before applying: "May I add a DirectionalLight3D Sun with shadows + a WorldEnvironment with a procedural sky?". See `../../_shared/collaborative-protocol.md`.
@@ -440,3 +440,13 @@ After first playable, ask the user what to improve. Common next steps:
440
440
  | Debug | `summer_get_console` | Read output |
441
441
  | Debug | `summer_get_script_errors` | Check .gd compile |
442
442
  | Debug | `summer_clear_console` | Clear output |
443
+
444
+ ---
445
+
446
+ ## Collaborative protocol
447
+
448
+ This skill orchestrates a full game build — many writes across scenes, scripts, and project settings. Always ask before each phase: "May I bootstrap the project (scenes, project.godot, folders)?", "May I generate the assets (image + 3D) for X, Y, Z?", "May I scaffold the player scene + attach `player.gd`?". Group related writes into one ask per phase, never bulk-write across phases. See `../../_shared/collaborative-protocol.md`.
449
+
450
+ ## Fallback
451
+
452
+ No fallback for this — Summer MCP required. The whole point of `make-game` is the orchestration layer over `summer_*` tools. If MCP isn't connected, fall back to running the individual sub-skills (`fps-controller`, `3d-lighting`, etc.) which each document their own MCP-or-`.tscn` fallback paths.
@@ -107,3 +107,11 @@ Use `./` for paths relative to the scene root:
107
107
  2. **Unnamed scene:** Saving without a path fails if the scene was never saved. Use `path` for new scenes.
108
108
  3. **Duplicate names:** Godot auto-renames (Node, Node2, etc.). Use descriptive unique names.
109
109
  4. **Mixing 2D and 3D:** Don't put Node2D under Node3D or vice versa in the same hierarchy.
110
+
111
+ ## Fallback
112
+
113
+ No fallback for this — Summer MCP required. Handwriting `.tscn` files for hierarchy mutations is error-prone (UID collisions, wrong format version, broken sub_resource refs). If MCP isn't connected, open the scene in the Godot editor and use the SceneTree dock.
114
+
115
+ ## Collaborative protocol
116
+
117
+ This skill creates and mutates scene files. Always ask before applying: "May I create `res://scenes/player.tscn` and instantiate it under `./World`?". See `../../_shared/collaborative-protocol.md`.
@@ -120,3 +120,31 @@ For UI that scales with window size:
120
120
  3. MarginContainer with percentage-based margins (may need theme or script)
121
121
 
122
122
  For simple cases, CenterContainer + fixed-size children works. For complex layouts, a Theme resource or script may be needed.
123
+
124
+ ## Fallback (no MCP — edit `.tscn` directly)
125
+
126
+ If Summer MCP isn't connected, write the menu hierarchy by hand:
127
+
128
+ ```
129
+ [node name="MainMenu" type="Control" parent="."]
130
+ anchors_preset = 15
131
+
132
+ [node name="Margin" type="MarginContainer" parent="MainMenu"]
133
+ anchors_preset = 15
134
+ theme_override_constants/margin_left = 50
135
+ theme_override_constants/margin_right = 50
136
+ theme_override_constants/margin_top = 50
137
+ theme_override_constants/margin_bottom = 50
138
+
139
+ [node name="VBox" type="VBoxContainer" parent="MainMenu/Margin"]
140
+
141
+ [node name="Title" type="Label" parent="MainMenu/Margin/VBox"]
142
+ text = "My Game"
143
+
144
+ [node name="StartButton" type="Button" parent="MainMenu/Margin/VBox"]
145
+ text = "Start Game"
146
+ ```
147
+
148
+ ## Collaborative protocol
149
+
150
+ This skill writes UI scene nodes and wires button signals. Always ask before applying: "May I add the MainMenu (Control + Margin + VBox + Title + Start/Quit buttons) and connect Start.pressed to `_on_start_pressed`?". See `../../_shared/collaborative-protocol.md`.