summer-engine 1.5.0 → 2.2.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 (101) hide show
  1. package/{marketplace.json → .claude-plugin/marketplace.json} +3 -3
  2. package/.claude-plugin/plugin.json +52 -11
  3. package/.codex-plugin/plugin.json +69 -8
  4. package/.cursor-plugin/plugin.json +53 -12
  5. package/.factory-plugin/plugin.json +20 -0
  6. package/.opencode/INSTALL.md +75 -0
  7. package/.opencode/plugins/summer.js +9 -6
  8. package/AGENTS.md +51 -0
  9. package/CHANGELOG.md +31 -1
  10. package/CLAUDE.md +5 -0
  11. package/GEMINI.md +56 -0
  12. package/README.md +409 -80
  13. package/_persona/summer/SKILL.md +113 -0
  14. package/dist/bin/summer.js +0 -0
  15. package/dist/commands/assets.d.ts +2 -0
  16. package/dist/commands/assets.js +189 -0
  17. package/dist/commands/create.js +55 -17
  18. package/dist/commands/debug.d.ts +2 -0
  19. package/dist/commands/debug.js +127 -0
  20. package/dist/commands/install.js +2 -2
  21. package/dist/commands/list.js +24 -5
  22. package/dist/commands/project.d.ts +2 -0
  23. package/dist/commands/project.js +170 -0
  24. package/dist/commands/scene.d.ts +2 -0
  25. package/dist/commands/scene.js +290 -0
  26. package/dist/lib/banner.js +2 -2
  27. package/dist/lib/log-filters.d.ts +53 -0
  28. package/dist/lib/log-filters.js +130 -0
  29. package/dist/lib/plugin-manifests.test.d.ts +1 -0
  30. package/dist/lib/plugin-manifests.test.js +134 -0
  31. package/dist/lib/remote-templates.d.ts +25 -0
  32. package/dist/lib/remote-templates.js +86 -0
  33. package/dist/lib/skills-registry.d.ts +283 -3
  34. package/dist/lib/skills-registry.js +522 -2
  35. package/dist/lib/telemetry.d.ts +13 -0
  36. package/dist/lib/telemetry.js +63 -0
  37. package/dist/lib/with-engine.d.ts +14 -0
  38. package/dist/lib/with-engine.js +109 -0
  39. package/dist/mcp/tools/asset-tools.js +12 -13
  40. package/dist/mcp/tools/debug-tools.js +32 -9
  41. package/dist/mcp/tools/generate-tools.js +101 -0
  42. package/dist/mcp/tools/with-engine.js +4 -0
  43. package/gemini-extension.json +2 -2
  44. package/hooks/pre-commit-doctor.sh +0 -0
  45. package/hooks/run-hook.cmd +0 -0
  46. package/hooks/session-start.sh +0 -0
  47. package/package.json +11 -5
  48. package/skills/2d-assets/character-portrait/SKILL.md +196 -0
  49. package/skills/2d-assets/concept-art/SKILL.md +152 -0
  50. package/skills/2d-assets/pixel-art/SKILL.md +180 -0
  51. package/skills/2d-assets/skybox-panorama/SKILL.md +207 -0
  52. package/skills/2d-assets/sprite-sheet/SKILL.md +239 -0
  53. package/skills/2d-assets/tileable-texture/SKILL.md +205 -0
  54. package/skills/2d-assets/ui-graphics/SKILL.md +210 -0
  55. package/skills/3d-assets/character-model/SKILL.md +225 -0
  56. package/skills/3d-assets/environment-kit/SKILL.md +164 -0
  57. package/skills/3d-assets/organic-model/SKILL.md +192 -0
  58. package/skills/3d-assets/prop-model/SKILL.md +133 -0
  59. package/skills/3d-assets/vehicle-model/SKILL.md +202 -0
  60. package/skills/README.md +1 -1
  61. package/skills/_meta/skill-create/SKILL.md +1 -1
  62. package/skills/_meta/skill-improve/SKILL.md +1 -1
  63. package/skills/_meta/skill-test/SKILL.md +1 -1
  64. package/skills/_meta/using-summer/SKILL.md +130 -0
  65. package/skills/ai-and-npcs/design-npc/SKILL.md +3 -1
  66. package/skills/animation/animation-tree/SKILL.md +260 -0
  67. package/skills/animation/facial-and-lipsync/SKILL.md +282 -0
  68. package/skills/animation/generate-motion/SKILL.md +129 -0
  69. package/skills/animation/procedural-animation/SKILL.md +234 -0
  70. package/skills/animation/retarget/SKILL.md +172 -0
  71. package/skills/asset-pipeline/asset-strategy/SKILL.md +102 -212
  72. package/skills/audio/adaptive-music/SKILL.md +326 -0
  73. package/skills/audio/ambient-bed/SKILL.md +278 -0
  74. package/skills/audio/music-track/SKILL.md +268 -0
  75. package/skills/audio/sound-effect/SKILL.md +237 -0
  76. package/skills/audio/voice-line/SKILL.md +253 -0
  77. package/skills/catalog.yaml +266 -5
  78. package/skills/character-controllers/fps-controller/SKILL.md +1 -1
  79. package/skills/debugging/debug/SKILL.md +102 -82
  80. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
  81. package/skills/scene-and-project/browse-templates/SKILL.md +105 -0
  82. package/skills/scene-and-project/make-game/SKILL.md +159 -346
  83. package/skills/scene-and-project/new-project/SKILL.md +106 -0
  84. package/skills/scene-and-project/scene-composition/SKILL.md +1 -1
  85. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  86. package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
  87. package/skills/video/animated-loop/SKILL.md +200 -0
  88. package/skills/video/cinematic-cutscene/SKILL.md +204 -0
  89. package/skills/video/trailer-shot/SKILL.md +203 -0
  90. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +28 -0
  91. package/skills/visual-effects/_building-blocks/noise-3d-fbm.gdshaderinc +45 -0
  92. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +51 -0
  93. package/skills/visual-effects/{vfx → game-feel}/SKILL.md +1 -1
  94. package/skills/visual-effects/recipes/dissolve/SKILL.md +349 -0
  95. package/skills/visual-effects/recipes/fire/SKILL.md +387 -0
  96. package/skills/visual-effects/recipes/hit-spark/SKILL.md +330 -0
  97. package/skills/visual-effects/recipes/lightning/SKILL.md +372 -0
  98. package/skills/visual-effects/recipes/magic-glow/SKILL.md +377 -0
  99. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +326 -0
  100. package/skills/visual-effects/recipes/smoke/SKILL.md +381 -0
  101. package/skills/visual-effects/recipes/water-ripple/SKILL.md +340 -0
@@ -0,0 +1,372 @@
1
+ ---
2
+ name: lightning
3
+ description: Use when authoring a lightning bolt visual effect — procedural jagged path drawn via ImmediateMesh, glow shader, sparks at endpoints, screen shake. Trigger on "lightning bolt", "chain lightning", "electric attack", "tesla coil", "shock spell", "thunderbolt", "energy beam".
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: visual-effects
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
9
+ paths: ["**/*.tscn", "**/*.gd", "**/*.gdshader", "addons/vfx/**"]
10
+ ---
11
+
12
+ # lightning — Procedural Jagged Bolt
13
+
14
+ A lightning bolt is a noisy line from A to B, drawn for ~150 ms with a glow shader, sparks at both endpoints, and camera shake on cast. The recipe builds the jagged path in GDScript (midpoint displacement on a polyline), feeds the verts into an `ImmediateMesh`, applies a `ShaderMaterial` for additive bloom + flicker, spawns `hit-spark` particles at each endpoint, and calls `CameraShake.add_trauma(0.6)` for the impact. Used for shock spells, tesla coils, weapon discharges, electric enemies.
15
+
16
+ ## When to use
17
+
18
+ - "Cast a lightning bolt from the wizard's hand to the enemy."
19
+ - "Chain lightning from one enemy to the next." (call `cast_lightning` per segment)
20
+ - "Tesla coil firing at the player every 2 seconds."
21
+ - "Energy weapon laser." (use the `laser-beam` variant)
22
+ - "Lightning strike from the sky."
23
+ - The user wants a *short, dramatic* energy connection between two points.
24
+
25
+ ## When NOT to use
26
+
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
+ - 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
+ - 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.
31
+
32
+ ## Recipe
33
+
34
+ ### 1. Files to create
35
+
36
+ ```
37
+ addons/vfx/lightning/lightning.gdshader # spatial shader for the bolt mesh
38
+ addons/vfx/lightning/lightning_caster.gd # build path, draw, animate, spawn endpoints, shake
39
+ addons/vfx/lightning/lightning.tscn # reusable scene
40
+ ```
41
+
42
+ ### 2. Shader code
43
+
44
+ `addons/vfx/lightning/lightning.gdshader`:
45
+
46
+ ```glsl
47
+ shader_type spatial;
48
+ render_mode unshaded, blend_add, depth_draw_never, cull_disabled, shadows_disabled;
49
+
50
+ uniform vec4 bolt_color : source_color = vec4(0.55, 0.75, 1.00, 1.0);
51
+ uniform vec4 core_color : source_color = vec4(1.00, 1.00, 1.00, 1.0);
52
+ uniform float intensity : hint_range(0.0, 1.0) = 1.0; // driven by the controller
53
+ uniform float emission_boost: hint_range(0.0, 16.0) = 8.0;
54
+ uniform float thickness : hint_range(0.05, 1.0) = 0.30; // visual halo width
55
+ uniform float flicker_rate : hint_range(0.0, 60.0) = 30.0;
56
+
57
+ void vertex() {
58
+ // 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
+ VERTEX += UV.y > 0.5 ? up * thickness * 0.5 : -up * thickness * 0.5;
62
+ }
63
+
64
+ void fragment() {
65
+ // V across the line gives 0 (top) → 1 (bottom); recenter to ±1.
66
+ float v = (UV.y - 0.5) * 2.0;
67
+ float core = smoothstep(1.0, 0.0, abs(v));
68
+ core = pow(core, 3.0);
69
+ float halo = smoothstep(1.0, 0.0, abs(v));
70
+ halo = pow(halo, 0.7);
71
+
72
+ // Per-bolt flicker (random per fragment time slice).
73
+ float t = floor(TIME * flicker_rate) / flicker_rate;
74
+ float flick = 0.85 + 0.30 * fract(sin(t * 91.7) * 43758.5);
75
+
76
+ vec3 col = mix(bolt_color.rgb, core_color.rgb, core);
77
+ float a = (core + halo * 0.4) * intensity * flick;
78
+
79
+ ALBEDO = col;
80
+ EMISSION = col * emission_boost * intensity * flick * (core + halo * 0.5);
81
+ ALPHA = clamp(a, 0.0, 1.0);
82
+ }
83
+ ```
84
+
85
+ ### 3. GDScript controller
86
+
87
+ `addons/vfx/lightning/lightning_caster.gd`:
88
+
89
+ ```gdscript
90
+ @tool
91
+ class_name LightningCaster
92
+ extends Node3D
93
+
94
+ ## Static-style helper. Add one anywhere in the scene; call cast_lightning(from, to) anytime.
95
+
96
+ const LIFETIME := 0.15
97
+ const SEGMENTS := 18 ## subdivisions of the bolt
98
+ const DISPLACEMENT := 0.35 ## meters of jaggedness per segment
99
+ const BRANCH_CHANCE := 0.25 ## probability of a forked sub-bolt per segment
100
+ const BRANCH_LENGTH := 0.8 ## relative length of a forked branch
101
+
102
+ @export var bolt_color: Color = Color(0.55, 0.75, 1.00)
103
+ @export var core_color: Color = Color(1.00, 1.00, 1.00)
104
+ @export var emission_boost: float = 8.0
105
+ @export var thickness: float = 0.30
106
+ @export var trauma_amount: float = 0.6
107
+ @export var spawn_endpoint_sparks: bool = true
108
+ @export_file("*.tscn") var spark_scene_path: String = "res://addons/vfx/hit-spark/hit_spark.tscn"
109
+
110
+ ## Public API. Call this to fire a bolt.
111
+ func cast_lightning(from: Vector3, to: Vector3, intensity_scale: float = 1.0) -> void:
112
+ var bolt := _build_bolt_node(from, to, intensity_scale)
113
+ add_child(bolt)
114
+ bolt.global_position = Vector3.ZERO
115
+
116
+ if spawn_endpoint_sparks:
117
+ _spawn_sparks(from)
118
+ _spawn_sparks(to)
119
+
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)
128
+
129
+ func _build_bolt_node(from: Vector3, to: Vector3, intensity_scale: float) -> Node3D:
130
+ var holder := Node3D.new()
131
+ var im := MeshInstance3D.new()
132
+ var mesh := ImmediateMesh.new()
133
+ im.mesh = mesh
134
+ var mat := ShaderMaterial.new()
135
+ mat.shader = preload("res://addons/vfx/lightning/lightning.gdshader")
136
+ mat.set_shader_parameter("bolt_color", bolt_color)
137
+ mat.set_shader_parameter("core_color", core_color)
138
+ mat.set_shader_parameter("emission_boost", emission_boost)
139
+ mat.set_shader_parameter("thickness", thickness)
140
+ mat.set_shader_parameter("intensity", intensity_scale)
141
+ im.material_override = mat
142
+ holder.add_child(im)
143
+
144
+ var path := _generate_path(from, to, SEGMENTS, DISPLACEMENT)
145
+ _draw_polyline(mesh, path)
146
+
147
+ # Optional forked branches.
148
+ for i in range(1, path.size() - 1):
149
+ if randf() < BRANCH_CHANCE:
150
+ var dir := (path[i] - path[i - 1]).normalized()
151
+ var perp := dir.cross(Vector3.UP).normalized()
152
+ if perp.length_squared() < 0.01:
153
+ perp = dir.cross(Vector3.RIGHT).normalized()
154
+ var branch_end: Vector3 = path[i] + (dir + perp * randf_range(-1.5, 1.5)) * (to - from).length() * BRANCH_LENGTH * 0.25
155
+ var branch_path := _generate_path(path[i], branch_end, SEGMENTS / 3, DISPLACEMENT * 0.6)
156
+ _draw_polyline(mesh, branch_path)
157
+
158
+ var t := holder.create_tween()
159
+ t.tween_method(func(v: float) -> void:
160
+ mat.set_shader_parameter("intensity", v * intensity_scale),
161
+ 1.0, 0.0, LIFETIME).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN)
162
+ t.tween_callback(holder.queue_free)
163
+ return holder
164
+
165
+ func _generate_path(a: Vector3, b: Vector3, n: int, displacement: float) -> PackedVector3Array:
166
+ var path: PackedVector3Array = []
167
+ var dir := (b - a).normalized()
168
+ var perp1 := dir.cross(Vector3.UP).normalized()
169
+ if perp1.length_squared() < 0.01:
170
+ perp1 = dir.cross(Vector3.RIGHT).normalized()
171
+ var perp2 := dir.cross(perp1).normalized()
172
+ for i in n + 1:
173
+ var t := float(i) / float(n)
174
+ var p := a.lerp(b, t)
175
+ # Falloff at endpoints so they meet cleanly.
176
+ var falloff := sin(t * PI)
177
+ var jitter := perp1 * randf_range(-1.0, 1.0) + perp2 * randf_range(-1.0, 1.0)
178
+ p += jitter * displacement * falloff
179
+ path.append(p)
180
+ return path
181
+
182
+ func _draw_polyline(mesh: ImmediateMesh, points: PackedVector3Array) -> void:
183
+ if points.size() < 2: return
184
+ mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
185
+ for i in points.size():
186
+ var v: float = float(i) / float(points.size() - 1)
187
+ mesh.surface_set_uv(Vector2(v, 0.0))
188
+ mesh.surface_add_vertex(points[i])
189
+ mesh.surface_set_uv(Vector2(v, 1.0))
190
+ mesh.surface_add_vertex(points[i])
191
+ mesh.surface_end()
192
+
193
+ func _spawn_sparks(at: Vector3) -> void:
194
+ if not ResourceLoader.exists(spark_scene_path):
195
+ return
196
+ var sparks := load(spark_scene_path).instantiate()
197
+ add_child(sparks)
198
+ sparks.global_position = at
199
+ if sparks.has_method("restart"):
200
+ sparks.restart()
201
+ var t := sparks.create_tween()
202
+ t.tween_interval(0.5)
203
+ t.tween_callback(sparks.queue_free)
204
+ ```
205
+
206
+ ### 4. Node tree
207
+
208
+ ```
209
+ Node3D ("LightningCaster") [script: lightning_caster.gd, autoload-friendly]
210
+ └── (children created at runtime per cast: ImmediateMesh + sparks scenes)
211
+ ```
212
+
213
+ Recommended: register one `LightningCaster` as an autoload (`/root/Lightning`) so any system can call `Lightning.cast_lightning(from, to)` from anywhere.
214
+
215
+ ### 5. Wire it in (MCP calls)
216
+
217
+ Place one `LightningCaster` per "world" (or autoload it), then call from gameplay code.
218
+
219
+ ```
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
225
+ ```
226
+
227
+ Then from the spell code:
228
+
229
+ ```gdscript
230
+ var origin: Vector3 = $Wizard/HandTip.global_position
231
+ var target: Vector3 = enemy.global_position
232
+ $World/LightningCaster.cast_lightning(origin, target)
233
+ enemy.take_damage(35)
234
+ ```
235
+
236
+ For chain lightning:
237
+
238
+ ```gdscript
239
+ var prev: Vector3 = origin
240
+ for enemy in nearest_enemies(prev, 4):
241
+ $World/LightningCaster.cast_lightning(prev, enemy.global_position, 0.85)
242
+ enemy.take_damage(20)
243
+ prev = enemy.global_position
244
+ await get_tree().create_timer(0.05).timeout
245
+ ```
246
+
247
+ ### 6. Parameters to tune
248
+
249
+ | Parameter | Range | Effect |
250
+ |---|---|---|
251
+ | `LIFETIME` (const) | 0.05–0.40 s | how long the bolt is on screen (0.15 is the sweet spot) |
252
+ | `SEGMENTS` (const) | 6–32 | path subdivisions; more = smoother jaggedness |
253
+ | `DISPLACEMENT` (const) | 0.05–1.5 m | how wild the jagged offset is per segment |
254
+ | `BRANCH_CHANCE` (const) | 0.0–0.6 | probability of forks per segment |
255
+ | `thickness` | 0.05–1.0 m | halo width of the bolt |
256
+ | `emission_boost` | 0.0–16.0 | bloom strength (needs Bloom in WorldEnvironment) |
257
+ | `bolt_color` / `core_color` | Color | recolor for fire-bolt, plasma, magic |
258
+ | `trauma_amount` | 0.0–1.0 | how hard the camera shakes |
259
+
260
+ ## Cookbook — named variants
261
+
262
+ ### thunderbolt (default)
263
+
264
+ Cool blue-white, dramatic shake, sky-to-ground.
265
+
266
+ ```
267
+ bolt_color = Color(0.55, 0.75, 1.00)
268
+ core_color = Color(1.0, 1.0, 1.0)
269
+ emission_boost = 8.0
270
+ thickness = 0.30
271
+ trauma_amount = 0.6
272
+ LIFETIME = 0.18
273
+ DISPLACEMENT = 0.45
274
+ ```
275
+
276
+ ### chain-lightning-spell
277
+
278
+ Tighter, chains between targets.
279
+
280
+ ```
281
+ bolt_color = Color(0.65, 0.85, 1.0)
282
+ emission_boost = 6.0
283
+ thickness = 0.18
284
+ trauma_amount = 0.25
285
+ LIFETIME = 0.12
286
+ DISPLACEMENT = 0.20
287
+ SEGMENTS = 14
288
+ BRANCH_CHANCE = 0.15
289
+ ```
290
+
291
+ ### tesla-arc
292
+
293
+ Short, fast, lots of forks.
294
+
295
+ ```
296
+ bolt_color = Color(0.75, 0.95, 1.0)
297
+ emission_boost = 10.0
298
+ thickness = 0.10
299
+ LIFETIME = 0.06
300
+ DISPLACEMENT = 0.15
301
+ SEGMENTS = 24
302
+ BRANCH_CHANCE = 0.45
303
+ trauma_amount = 0.10
304
+ ```
305
+
306
+ ### plasma-laser (held beam variant)
307
+
308
+ Override the script to NOT free after `LIFETIME` — use a `MeshInstance3D` with a stretched cylinder mesh updated each frame between two transforms; same shader.
309
+
310
+ ```
311
+ bolt_color = Color(1.0, 0.40, 0.55)
312
+ core_color = Color(1.0, 0.85, 0.95)
313
+ emission_boost = 12.0
314
+ thickness = 0.20
315
+ DISPLACEMENT = 0.05 # nearly straight
316
+ SEGMENTS = 8
317
+ trauma_amount = 0.0 # no shake on a held beam
318
+ ```
319
+
320
+ ## Anti-patterns
321
+
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.
323
+ - **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
+ - **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`.
326
+ - **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
+ - **`SEGMENTS` too low (< 6).** Bolt looks like a zigzag triangle. Default 18 is right.
328
+ - **`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
+
330
+ ## Performance notes
331
+
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.
334
+ - Endpoint sparks (instantiated `hit-spark` scenes) are the bigger cost — 64 particles × 2 = 128 particles per cast. Throttle for storms (see edge cases).
335
+ - 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
+
337
+ ## Edge cases
338
+
339
+ - **Lightning storm (50 strikes/sec).** Disable endpoint sparks for ambient strikes. Pool the `LightningCaster` mesh nodes instead of allocating per cast.
340
+ - **Bolt passes through a wall.** Visual only — the bolt shader doesn't depth test. If the user wants the bolt to be occluded by walls, set `depth_draw_opaque` and let the depth buffer cull it (loses the additive bloom in front of geometry).
341
+ - **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
+ - **Underwater bolt.** Tint blue-green, drop `emission_boost` to 5.0; underwater bloom is muted.
343
+ - **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.
345
+
346
+ ## Fallback (no MCP)
347
+
348
+ VFX is code, no MCP required:
349
+
350
+ 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`.
352
+ 3. From any spell script, call `Lightning.cast_lightning(from_pos, to_pos)`.
353
+ 4. Wire the `CameraShake` autoload from `_building-blocks/trauma-shake-snippet.md` for the punch.
354
+
355
+ ## Handoff
356
+
357
+ After firing this recipe, suggest:
358
+
359
+ - `summer:visual-effects/recipes/hit-spark` — automatically called at endpoints; tune the spark color to match `bolt_color`.
360
+ - `summer:visual-effects/recipes/magic-glow` — for the wizard's hand glow during charge-up before the cast.
361
+ - `summer:visual-effects/recipes/muzzle-flash` — for the brief bright flash at the casting hand on release frame.
362
+ - `summer:visual-effects/game-feel` — `CameraShake.add_trauma` is already called; pair with hit-stop on the target for impact emphasis.
363
+ - `summer:audio/sound-effect` — generate `electric crack thunder, sharp impact, rumble tail, 800ms` and play on cast.
364
+
365
+ ## See also
366
+
367
+ - `_building-blocks/trauma-shake-snippet.md` — `CameraShake` autoload (REQUIRED for the shake call)
368
+ - `_building-blocks/additive-billboard-particles.md` — for the endpoint sparks
369
+ - `summer:visual-effects/recipes/hit-spark` — endpoint companion
370
+ - `summer:visual-effects/recipes/magic-glow` — pre-cast charge-up
371
+ - `summer:visual-effects/recipes/muzzle-flash` — for the cast-release flash
372
+ - `summer:visual-effects/game-feel` — full game-feel pairing