summer-engine 2.7.0 → 2.8.1

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 (153) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.codex-plugin/plugin.json +3 -2
  3. package/.cursor-plugin/plugin.json +2 -1
  4. package/.opencode/INSTALL.md +1 -1
  5. package/.opencode/plugins/summer.js +1 -1
  6. package/AGENTS.md +4 -4
  7. package/CHANGELOG.md +28 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +58 -21
  10. package/assets/autopilot/README.md +51 -0
  11. package/assets/autopilot/autopilot.gd +222 -0
  12. package/assets/autopilot/probe_base.gd +262 -0
  13. package/assets/autopilot/run.sh +78 -0
  14. package/commands/gameskill.md +11 -2
  15. package/dist/bin/summer.js +8 -0
  16. package/dist/commands/config.d.ts +2 -0
  17. package/dist/commands/config.js +53 -0
  18. package/dist/commands/create.d.ts +1 -0
  19. package/dist/commands/create.js +38 -6
  20. package/dist/commands/list.js +1 -1
  21. package/dist/commands/login.d.ts +19 -0
  22. package/dist/commands/login.js +136 -37
  23. package/dist/commands/logout.js +3 -16
  24. package/dist/commands/logs.d.ts +2 -0
  25. package/dist/commands/logs.js +19 -0
  26. package/dist/commands/mcp.js +7 -2
  27. package/dist/commands/open.js +1 -1
  28. package/dist/commands/publish.d.ts +2 -0
  29. package/dist/commands/publish.js +20 -0
  30. package/dist/commands/releases.d.ts +2 -0
  31. package/dist/commands/releases.js +20 -0
  32. package/dist/commands/run.js +1 -1
  33. package/dist/lib/api-client.d.ts +27 -8
  34. package/dist/lib/api-client.js +65 -14
  35. package/dist/lib/auth.d.ts +40 -12
  36. package/dist/lib/auth.js +173 -37
  37. package/dist/lib/cloud/engine-bridge.d.ts +17 -4
  38. package/dist/lib/cloud/engine-bridge.js +32 -6
  39. package/dist/lib/config.d.ts +20 -0
  40. package/dist/lib/config.js +142 -0
  41. package/dist/lib/creator.d.ts +93 -0
  42. package/dist/lib/creator.js +393 -0
  43. package/dist/lib/engine.d.ts +35 -2
  44. package/dist/lib/engine.js +240 -6
  45. package/dist/lib/public-surface-paths.d.ts +13 -0
  46. package/dist/lib/public-surface-paths.js +42 -0
  47. package/dist/lib/skills-registry.d.ts +8 -0
  48. package/dist/lib/skills-registry.js +9 -0
  49. package/dist/lib/store.d.ts +14 -0
  50. package/dist/lib/store.js +145 -0
  51. package/dist/lib/summer-compatibility.d.ts +6 -0
  52. package/dist/lib/summer-compatibility.js +22 -0
  53. package/dist/mcp/server.d.ts +8 -1
  54. package/dist/mcp/server.js +25 -4
  55. package/dist/mcp/tools/creator-tools.d.ts +2 -0
  56. package/dist/mcp/tools/creator-tools.js +105 -0
  57. package/dist/mcp/tools/debug-tools.d.ts +12 -0
  58. package/dist/mcp/tools/debug-tools.js +100 -1
  59. package/dist/mcp/tools/generate-tools.js +16 -6
  60. package/dist/mcp/tools/project-tools.js +131 -16
  61. package/dist/mcp/tools/scene-tools.js +166 -66
  62. package/dist/mcp/tools/visual-tools.js +32 -8
  63. package/dist/mcp/tools/with-engine.js +51 -19
  64. package/hooks/pre-commit-doctor.sh +0 -0
  65. package/hooks/run-hook.cmd +0 -0
  66. package/hooks/session-start.sh +5 -5
  67. package/package.json +3 -1
  68. package/references/collaborative-protocol.md +50 -0
  69. package/references/gd-style.md +121 -0
  70. package/references/godot-version.md +51 -0
  71. package/references/mcp-tools-reference.md +199 -0
  72. package/references/public-surface-language.md +30 -0
  73. package/references/summer-folder.md +90 -0
  74. package/references/template-registry.md +73 -0
  75. package/skills/2d-assets/character-portrait/SKILL.md +17 -12
  76. package/skills/2d-assets/concept-art/SKILL.md +10 -9
  77. package/skills/2d-assets/pixel-art/SKILL.md +36 -20
  78. package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
  79. package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
  80. package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
  81. package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
  82. package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
  83. package/skills/3d-assets/character-model/SKILL.md +34 -19
  84. package/skills/3d-assets/environment-kit/SKILL.md +11 -5
  85. package/skills/3d-assets/organic-model/SKILL.md +8 -5
  86. package/skills/3d-assets/prop-model/SKILL.md +9 -3
  87. package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
  88. package/skills/README.md +1 -1
  89. package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
  90. package/skills/animation/animation-tree/SKILL.md +39 -15
  91. package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
  92. package/skills/animation/generate-motion/SKILL.md +47 -15
  93. package/skills/animation/procedural-animation/SKILL.md +31 -13
  94. package/skills/animation/retarget/SKILL.md +127 -79
  95. package/skills/audio/adaptive-music/SKILL.md +24 -20
  96. package/skills/audio/ambient-bed/SKILL.md +22 -16
  97. package/skills/audio/audio-direction/SKILL.md +15 -10
  98. package/skills/audio/music-track/SKILL.md +18 -12
  99. package/skills/audio/sound-effect/SKILL.md +17 -12
  100. package/skills/audio/voice-line/SKILL.md +42 -42
  101. package/skills/character-controllers/fps-controller/SKILL.md +26 -16
  102. package/skills/debugging/debug/SKILL.md +52 -20
  103. package/skills/deployment/export-and-ship/SKILL.md +105 -38
  104. package/skills/deployment/remote-deploy/SKILL.md +56 -23
  105. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
  106. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
  107. package/skills/level-design/design-level/SKILL.md +22 -8
  108. package/skills/level-design/scene-to-level/SKILL.md +13 -7
  109. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
  110. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
  111. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
  112. package/skills/performance/tune-performance/SKILL.md +51 -32
  113. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
  114. package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
  115. package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
  116. package/skills/scene-and-project/make-game/SKILL.md +2 -1
  117. package/skills/scene-and-project/play/SKILL.md +10 -4
  118. package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
  119. package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
  120. package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
  121. package/skills/video/animated-loop/SKILL.md +63 -39
  122. package/skills/video/cinematic-cutscene/SKILL.md +66 -30
  123. package/skills/video/trailer-shot/SKILL.md +30 -21
  124. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
  125. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
  126. package/skills/visual-effects/game-feel/SKILL.md +51 -33
  127. package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
  128. package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
  129. package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
  130. package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
  131. package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
  132. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
  133. package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
  134. package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
  135. package/skills/workflow/brainstorming/SKILL.md +1 -1
  136. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  137. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  138. package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
  139. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
  140. package/skills/workflow/gameskill/SKILL.md +11 -2
  141. package/skills/workflow/headless-scripting/SKILL.md +407 -0
  142. package/skills/workflow/investigating-bugs/SKILL.md +4 -2
  143. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  144. package/skills/workflow/mcpupdate/SKILL.md +3 -3
  145. package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
  146. package/skills/workflow/skill-create/SKILL.md +7 -7
  147. package/skills/workflow/skill-test/SKILL.md +3 -3
  148. package/skills/workflow/using-summer/SKILL.md +12 -2
  149. package/skills/workflow/verification-before-completion/SKILL.md +2 -0
  150. package/skills/workflow/writing-plans/SKILL.md +7 -7
  151. package/skills/workflow/writing-skills/SKILL.md +7 -5
  152. package/skills/workflow/writing-skills/render-graphs.js +0 -0
  153. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: visual-effects
7
7
  user-invocable: true
8
- allowed-tools: Read Write Edit summer_add_node summer_set_prop summer_set_resource_property summer_inspect_node summer_save_scene
8
+ allowed-tools: Read Write Edit summer_write_file summer_read_file summer_create_scene summer_add_node summer_set_prop summer_set_resource_property summer_inspect_node summer_inspect_resource summer_save_scene summer_get_script_errors summer_get_debugger_errors summer_play summer_stop
9
9
  paths: ["**/*.tscn", "**/*.gd", "**/*.gdshader", "addons/vfx/**"]
10
10
  ---
11
11
 
@@ -35,12 +35,17 @@ Fire in games is almost never a video clip and never an AI-generated sprite. It'
35
35
  ### 1. Files to create
36
36
 
37
37
  ```
38
+ addons/vfx/_building-blocks/noise-3d-fbm.gdshaderinc # copy from this skill FIRST
38
39
  addons/vfx/fire/fire.gdshader # spatial shader for the visual quad
39
40
  addons/vfx/fire/fire_process.gdshader # particle shader for the simulation
40
41
  addons/vfx/fire/fire_controller.gd # parameter dials + lifetime
41
42
  addons/vfx/fire/fire.tscn # reusable scene (instantiate per torch)
42
43
  ```
43
44
 
45
+ `fire.gdshader` `#include`s the FBM noise file. A missing include is a hard compile
46
+ error, so copy `_building-blocks/noise-3d-fbm.gdshaderinc` from this skill into
47
+ `res://addons/vfx/_building-blocks/` before writing the shader.
48
+
44
49
  ### 2. Visual shader code
45
50
 
46
51
  `addons/vfx/fire/fire.gdshader`:
@@ -62,22 +67,41 @@ uniform float distortion : hint_range(0.0, 0.6) = 0.20;
62
67
  uniform float emission_energy : hint_range(0.0, 8.0) = 3.5;
63
68
  uniform float soft_edge : hint_range(0.01, 0.5) = 0.20;
64
69
 
70
+ // INSTANCE_CUSTOM is vertex-stage only, so carry what fragment() needs across.
71
+ varying float age;
72
+ varying float seed;
73
+
65
74
  void vertex() {
66
- // Billboard the quad toward the camera (view-space).
67
- MODELVIEW_MATRIX = VIEW_MATRIX * mat4(
68
- INV_VIEW_MATRIX[0],
69
- INV_VIEW_MATRIX[1],
70
- INV_VIEW_MATRIX[2],
71
- MODEL_MATRIX[3]
72
- );
75
+ // Billboard toward the camera, keeping per-particle rotation (INSTANCE_CUSTOM.x)
76
+ // and per-particle scale. The bare `VIEW_MATRIX * mat4(INV_VIEW_MATRIX[0..2],
77
+ // MODEL_MATRIX[3])` form throws both away — this mirrors what Godot's own
78
+ // BILLBOARD_PARTICLES generator emits (scene/resources/material.cpp:1313-1334).
79
+ mat4 mat_world = mat4(
80
+ normalize(INV_VIEW_MATRIX[0]),
81
+ normalize(INV_VIEW_MATRIX[1]),
82
+ normalize(INV_VIEW_MATRIX[2]),
83
+ MODEL_MATRIX[3]);
84
+ mat_world = mat_world * mat4(
85
+ vec4(cos(INSTANCE_CUSTOM.x), -sin(INSTANCE_CUSTOM.x), 0.0, 0.0),
86
+ vec4(sin(INSTANCE_CUSTOM.x), cos(INSTANCE_CUSTOM.x), 0.0, 0.0),
87
+ vec4(0.0, 0.0, 1.0, 0.0),
88
+ vec4(0.0, 0.0, 0.0, 1.0));
89
+ MODELVIEW_MATRIX = VIEW_MATRIX * mat_world * mat4(
90
+ vec4(length(MODEL_MATRIX[0].xyz), 0.0, 0.0, 0.0),
91
+ vec4(0.0, length(MODEL_MATRIX[1].xyz), 0.0, 0.0),
92
+ vec4(0.0, 0.0, length(MODEL_MATRIX[2].xyz), 0.0),
93
+ vec4(0.0, 0.0, 0.0, 1.0));
94
+ MODELVIEW_NORMAL_MATRIX = mat3(MODELVIEW_MATRIX);
95
+
96
+ // CUSTOM.x is the rotation angle, NOT the age. The age lives in CUSTOM.y
97
+ // (it counts up by DELTA/LIFETIME) and CUSTOM.w holds the lifetime scale.
98
+ age = clamp(INSTANCE_CUSTOM.y / max(INSTANCE_CUSTOM.w, 0.0001), 0.0, 1.0);
99
+ seed = fract(sin(float(INSTANCE_ID) * 12.9898) * 43758.5453);
73
100
  }
74
101
 
75
102
  void fragment() {
76
- // CUSTOM.x is particle age 0..1 (set by the process material).
77
- float age = CUSTOM.x;
78
-
79
103
  // Distort UVs with scrolling 3D noise so the flame writhes.
80
- vec3 np = vec3(UV * noise_scale, TIME * noise_speed);
104
+ vec3 np = vec3(UV * noise_scale, TIME * noise_speed + seed * 10.0);
81
105
  float n = fbm3(np);
82
106
  vec2 uv = UV + vec2(n - 0.5) * distortion;
83
107
 
@@ -99,8 +123,10 @@ void fragment() {
99
123
  // Add the noise as a brightness modulation so flames flicker.
100
124
  float flicker = mix(0.6, 1.2, n);
101
125
 
102
- ALBEDO = col.rgb * flicker;
103
- EMISSION = col.rgb * flicker * emission_energy * (1.0 - age);
126
+ // `render_mode unshaded` outputs ALBEDO and discards EMISSION entirely
127
+ // (scene_forward_clustered.glsl:2962 — `frag_color = vec4(albedo, alpha)`
128
+ // under MODE_UNSHADED), so the HDR boost that drives glow lives in ALBEDO.
129
+ ALBEDO = col.rgb * flicker * emission_energy * (1.0 - age * 0.5);
104
130
  ALPHA = col.a * mask * (1.0 - age * age);
105
131
  }
106
132
  ```
@@ -118,36 +144,61 @@ uniform float rise_speed : hint_range(0.1, 6.0) = 1.8;
118
144
  uniform float spread_angle : hint_range(0.0, 1.5) = 0.35;
119
145
  uniform float lifetime_var : hint_range(0.0, 1.0) = 0.25;
120
146
 
147
+ // `rand_from_seed()` is generated into ParticleProcessMaterial's own shader; it is
148
+ // NOT a built-in you can call from a hand-written particles shader
149
+ // ("SHADER ERROR: No matching function found for: 'rand_from_seed'"). Supply one.
150
+ uint hash_u(uint x) {
151
+ x ^= x >> 16u;
152
+ x *= 0x7feb352du;
153
+ x ^= x >> 15u;
154
+ x *= 0x846ca68bu;
155
+ x ^= x >> 16u;
156
+ return x;
157
+ }
158
+
159
+ float rnd(uint s) {
160
+ return float(hash_u(s) & 0x00ffffffu) / 16777216.0;
161
+ }
162
+
121
163
  void start() {
164
+ uint s = RANDOM_SEED + INDEX * 747796405u;
165
+
122
166
  // Random spawn within a small disc.
123
- float r = sqrt(rand_from_seed(RANDOM_SEED + uint(7))) * 0.18;
124
- float th = rand_from_seed(RANDOM_SEED + uint(11)) * 6.2831853;
167
+ float r = sqrt(rnd(s + 7u)) * 0.18;
168
+ float th = rnd(s + 11u) * 6.2831853;
125
169
  TRANSFORM[3].xyz = vec3(cos(th) * r, 0.0, sin(th) * r);
126
170
 
127
171
  // Initial velocity: mostly up, slight cone.
128
172
  vec3 dir = vec3(
129
- (rand_from_seed(RANDOM_SEED + uint(13)) - 0.5) * spread_angle,
173
+ (rnd(s + 13u) - 0.5) * spread_angle,
130
174
  1.0,
131
- (rand_from_seed(RANDOM_SEED + uint(17)) - 0.5) * spread_angle
175
+ (rnd(s + 17u) - 0.5) * spread_angle
132
176
  );
133
177
  VELOCITY = normalize(dir) * rise_speed;
134
178
 
135
- // Stagger lifetimes for variety.
136
- LIFETIME = LIFETIME * (1.0 + (rand_from_seed(RANDOM_SEED + uint(19)) - 0.5) * 2.0 * lifetime_var);
137
-
138
- CUSTOM.x = 0.0; // age
139
- CUSTOM.y = rand_from_seed(RANDOM_SEED + uint(23)); // per-particle seed
179
+ // LIFETIME is read-only ("SHADER ERROR: Constants cannot be modified") and there
180
+ // is no TOTAL_LIFETIME. Stagger lifetimes the way ParticleProcessMaterial does:
181
+ // keep a per-particle lifetime scale in CUSTOM.w and retire the particle when
182
+ // the age in CUSTOM.y passes it.
183
+ CUSTOM = vec4(0.0);
184
+ CUSTOM.w = 1.0 + (rnd(s + 19u) - 0.5) * 2.0 * lifetime_var;
185
+ CUSTOM.z = rnd(s + 23u); // per-particle seed
140
186
  }
141
187
 
142
188
  void process() {
189
+ // CUSTOM.x = rotation angle, .y = age, .z = per-particle seed, .w = lifetime scale.
190
+ CUSTOM.y += DELTA / LIFETIME;
191
+ if (CUSTOM.y > CUSTOM.w) {
192
+ ACTIVE = false;
193
+ }
194
+ float age = clamp(CUSTOM.y / max(CUSTOM.w, 0.0001), 0.0, 1.0);
195
+
143
196
  // Rise + small horizontal sway.
144
197
  VELOCITY.y += DELTA * 0.6;
145
- VELOCITY.x += sin(TIME * 2.0 + CUSTOM.y * 6.28) * DELTA * 0.4;
146
- VELOCITY.z += cos(TIME * 2.3 + CUSTOM.y * 6.28) * DELTA * 0.4;
198
+ VELOCITY.x += sin(TIME * 2.0 + CUSTOM.z * 6.28) * DELTA * 0.4;
199
+ VELOCITY.z += cos(TIME * 2.3 + CUSTOM.z * 6.28) * DELTA * 0.4;
147
200
 
148
201
  // Shrink as it ages.
149
- float age = clamp(1.0 - LIFETIME / TOTAL_LIFETIME, 0.0, 1.0);
150
- CUSTOM.x = age;
151
202
  float scale = mix(1.0, 0.35, age);
152
203
  TRANSFORM[0].xyz = vec3(scale, 0.0, 0.0);
153
204
  TRANSFORM[1].xyz = vec3(0.0, scale, 0.0);
@@ -224,36 +275,63 @@ func extinguish(fade_seconds: float = 0.5) -> void:
224
275
  ```
225
276
  GPUParticles3D ("Fire") [script: fire_controller.gd]
226
277
  ├── process_material: ParticleProcessMaterial (or ShaderMaterial w/ fire_process.gdshader)
227
- └── draw_pass_1: QuadMesh (size 1.0 × 1.0)
228
- └── surface_material_override[0]: ShaderMaterial (shader: fire.gdshader)
278
+ └── draw_pass_1: QuadMesh (size Vector2(1, 1))
279
+ └── material: ShaderMaterial (shader: fire.gdshader)
229
280
  ```
230
281
 
282
+ `process_material` and `draw_pass_1` are resource properties on the one node, not
283
+ child nodes. `surface_material_override` belongs to `MeshInstance3D` and does not
284
+ apply here — a draw pass mesh carries its material in `Mesh.material`.
285
+
231
286
  Add an OmniLight3D as a sibling (parented to the same anchor as the GPUParticles3D, NOT under it) for proper light cast. Recommended: `light_color = Color(1.0, 0.6, 0.25)`, `light_energy = 1.6`, `omni_range = 6.0`. Pulse it lightly via the controller for free atmosphere.
232
287
 
233
288
  ### 6. Wire it in (MCP calls)
234
289
 
290
+ Every scene-mutating call takes an explicit `scenePath`. `summer_set_prop` uses `key`
291
+ (not `property`) and only accepts a string, number, or boolean.
292
+
235
293
  ```
236
- summer_add_node(parent="./World/Torch", type="GPUParticles3D", name="Fire")
237
- summer_set_prop(path="./World/Torch/Fire", property="script", value="res://addons/vfx/fire/fire_controller.gd")
294
+ summer_write_file(path="res://addons/vfx/_building-blocks/noise-3d-fbm.gdshaderinc", content="<from this skill>", create_only=true)
295
+ summer_write_file(path="res://addons/vfx/fire/fire.gdshader", content="<section 2>", create_only=true)
296
+ summer_write_file(path="res://addons/vfx/fire/fire_controller.gd", content="<section 4>", create_only=true)
238
297
 
239
- summer_add_node(parent="./World/Torch/Fire", type="ParticleProcessMaterial", name="ProcessMat")
240
- summer_set_resource_property(nodePath="./World/Torch/Fire", resourceProperty="process_material", value="res://addons/vfx/fire/process_mat.tres")
298
+ summer_add_node(scenePath="res://main.tscn", parent="./World/Torch", type="GPUParticles3D", name="Fire")
299
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="script", value="res://addons/vfx/fire/fire_controller.gd")
241
300
 
242
- summer_set_resource_property(nodePath="./World/Torch/Fire", resourceProperty="draw_pass_1", value="res://addons/vfx/fire/quad.tres")
243
- # quad.tres is a QuadMesh with size (1,1) and surface_material_override[0] = fire_material.tres (ShaderMaterial wrapping fire.gdshader)
301
+ # ParticleProcessMaterial is a Material, not a Node — you cannot summer_add_node it.
302
+ # Assign the resource by path; the controller configures it on _ready().
303
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="process_material", value="res://addons/vfx/fire/process_mat.tres")
244
304
 
245
- summer_set_prop(path="./World/Torch/Fire", property="flame_height", value=1.2)
246
- summer_set_prop(path="./World/Torch/Fire", property="flame_radius", value=0.35)
247
- summer_set_prop(path="./World/Torch/Fire", property="particle_count", value=96)
305
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="draw_pass_1", value="res://addons/vfx/fire/quad.tres")
306
+ # quad.tres is a QuadMesh with size (1,1) and material = fire_material.tres (ShaderMaterial wrapping fire.gdshader)
307
+ summer_set_resource_property(scenePath="res://main.tscn", nodePath="./World/Torch/Fire", resourceProperty="draw_pass_1", subProperty="size", value="Vector2(1, 1)")
248
308
 
249
- summer_add_node(parent="./World/Torch", type="OmniLight3D", name="FireLight")
250
- summer_set_prop(path="./World/Torch/FireLight", property="light_color", value="Color(1.0, 0.6, 0.25)")
251
- summer_set_prop(path="./World/Torch/FireLight", property="light_energy", value=1.6)
252
- summer_set_prop(path="./World/Torch/FireLight", property="omni_range", value=6.0)
309
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="flame_height", value=1.2)
310
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="flame_radius", value=0.35)
311
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/Fire", key="particle_count", value=96)
253
312
 
254
- summer_save_scene
313
+ summer_add_node(scenePath="res://main.tscn", parent="./World/Torch", type="OmniLight3D", name="FireLight")
314
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/FireLight", key="light_color", value="Color(1.0, 0.6, 0.25)")
315
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/FireLight", key="light_energy", value=1.6)
316
+ summer_set_prop(scenePath="res://main.tscn", path="./World/Torch/FireLight", key="omni_range", value=6.0)
317
+
318
+ summer_save_scene(scenePath="res://main.tscn")
255
319
  ```
256
320
 
321
+ ### 6b. Verify
322
+
323
+ Every file this recipe writes is compiled code. Check it before claiming the fire works.
324
+
325
+ ```
326
+ summer_get_script_errors # fire_controller.gd parsed?
327
+ summer_play
328
+ summer_get_debugger_errors # shader compile errors surface here at runtime
329
+ summer_stop
330
+ ```
331
+
332
+ A missing `noise-3d-fbm.gdshaderinc` and an un-copied shader both show up as compile
333
+ errors, not as an invisible flame. Read the errors before tuning any dial.
334
+
257
335
  ### 7. Parameters to tune
258
336
 
259
337
  | Parameter | Range | Effect |
@@ -337,12 +415,14 @@ OmniLight3D: light_color = Color(0.40, 0.65, 1.0), light_energy = 2.4
337
415
  - **Missing `cull_disabled`.** Particles billboard toward camera; if you cull backfaces, the flame disappears at certain angles.
338
416
  - **`depth_draw_opaque` on the visual material.** You'll z-fight with anything inside the flame (the torch handle). Use `depth_draw_never`.
339
417
  - **OmniLight3D parented under the GPUParticles3D.** The particles emit in local space; the light moves with them. Sibling, not child.
340
- - **Procedural noise computed in world space without a per-instance seed.** Every particle pulses identically, looking like a strobe. The included shader uses `CUSTOM.y` as a per-particle seed.
418
+ - **Procedural noise computed in world space without a per-instance seed.** Every particle pulses identically, looking like a strobe. The included shader hashes `INSTANCE_ID` into a per-particle seed in `vertex()` and passes it to `fragment()` via a varying.
419
+ - **Reading `CUSTOM` in `fragment()`.** In a `spatial` shader `CUSTOM` does not exist at all ("SHADER ERROR: Unknown identifier in expression: 'CUSTOM'"). The particle built-in is `INSTANCE_CUSTOM`, it is vertex-stage only, and `INSTANCE_CUSTOM.x` is the rotation angle — the age is in `.y`.
420
+ - **Hand-rolling the billboard as `VIEW_MATRIX * mat4(INV_VIEW_MATRIX[0..2], MODEL_MATRIX[3])`.** That form discards per-particle scale *and* rotation, so `flame_radius`'s per-particle scale never reaches the screen. Use the full block in section 2.
341
421
  - **Generating a fire image and using it as the particle texture.** That's the misroute this skill exists to prevent. The shader-driven look is more alive and trivially recolorable.
342
422
 
343
423
  ## Performance notes
344
424
 
345
- - Default 96 particles per fire: ~0.05 ms on desktop GPU, scales linearly. Budget ~512 fire particles total in a scene before you should think about LOD.
425
+ - Default 96 particles per fire is sub-millisecond on a desktop GPU and scales linearly with count. That is an order-of-magnitude expectation, not a measurement — profile your own scene before budgeting. Budget ~512 fire particles total in a scene before you should think about LOD.
346
426
  - Mobile: drop default to 48, disable the `OmniLight3D` shadow cast, and use a 64×64 noise texture instead of FBM if FPS dips.
347
427
  - LOD: in the controller's `_process`, scale `amount_ratio` by `clamp(1.0 - dist_to_camera / 30.0, 0.05, 1.0)`. Past 30 m, drop to a single billboard quad with the same shader (no particles).
348
428
  - The OmniLight3D shadow is the expensive part. Disable shadows on torches that aren't doing dramatic work.
@@ -358,13 +438,20 @@ OmniLight3D: light_color = Color(0.40, 0.65, 1.0), light_energy = 2.4
358
438
 
359
439
  ## Fallback (no MCP)
360
440
 
361
- VFX is code, no MCP required. The user can:
441
+ Section 6 is fully automatable — `summer_write_file` writes the shaders and the
442
+ controller, `summer_create_scene` + `summer_add_node` + `summer_set_prop` +
443
+ `summer_save_scene` build and save `fire.tscn`. Do not hand these steps to the user
444
+ when the MCP tools are available.
445
+
446
+ Without the MCP connection there is no engine to drive, so the user does it
447
+ manually in Summer Engine:
362
448
 
363
- 1. Create the four files at `addons/vfx/fire/` with the contents above.
364
- 2. In Godot, add a `GPUParticles3D` node, attach `fire_controller.gd`.
449
+ 1. Create the files at `addons/vfx/_building-blocks/` and `addons/vfx/fire/` with the contents above.
450
+ 2. Add a `GPUParticles3D` node, attach `fire_controller.gd`.
365
451
  3. Set `process_material` to a new `ParticleProcessMaterial` (the script configures it on `_ready`).
366
- 4. Set `draw_pass_1` to a new `QuadMesh` (size 1×1), with `surface_material_override[0]` set to a `ShaderMaterial` whose shader is `fire.gdshader`.
452
+ 4. Set `draw_pass_1` to a new `QuadMesh` (size 1×1), with the mesh's `material` set to a `ShaderMaterial` whose shader is `fire.gdshader`.
367
453
  5. Save as `fire.tscn` and instantiate at every torch / brazier.
454
+ 6. Check the Errors dock — a missing `.gdshaderinc` fails the whole shader.
368
455
 
369
456
  ## Handoff
370
457
 
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: visual-effects
7
7
  user-invocable: true
8
- allowed-tools: Read Write Edit summer_add_node summer_set_prop summer_set_resource_property summer_inspect_node summer_save_scene
8
+ allowed-tools: Read Write Edit summer_write_file summer_read_file summer_create_scene summer_add_node summer_replace_node summer_set_prop summer_set_resource_property summer_inspect_node summer_save_scene summer_get_script_errors summer_get_debugger_errors summer_play summer_stop
9
9
  paths: ["**/*.tscn", "**/*.gd", "**/*.gdshader", "addons/vfx/**"]
10
10
  ---
11
11
 
@@ -39,7 +39,7 @@ addons/vfx/hit-spark/hit_spark.gd
39
39
  addons/vfx/hit-spark/hit_spark.tscn
40
40
  ```
41
41
 
42
- No custom shader — uses the canonical additive billboard material (see `_building-blocks/additive-billboard-particles.md`). The default `BaseMaterial3D` with the right flags is enough.
42
+ No custom shader — uses the canonical additive billboard material (see `_building-blocks/additive-billboard-particles.md`). A `StandardMaterial3D` with the right flags is enough. (`BaseMaterial3D` is abstract: `BaseMaterial3D.new()` is a parse error. Its enum constants are still the right names to use.)
43
43
 
44
44
  ### 2. GDScript controller
45
45
 
@@ -101,7 +101,9 @@ func _ensure_material() -> void:
101
101
  if draw_pass_1 == null:
102
102
  var mesh := QuadMesh.new()
103
103
  mesh.size = Vector2(0.08, 0.30) if stretch_to_velocity else Vector2(0.10, 0.10)
104
- var bm := BaseMaterial3D.new()
104
+ # BaseMaterial3D is abstract ("Native class "BaseMaterial3D" cannot be
105
+ # constructed as it is abstract"); StandardMaterial3D is the concrete subclass.
106
+ var bm := StandardMaterial3D.new()
105
107
  bm.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
106
108
  bm.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
107
109
  bm.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
@@ -137,7 +139,12 @@ static func spawn_hit_spark(
137
139
  if not ResourceLoader.exists(scene_path):
138
140
  push_error("HitSpark: scene missing at %s" % scene_path)
139
141
  return null
142
+ # `var inst := load(...).instantiate()` cannot infer a type; the `as HitSpark`
143
+ # cast is what gives this one a static type.
140
144
  var inst := load(scene_path).instantiate() as HitSpark
145
+ if inst == null:
146
+ push_error("HitSpark: %s is not a HitSpark scene" % scene_path)
147
+ return null
141
148
  parent.add_child(inst)
142
149
  inst.global_position = position
143
150
  # Orient local +Y (cone direction) to the surface normal.
@@ -167,11 +174,34 @@ The script builds material and process material on `_ready` if missing, so the `
167
174
 
168
175
  ### 4. Wire it in (MCP calls)
169
176
 
170
- This recipe is a `.tscn` you instantiate per impact, not added once to the scene. After writing the files, the rest is gameplay code:
177
+ This recipe is a `.tscn` you instantiate per impact, not added once to the open scene.
178
+ Building that `.tscn` is itself an MCP job — `summer_create_scene` makes it,
179
+ `summer_set_prop` attaches the script, `summer_save_scene` writes it out. Every
180
+ scene-mutating call takes an explicit `scenePath`, and `summer_set_prop` uses `key`
181
+ (not `property`).
171
182
 
172
183
  ```
173
- # No scene mutation — just create the .tscn at addons/vfx/hit-spark/hit_spark.tscn
174
- # with one root GPUParticles3D node, script attached, and save it.
184
+ summer_write_file(path="res://addons/vfx/hit-spark/hit_spark.gd", content="<section 2>", create_only=true)
185
+
186
+ summer_create_scene(path="res://addons/vfx/hit-spark/hit_spark.tscn", rootName="HitSpark", allow_temporary_scene_mutation=true)
187
+ # The .tscn root must be a GPUParticles3D, since hit_spark.gd extends it.
188
+ # Inspect what summer_create_scene produced and convert the root if it isn't one:
189
+ summer_inspect_node(path="HitSpark")
190
+ summer_replace_node(scenePath="res://addons/vfx/hit-spark/hit_spark.tscn", path=".", type="GPUParticles3D")
191
+ summer_set_prop(scenePath="res://addons/vfx/hit-spark/hit_spark.tscn", path=".", key="script", value="res://addons/vfx/hit-spark/hit_spark.gd")
192
+ summer_save_scene(scenePath="res://addons/vfx/hit-spark/hit_spark.tscn")
193
+ ```
194
+
195
+ ### 4b. Verify
196
+
197
+ `hit_spark.gd` is the whole recipe — if it did not parse, every `spawn_hit_spark`
198
+ call is a no-op.
199
+
200
+ ```
201
+ summer_get_script_errors # hit_spark.gd parsed?
202
+ summer_play
203
+ summer_get_debugger_errors # e.g. a wrong root type on the .tscn shows up here
204
+ summer_stop
175
205
  ```
176
206
 
177
207
  Then from any impact handler:
@@ -284,6 +314,7 @@ emission_boost = 9.0
284
314
  - **Skipping the surface-normal orientation.** Sparks always shoot upward regardless of impact direction. The `spawn_hit_spark` helper handles `look_at` + the −90° rotate-around-X (because `look_at` uses −Z as forward).
285
315
  - **Spawning the spark scene as a child of the impacted object.** If the object moves or frees, the sparks vanish. Spawn under the scene tree root.
286
316
  - **`BLEND_MODE_MIX` instead of `BLEND_MODE_ADD`.** Sparks don't bloom; they look like flat orange triangles.
317
+ - **`BaseMaterial3D.new()`.** `BaseMaterial3D` is abstract — that line is a parse error and takes the whole script down. Instantiate `StandardMaterial3D`; keep using the `BaseMaterial3D.*` enum constants.
287
318
  - **Forgetting to free.** The `spawn_hit_spark` helper creates a `Timer`-equivalent via `get_tree().create_timer` to free after lifetime. Otherwise spent emitters accumulate forever.
288
319
  - **`spark_lifetime > 1.0` for sharp impacts.** Sparks lingering for a full second look like fairy dust. Keep ≤ 0.5 s for snappy hits.
289
320
 
@@ -305,11 +336,17 @@ emission_boost = 9.0
305
336
 
306
337
  ## Fallback (no MCP)
307
338
 
308
- VFX is code, no MCP required:
339
+ Section 4 is fully automatable — `summer_write_file` writes the controller and
340
+ `summer_create_scene` + `summer_set_prop` + `summer_save_scene` build `hit_spark.tscn`.
341
+ Do not hand these steps to the user when the MCP tools are available.
342
+
343
+ Without the MCP connection there is no engine to drive, so the user does it
344
+ manually in Summer Engine:
309
345
 
310
346
  1. Create `addons/vfx/hit-spark/` with the two files above.
311
- 2. In Godot, add a `GPUParticles3D` to a new scene, attach `hit_spark.gd`. Save as `hit_spark.tscn`.
347
+ 2. Add a `GPUParticles3D` to a new scene, attach `hit_spark.gd`. Save as `hit_spark.tscn`.
312
348
  3. From any impact handler: `HitSpark.spawn_hit_spark(get_tree().root, position, normal)`.
349
+ 4. Check the Errors dock before wiring the first impact.
313
350
 
314
351
  ## Handoff
315
352
 
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: visual-effects
7
7
  user-invocable: true
8
- allowed-tools: Read Write Edit summer_add_node summer_set_prop summer_set_resource_property summer_inspect_node summer_save_scene
8
+ allowed-tools: Read Write Edit summer_write_file summer_read_file summer_create_scene summer_add_node summer_set_prop summer_set_resource_property summer_inspect_node summer_save_scene summer_project_setting summer_get_script_errors summer_get_debugger_errors summer_play summer_stop
9
9
  paths: ["**/*.tscn", "**/*.gd", "**/*.gdshader", "addons/vfx/**"]
10
10
  ---
11
11
 
@@ -27,7 +27,9 @@ A lightning bolt is a noisy line from A to B, drawn for ~150 ms with a glow shad
27
27
  - The user wants a continuous beam that holds (e.g., laser sniper holding the trigger) — use the same shader on a `MeshInstance3D` cylinder with continuous emission, not the `ImmediateMesh` one-shot pattern. (See variants for `held-beam`.)
28
28
  - The user wants ambient electricity *on* an object (Frankenstein arcs) — use a custom shader on the mesh with screen-space arcs; this recipe is for two-point bolts.
29
29
  - The user wants a 2D lightning effect on UI — `canvas_item` shader, not this.
30
- - The user wants a tracer line for a bullet — use a simple `Line3D`/`ImmediateMesh` without the noise displacement; skip this recipe.
30
+ - The user wants a tracer line for a bullet — use a simple `ImmediateMesh`
31
+ without noise displacement; skip this recipe. The current Summer build has no
32
+ `Line3D` (`ClassDB.class_exists("Line3D")` is `false`); `Line2D` is 2D-only.
31
33
 
32
34
  ## Recipe
33
35
 
@@ -56,8 +58,7 @@ uniform float flicker_rate : hint_range(0.0, 60.0) = 30.0;
56
58
 
57
59
  void vertex() {
58
60
  // Billboard each segment toward the camera around its own forward axis.
59
- vec3 right = INV_VIEW_MATRIX[0].xyz;
60
- vec3 up = INV_VIEW_MATRIX[1].xyz;
61
+ vec3 up = INV_VIEW_MATRIX[1].xyz;
61
62
  VERTEX += UV.y > 0.5 ? up * thickness * 0.5 : -up * thickness * 0.5;
62
63
  }
63
64
 
@@ -76,8 +77,10 @@ void fragment() {
76
77
  vec3 col = mix(bolt_color.rgb, core_color.rgb, core);
77
78
  float a = (core + halo * 0.4) * intensity * flick;
78
79
 
79
- ALBEDO = col;
80
- EMISSION = col * emission_boost * intensity * flick * (core + halo * 0.5);
80
+ // `render_mode unshaded` outputs ALBEDO and discards EMISSION entirely
81
+ // (scene_forward_clustered.glsl:2962 — `frag_color = vec4(albedo, alpha)`
82
+ // under MODE_UNSHADED), so the bloom boost has to live in ALBEDO.
83
+ ALBEDO = col * emission_boost * flick * (core + halo * 0.5);
81
84
  ALPHA = clamp(a, 0.0, 1.0);
82
85
  }
83
86
  ```
@@ -117,14 +120,14 @@ func cast_lightning(from: Vector3, to: Vector3, intensity_scale: float = 1.0) ->
117
120
  _spawn_sparks(from)
118
121
  _spawn_sparks(to)
119
122
 
120
- if Engine.has_singleton("CameraShake") or _has_autoload("CameraShake"):
121
- var cs := Engine.get_main_loop().root.get_node_or_null("/root/CameraShake")
122
- if cs and cs.has_method("add_trauma"):
123
- cs.add_trauma(trauma_amount * intensity_scale)
124
-
125
- func _has_autoload(name: String) -> bool:
126
- var root := Engine.get_main_loop().root
127
- return root and root.has_node("/root/" + name)
123
+ # An autoload is a node at /root/<Name>, not an engine singleton:
124
+ # Engine.has_singleton("CameraShake") is always false for one (measured).
125
+ # get_node_or_null returns null when nothing is registered — that is the guard.
126
+ # (MainLoop has no `root`, so `var root := Engine.get_main_loop().root` is a
127
+ # parse error that takes the entire script down with it.)
128
+ var cs := get_node_or_null(^"/root/CameraShake")
129
+ if cs and cs.has_method("add_trauma"):
130
+ cs.add_trauma(trauma_amount * intensity_scale)
128
131
 
129
132
  func _build_bolt_node(from: Vector3, to: Vector3, intensity_scale: float) -> Node3D:
130
133
  var holder := Node3D.new()
@@ -193,7 +196,10 @@ func _draw_polyline(mesh: ImmediateMesh, points: PackedVector3Array) -> void:
193
196
  func _spawn_sparks(at: Vector3) -> void:
194
197
  if not ResourceLoader.exists(spark_scene_path):
195
198
  return
196
- var sparks := load(spark_scene_path).instantiate()
199
+ # `var sparks := load(...).instantiate()` is a parse error — load() has no
200
+ # static return type, so `:=` cannot infer. Annotate both steps.
201
+ var packed: PackedScene = load(spark_scene_path)
202
+ var sparks: Node3D = packed.instantiate()
197
203
  add_child(sparks)
198
204
  sparks.global_position = at
199
205
  if sparks.has_method("restart"):
@@ -216,12 +222,28 @@ Recommended: register one `LightningCaster` as an autoload (`/root/Lightning`) s
216
222
 
217
223
  Place one `LightningCaster` per "world" (or autoload it), then call from gameplay code.
218
224
 
225
+ Every scene-mutating call takes an explicit `scenePath`. `summer_set_prop` uses `key`
226
+ (not `property`) and only accepts a string, number, or boolean.
227
+
219
228
  ```
220
- summer_add_node(parent="./World", type="Node3D", name="LightningCaster")
221
- summer_set_prop(path="./World/LightningCaster", property="script", value="res://addons/vfx/lightning/lightning_caster.gd")
222
- summer_set_prop(path="./World/LightningCaster", property="bolt_color", value="Color(0.55, 0.75, 1.0)")
223
- summer_set_prop(path="./World/LightningCaster", property="trauma_amount", value=0.6)
224
- summer_save_scene
229
+ summer_write_file(path="res://addons/vfx/lightning/lightning.gdshader", content="<section 2>", create_only=true)
230
+ summer_write_file(path="res://addons/vfx/lightning/lightning_caster.gd", content="<section 3>", create_only=true)
231
+
232
+ summer_add_node(scenePath="res://main.tscn", parent="./World", type="Node3D", name="LightningCaster")
233
+ summer_set_prop(scenePath="res://main.tscn", path="./World/LightningCaster", key="script", value="res://addons/vfx/lightning/lightning_caster.gd")
234
+ summer_set_prop(scenePath="res://main.tscn", path="./World/LightningCaster", key="bolt_color", value="Color(0.55, 0.75, 1.0)")
235
+ summer_set_prop(scenePath="res://main.tscn", path="./World/LightningCaster", key="trauma_amount", value=0.6)
236
+ summer_save_scene(scenePath="res://main.tscn")
237
+ ```
238
+
239
+ Then verify — the caster `preload`s its own shader, so a shader that failed to write
240
+ takes the whole script down at parse time:
241
+
242
+ ```
243
+ summer_get_script_errors # lightning_caster.gd parsed?
244
+ summer_play
245
+ summer_get_debugger_errors # shader compile errors surface here at runtime
246
+ summer_stop
225
247
  ```
226
248
 
227
249
  Then from the spell code:
@@ -319,18 +341,21 @@ trauma_amount = 0.0 # no shake on a held beam
319
341
 
320
342
  ## Anti-patterns
321
343
 
322
- - **Drawing the bolt as a `Line3D`.** Line3D doesn't billboard or accept the additive shader cleanly. Use `ImmediateMesh` triangle strip with the billboard vertex shader.
344
+ - **Reaching for a `Line3D`.** The current Summer build has no such class
345
+ (`ClassDB.class_exists("Line3D")` is `false`); `Line2D` is 2D-only. Use an
346
+ `ImmediateMesh` triangle strip with the billboard vertex shader.
323
347
  - **Animating the path frame-by-frame.** A bolt is one shape, on screen for 150 ms. You don't see the inside detail moving. One generation per cast.
324
348
  - **Forgetting `blend_add`.** A bolt that doesn't bloom looks like a curved metal stick.
325
- - **No camera shake.** Lightning without screen shake feels weightless. Always call `CameraShake.add_trauma`.
349
+ - **No camera shake.** Lightning without screen shake feels weightless. Always call `add_trauma` on the `/root/CameraShake` autoload.
350
+ - **Gating the shake on `Engine.has_singleton("CameraShake")`.** That is always `false` for an autoload (measured) — the shake would never fire. Autoloads are nodes at `/root/<Name>`; look them up with `get_node_or_null`.
326
351
  - **Spawning lightning every frame for a "continuous" beam.** Use the `plasma-laser` variant pattern instead — one mesh, updated per-frame transform, never re-instantiated.
327
352
  - **`SEGMENTS` too low (< 6).** Bolt looks like a zigzag triangle. Default 18 is right.
328
353
  - **`DISPLACEMENT` proportional to total length without falloff.** Endpoints drift away from `from`/`to`. The included code uses `sin(t * PI)` falloff so endpoints meet cleanly.
329
354
 
330
355
  ## Performance notes
331
356
 
332
- - One bolt: ~36 verts, one draw call, freed after 150 ms. Effectively free.
333
- - Chain lightning with 5 segments: still under 0.05 ms.
357
+ - One bolt at `SEGMENTS = 18`: 19 path points × 2 verts = 38 verts for the trunk, plus 2 × (`SEGMENTS / 3` + 1) = 14 verts per forked branch. One draw call, freed after 150 ms. Effectively free.
358
+ - Chain lightning with 5 segments stays in the same negligible band — that is an order-of-magnitude expectation, not a measurement.
334
359
  - Endpoint sparks (instantiated `hit-spark` scenes) are the bigger cost — 64 particles × 2 = 128 particles per cast. Throttle for storms (see edge cases).
335
360
  - The shader's flicker uses `floor(TIME * flicker_rate)`, so it ages with `TIME` not particle time — flicker is consistent across all bolts in flight.
336
361
 
@@ -341,16 +366,23 @@ trauma_amount = 0.0 # no shake on a held beam
341
366
  - **Bolt origin and target at the same point.** Falloff goes to zero; `_generate_path` returns a degenerate strip. Add a guard: if `(to - from).length() < 0.01`, skip.
342
367
  - **Underwater bolt.** Tint blue-green, drop `emission_boost` to 5.0; underwater bloom is muted.
343
368
  - **First-person caster (player's hand).** The bolt starts inside the player's view and reads as a flash. Use a `held-beam` variant or shorten `LIFETIME` to 0.08.
344
- - **No `CameraShake` autoload registered.** The script's `_has_autoload` guard prevents a crash; the bolt fires without shake. Suggest `_building-blocks/trauma-shake-snippet.md` to wire it.
369
+ - **No `CameraShake` autoload registered.** `get_node_or_null("/root/CameraShake")` returns null and the `if cs` check skips the call; the bolt fires without shake. Suggest `_building-blocks/trauma-shake-snippet.md` to wire it.
345
370
 
346
371
  ## Fallback (no MCP)
347
372
 
348
- VFX is code, no MCP required:
373
+ Section 5 is fully automatable — `summer_write_file` writes the shader and the
374
+ caster, `summer_add_node` + `summer_set_prop` + `summer_save_scene` place it, and
375
+ `summer_project_setting` registers the autoload. Do not hand these steps to the user
376
+ when the MCP tools are available.
377
+
378
+ Without the MCP connection there is no engine to drive, so the user does it
379
+ manually in Summer Engine:
349
380
 
350
381
  1. Create `addons/vfx/lightning/` and write the three files above.
351
- 2. In Godot, add a `Node3D`, attach `lightning_caster.gd`. Optionally autoload as `Lightning`.
382
+ 2. Add a `Node3D`, attach `lightning_caster.gd`. Optionally autoload as `Lightning`.
352
383
  3. From any spell script, call `Lightning.cast_lightning(from_pos, to_pos)`.
353
384
  4. Wire the `CameraShake` autoload from `_building-blocks/trauma-shake-snippet.md` for the punch.
385
+ 5. Check the Errors dock — `lightning_caster.gd` `preload`s the shader, so a broken shader path fails the script too.
354
386
 
355
387
  ## Handoff
356
388