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,340 @@
1
+ ---
2
+ name: water-ripple
3
+ description: Use when authoring a water-ripple visual effect — animated normal-distortion ripples on a water plane (or as a Decal) triggered by impacts. Trigger on "water ripple", "ripples on water", "raindrop ripple", "splash ripple", "rain on water", "footstep in puddle", "fish jumps".
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
+ # water-ripple — Impact Ripples on a Water Plane
13
+
14
+ Concentric expanding rings on a water surface, normal-distorted so the ripple actually refracts the reflection. The shader keeps a small ring buffer of (origin, time, amplitude) tuples and renders all of them simultaneously over a base water plane. GDScript exposes `add_ripple(world_pos, amplitude)` so any system (rain, footsteps, projectile impacts, fish) can spawn one with a single call. Tested in BotW-style stylized water and in flat-puddle FPS scenes.
15
+
16
+ ## When to use
17
+
18
+ - "Add ripples when the player walks through the puddle."
19
+ - "Rain hitting the lake should make ripples."
20
+ - "When the bullet hits the water, ripple."
21
+ - "Fish jumping leaves ripples."
22
+ - "Footsteps on the wet street should ripple."
23
+ - A water plane already exists and looks too static.
24
+
25
+ ## When NOT to use
26
+
27
+ - The user wants the *whole* water surface to wave (Gerstner / FFT ocean) — that's a separate `ocean-water` recipe; this is impact ripples on top.
28
+ - The user wants a 2D top-down ripple effect on a UI map — use `canvas_item` shader, not this `spatial` one.
29
+ - The user wants splash particles flying upward on impact — pair this with `hit-spark` (recolor blue) for the droplets; this only handles the ring on the surface.
30
+ - The user wants the ripple to physically displace floating objects — this is visual only. Use `Area3D` impulses for the simulation side.
31
+
32
+ ## Recipe
33
+
34
+ ### 1. Files to create
35
+
36
+ ```
37
+ addons/vfx/water-ripple/water_ripple.gdshader # spatial shader on the water plane
38
+ addons/vfx/water-ripple/water_ripple_surface.gd # MeshInstance3D controller w/ add_ripple()
39
+ addons/vfx/water-ripple/water_ripple.tscn # reusable plane
40
+ ```
41
+
42
+ ### 2. Shader code
43
+
44
+ `addons/vfx/water-ripple/water_ripple.gdshader`:
45
+
46
+ ```glsl
47
+ shader_type spatial;
48
+ render_mode blend_mix, depth_draw_opaque, cull_disabled;
49
+
50
+ const int MAX_RIPPLES = 16;
51
+
52
+ uniform vec4 water_color : source_color = vec4(0.10, 0.30, 0.45, 0.85);
53
+ uniform vec4 ripple_color : source_color = vec4(0.85, 0.95, 1.00, 1.0);
54
+ uniform float water_metallic : hint_range(0.0, 1.0) = 0.4;
55
+ uniform float water_roughness: hint_range(0.0, 1.0) = 0.15;
56
+ uniform float fresnel_power : hint_range(0.5, 8.0) = 4.0;
57
+ uniform float scroll_speed : hint_range(0.0, 0.5) = 0.05;
58
+ uniform sampler2D normal_map : hint_normal;
59
+
60
+ // Per-ripple data: (origin.xz, start_time, amplitude * (1 if active else 0))
61
+ uniform vec4 ripples[MAX_RIPPLES];
62
+
63
+ uniform float ripple_speed : hint_range(0.5, 8.0) = 2.5; // m/s expansion
64
+ uniform float ripple_wavelength : hint_range(0.05, 1.0) = 0.20; // distance between crests
65
+ uniform float ripple_lifetime : hint_range(0.5, 6.0) = 2.5; // seconds visible
66
+ uniform float ripple_height : hint_range(0.0, 0.4) = 0.08; // visual normal kick
67
+
68
+ void vertex() {
69
+ // Sample ripples in vertex to displace Y for proper silhouette.
70
+ vec3 wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
71
+ float disp = 0.0;
72
+ for (int i = 0; i < MAX_RIPPLES; i++) {
73
+ vec4 r = ripples[i];
74
+ if (r.w <= 0.0) continue;
75
+ float age = TIME - r.z;
76
+ if (age < 0.0 || age > ripple_lifetime) continue;
77
+ float dist = length(wpos.xz - r.xy);
78
+ float front = age * ripple_speed;
79
+ float ring = exp(-pow((dist - front) / 0.25, 2.0));
80
+ float fade = 1.0 - smoothstep(0.0, ripple_lifetime, age);
81
+ disp += sin((dist - front) / ripple_wavelength * 6.2831) * ring * fade * r.w;
82
+ }
83
+ VERTEX.y += disp * ripple_height;
84
+ }
85
+
86
+ void fragment() {
87
+ // Base water normal scroll.
88
+ vec2 nuv = UV + vec2(TIME * scroll_speed, TIME * scroll_speed * 0.7);
89
+ vec3 base_n = texture(normal_map, nuv).rgb * 2.0 - 1.0;
90
+
91
+ // Composite ripple normals on top.
92
+ vec3 wpos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
93
+ vec3 ripple_n = vec3(0.0, 0.0, 1.0);
94
+ float ripple_glow = 0.0;
95
+ for (int i = 0; i < MAX_RIPPLES; i++) {
96
+ vec4 r = ripples[i];
97
+ if (r.w <= 0.0) continue;
98
+ float age = TIME - r.z;
99
+ if (age < 0.0 || age > ripple_lifetime) continue;
100
+ vec2 d = wpos.xz - r.xy;
101
+ float dist = length(d);
102
+ float front = age * ripple_speed;
103
+ float ring = exp(-pow((dist - front) / 0.25, 2.0));
104
+ float fade = 1.0 - smoothstep(0.0, ripple_lifetime, age);
105
+ vec2 dir = (dist > 0.0001) ? (d / dist) : vec2(0.0);
106
+ float wave = cos((dist - front) / ripple_wavelength * 6.2831);
107
+ ripple_n.xy += dir * wave * ring * fade * r.w * 1.5;
108
+ ripple_glow += ring * fade * r.w * 0.5;
109
+ }
110
+
111
+ NORMAL_MAP = normalize(base_n + vec3(ripple_n.xy, 0.0));
112
+ NORMAL_MAP_DEPTH = 0.6;
113
+
114
+ // Fresnel-tinted color.
115
+ float fres = pow(1.0 - clamp(dot(NORMAL, VIEW), 0.0, 1.0), fresnel_power);
116
+ ALBEDO = mix(water_color.rgb, ripple_color.rgb, ripple_glow);
117
+ METALLIC = water_metallic;
118
+ ROUGHNESS = water_roughness;
119
+ ALPHA = mix(water_color.a, 1.0, fres);
120
+ EMISSION = ripple_color.rgb * ripple_glow * 0.4;
121
+ }
122
+ ```
123
+
124
+ ### 3. GDScript controller
125
+
126
+ `addons/vfx/water-ripple/water_ripple_surface.gd`:
127
+
128
+ ```gdscript
129
+ @tool
130
+ class_name WaterRippleSurface
131
+ extends MeshInstance3D
132
+
133
+ const MAX_RIPPLES := 16
134
+
135
+ @export var water_color: Color = Color(0.10, 0.30, 0.45, 0.85)
136
+ @export var ripple_color: Color = Color(0.85, 0.95, 1.00)
137
+ @export_range(0.5, 8.0) var ripple_speed: float = 2.5
138
+ @export_range(0.5, 6.0) var ripple_lifetime: float = 2.5
139
+ @export_range(0.0, 0.4) var ripple_height: float = 0.08
140
+ @export var normal_texture: Texture2D
141
+
142
+ var _ripples: Array[Vector4] = []
143
+ var _next_slot: int = 0
144
+ var _mat: ShaderMaterial
145
+
146
+ func _ready() -> void:
147
+ _ripples.resize(MAX_RIPPLES)
148
+ for i in MAX_RIPPLES:
149
+ _ripples[i] = Vector4.ZERO
150
+ if material_override == null or not (material_override is ShaderMaterial):
151
+ var sm := ShaderMaterial.new()
152
+ sm.shader = preload("res://addons/vfx/water-ripple/water_ripple.gdshader")
153
+ material_override = sm
154
+ _mat = material_override as ShaderMaterial
155
+ _apply()
156
+ set_process(true)
157
+
158
+ func _apply() -> void:
159
+ if _mat == null: return
160
+ _mat.set_shader_parameter("water_color", water_color)
161
+ _mat.set_shader_parameter("ripple_color", ripple_color)
162
+ _mat.set_shader_parameter("ripple_speed", ripple_speed)
163
+ _mat.set_shader_parameter("ripple_lifetime", ripple_lifetime)
164
+ _mat.set_shader_parameter("ripple_height", ripple_height)
165
+ if normal_texture:
166
+ _mat.set_shader_parameter("normal_map", normal_texture)
167
+
168
+ ## Spawn a ripple at a world position.
169
+ ## amplitude: 0..1 (raindrop ~0.2, footstep ~0.5, body splash ~1.0)
170
+ func add_ripple(world_pos: Vector3, amplitude: float = 0.5) -> void:
171
+ var local := global_transform.affine_inverse() * world_pos
172
+ var t := float(Time.get_ticks_msec()) / 1000.0 - _shader_time_origin()
173
+ _ripples[_next_slot] = Vector4(world_pos.x, world_pos.z, t, clamp(amplitude, 0.0, 1.0))
174
+ _next_slot = (_next_slot + 1) % MAX_RIPPLES
175
+ _push()
176
+
177
+ func _push() -> void:
178
+ if _mat == null: return
179
+ _mat.set_shader_parameter("ripples", _ripples)
180
+
181
+ func _shader_time_origin() -> float:
182
+ # Shader's TIME starts at scene load; align our timestamps to it.
183
+ return 0.0
184
+
185
+ func _process(_delta: float) -> void:
186
+ # Re-push so editor updates pick up changes; cheap.
187
+ if Engine.is_editor_hint():
188
+ _push()
189
+ ```
190
+
191
+ ### 4. Node tree
192
+
193
+ ```
194
+ MeshInstance3D ("Water") [script: water_ripple_surface.gd]
195
+ └── mesh: PlaneMesh (size matches the puddle / lake)
196
+ └── material_override: ShaderMaterial (shader: water_ripple.gdshader, normal_map: <noise>)
197
+ ```
198
+
199
+ ### 5. Wire it in (MCP calls)
200
+
201
+ ```
202
+ summer_add_node(parent="./World", type="MeshInstance3D", name="Puddle")
203
+ summer_set_prop(path="./World/Puddle", property="script", value="res://addons/vfx/water-ripple/water_ripple_surface.gd")
204
+ summer_set_prop(path="./World/Puddle", property="position", value="Vector3(0, 0.01, 0)")
205
+ summer_set_resource_property(nodePath="./World/Puddle", resourceProperty="mesh", value="res://addons/vfx/water-ripple/plane.tres")
206
+ summer_set_resource_property(nodePath="./World/Puddle", resourceProperty="normal_texture", value="res://addons/vfx/water-ripple/water_normals.png")
207
+ summer_save_scene
208
+ ```
209
+
210
+ Then trigger ripples from any system. Footstep:
211
+
212
+ ```gdscript
213
+ # In your character controller's footstep callback:
214
+ if puddle and puddle.is_in_water(global_position):
215
+ puddle.add_ripple(global_position, 0.5)
216
+ ```
217
+
218
+ Rain:
219
+
220
+ ```gdscript
221
+ # In a Rain node spawning N drops/sec on the water plane bounds:
222
+ puddle.add_ripple(rand_xz_inside_puddle(), 0.20)
223
+ ```
224
+
225
+ Bullet impact:
226
+
227
+ ```gdscript
228
+ # In the bullet's hit handler when hit.collider == puddle:
229
+ puddle.add_ripple(hit.position, 0.8)
230
+ ```
231
+
232
+ ### 6. Parameters to tune
233
+
234
+ | Parameter | Range | Effect |
235
+ |---|---|---|
236
+ | `ripple_speed` | 0.5–8.0 m/s | how fast the ring expands |
237
+ | `ripple_lifetime` | 0.5–6.0 s | how long before it fades |
238
+ | `ripple_height` | 0.0–0.4 | vertex displacement amplitude (silhouette wobble) |
239
+ | `ripple_wavelength` | 0.05–1.0 m | distance between crests within one ring |
240
+ | `water_color` | Color | base tint, alpha controls transparency |
241
+ | `ripple_color` | Color | crest tint, additive glow at the ring front |
242
+ | `fresnel_power` | 0.5–8.0 | how grazing the angle has to be before the surface goes opaque |
243
+ | `scroll_speed` | 0.0–0.5 | base normal map scroll speed (subtle shimmer) |
244
+
245
+ ## Cookbook — named variants
246
+
247
+ ### puddle-shallow (default)
248
+
249
+ Tight, fast ripples, high contrast.
250
+
251
+ ```
252
+ ripple_speed = 3.0
253
+ ripple_lifetime = 1.6
254
+ ripple_height = 0.04
255
+ ripple_wavelength= 0.12
256
+ water_color = Color(0.05, 0.07, 0.10, 0.75)
257
+ ```
258
+
259
+ ### lake-calm
260
+
261
+ Slow, wide, long-lived ripples.
262
+
263
+ ```
264
+ ripple_speed = 1.5
265
+ ripple_lifetime = 4.0
266
+ ripple_height = 0.10
267
+ ripple_wavelength= 0.30
268
+ water_color = Color(0.10, 0.30, 0.45, 0.85)
269
+ ```
270
+
271
+ ### lava-glow (recolor for hot liquid)
272
+
273
+ ```
274
+ water_color = Color(0.12, 0.02, 0.0, 1.0)
275
+ ripple_color = Color(1.0, 0.55, 0.10)
276
+ ripple_lifetime = 1.2
277
+ ripple_height = 0.02
278
+ emission via shader: bump fres mix toward ripple_color
279
+ ```
280
+
281
+ ### blood-pool
282
+
283
+ Sticky, slow, dark.
284
+
285
+ ```
286
+ water_color = Color(0.18, 0.02, 0.02, 0.95)
287
+ ripple_color = Color(0.45, 0.05, 0.05)
288
+ ripple_speed = 1.0
289
+ ripple_lifetime = 3.0
290
+ ripple_wavelength= 0.40
291
+ ```
292
+
293
+ ## Anti-patterns
294
+
295
+ - **Spawning more than 16 ripples per surface.** The shader has a fixed array. Older ripples get overwritten — fine for rain (they were fading anyway), bad if you spawn 50 in one frame. Either raise `MAX_RIPPLES` (and the GLSL `MAX_RIPPLES`) together, or throttle spawns.
296
+ - **Ripple position in local instead of world space.** The shader expects world XZ. Always convert in the GDScript before pushing.
297
+ - **No normal map assigned.** The surface looks like flat-shaded plastic. Always assign a tileable water normals texture (any blue noise normals will do).
298
+ - **PlaneMesh too low-res.** The vertex displacement needs ~2–4 verts per meter or the silhouette wobble looks blocky. Use `subdivide_width = 32, subdivide_depth = 32` minimum on a 10×10 plane.
299
+ - **`depth_draw_disabled` on the water material.** You'll see the bottom of the puddle through the water at every angle. Use `depth_draw_opaque`.
300
+ - **Triggering ripples in `_process` without spacing.** Spamming 60/sec floods the buffer. Throttle to ~5 ripples/sec for rain unless tuned.
301
+
302
+ ## Performance notes
303
+
304
+ - Cost is O(MAX_RIPPLES × pixels_drawn × 2). With 16 ripples on a 5×5 m puddle visible at 1080p: ~0.15 ms. Cheap.
305
+ - The vertex displacement loop runs per vertex; raising plane subdivision past 64 starts to hurt. 32×32 is plenty.
306
+ - For lakes (large planes), don't apply this shader to the entire lake — split the lake into chunks and only apply ripples to the chunk near impacts. Or use a `Decal` projection instead (see edge cases).
307
+ - Mobile: drop `MAX_RIPPLES` to 8, disable vertex displacement (set `ripple_height = 0` and skip the vertex loop), and rely on normal-only ripples.
308
+
309
+ ## Edge cases
310
+
311
+ - **Ripple on a non-flat water surface (Gerstner ocean).** This shader assumes the water plane is flat. For wavy water, sample the wave height in the same shader and add the ripple displacement on top.
312
+ - **Want to use a Decal instead of overriding the water material.** Use `Decal` projecting straight down with a 256×256 ring texture animated by `albedo_mix`. Cheaper but doesn't refract reflections — only colors the surface.
313
+ - **Ripple needs to interact with floating debris.** Visual ripples don't push physics. Spawn an `Area3D` impulse alongside if you need physical effect.
314
+ - **Camera under water (POV swimming).** This shader is one-sided in concept (ring expanding outward from above). For under-water POV, render a separate shader.
315
+ - **Ripple at the edge of the plane.** It clips at the plane boundary because the plane has no geometry past it. Expand the plane 2 m beyond the gameplay area.
316
+
317
+ ## Fallback (no MCP)
318
+
319
+ VFX is code, no MCP required:
320
+
321
+ 1. Create `addons/vfx/water-ripple/` and write the three files above.
322
+ 2. In Godot, add a `MeshInstance3D` with a high-subdivision `PlaneMesh`.
323
+ 3. Attach `water_ripple_surface.gd` and assign a tileable water normals texture.
324
+ 4. From your trigger code, call `$Puddle.add_ripple(impact_pos, amplitude)`.
325
+
326
+ ## Handoff
327
+
328
+ After firing this recipe, suggest:
329
+
330
+ - `summer:visual-effects/recipes/hit-spark` — recolor white-blue and spawn at the impact point for a few water droplets flying up. Pair them.
331
+ - `summer:visual-effects/recipes/smoke` — recolor pale gray for the steam if you're hitting hot water or lava.
332
+ - `summer:audio/sound-effect` — generate `single water drop in shallow puddle, splash, 200ms` and trigger on each `add_ripple`.
333
+ - For weather-driven rain ripples, see `summer:visual-effects/weather` (if available) or scaffold a simple Rain node that calls `add_ripple` 30×/sec across the puddle bounds.
334
+
335
+ ## See also
336
+
337
+ - `_building-blocks/additive-billboard-particles.md` — for paired splash droplets
338
+ - `summer:visual-effects/recipes/hit-spark` — sister recipe for the upward droplets
339
+ - `summer:visual-effects/recipes/smoke` — for steam over hot liquids
340
+ - `summer:gdscript-patterns` — for the controller idioms