summer-engine 2.6.6 → 2.8.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/plugin.json +6 -2
- package/.cursor-plugin/plugin.json +5 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +2 -2
- package/AGENTS.md +11 -7
- package/CHANGELOG.md +40 -0
- package/GEMINI.md +9 -7
- package/README.md +63 -21
- package/_persona/summer/SKILL.md +1 -0
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +12 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +123 -36
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +15 -4
- package/dist/lib/api-client.js +138 -13
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +33 -1
- package/dist/lib/skills-registry.js +55 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +10 -3
- package/dist/mcp/server.js +104 -11
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +159 -11
- package/dist/mcp/tools/project-tools.js +37 -27
- package/dist/mcp/tools/scene-tools.js +106 -29
- package/dist/mcp/tools/visual-tools.js +4 -3
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +74 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +140 -0
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +7 -9
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/mcpupdate/SKILL.md +9 -9
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +32 -3
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- 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
|
|
8
|
+
allowed-tools: Read Write Edit summer_write_file summer_read_file summer_inspect_node summer_inspect_resource summer_get_script_errors summer_get_debugger_errors summer_play summer_stop
|
|
9
9
|
paths: ["**/*.tscn", "**/*.gd", "**/*.gdshader", "addons/vfx/**"]
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -36,12 +36,17 @@ The dissolve shader samples 3D noise per fragment and clips pixels whose noise v
|
|
|
36
36
|
### 1. Files to create
|
|
37
37
|
|
|
38
38
|
```
|
|
39
|
+
addons/vfx/_building-blocks/noise-3d-fbm.gdshaderinc # copy from this skill FIRST
|
|
39
40
|
addons/vfx/dissolve/dissolve.gdshader
|
|
40
41
|
addons/vfx/dissolve/dissolve_controller.gd
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
No `.tscn` needed — this overrides materials on existing meshes.
|
|
44
45
|
|
|
46
|
+
`dissolve.gdshader` `#include`s the FBM noise file. A missing include is a hard
|
|
47
|
+
compile error, so copy `_building-blocks/noise-3d-fbm.gdshaderinc` from this skill
|
|
48
|
+
into `res://addons/vfx/_building-blocks/` before writing the shader.
|
|
49
|
+
|
|
45
50
|
### 2. Shader code
|
|
46
51
|
|
|
47
52
|
`addons/vfx/dissolve/dissolve.gdshader`:
|
|
@@ -68,8 +73,16 @@ uniform vec3 noise_offset = vec3(0.0);
|
|
|
68
73
|
uniform bool use_object_space = true;
|
|
69
74
|
|
|
70
75
|
void fragment() {
|
|
71
|
-
//
|
|
72
|
-
|
|
76
|
+
// In fragment(), VERTEX is in VIEW space — not world, not object. Undo the view
|
|
77
|
+
// transform first, then the model transform, or neither branch samples where it
|
|
78
|
+
// claims to (`inverse(MODEL_MATRIX) * vec4(VERTEX, 1.0)` alone is object-space
|
|
79
|
+
// noise driven by camera-relative coordinates, which slides with the camera).
|
|
80
|
+
vec3 wpos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
|
81
|
+
vec3 opos = (inverse(MODEL_MATRIX) * vec4(wpos, 1.0)).xyz;
|
|
82
|
+
|
|
83
|
+
// Object space keeps the dissolve pattern attached to the mesh; world space
|
|
84
|
+
// keeps it fixed in the level and lets the mesh move through it.
|
|
85
|
+
vec3 npos = (use_object_space ? opos : wpos) * noise_scale + noise_offset;
|
|
73
86
|
float n = fbm3(npos);
|
|
74
87
|
|
|
75
88
|
// Discard everything below the threshold.
|
|
@@ -204,7 +217,15 @@ No new nodes added. The recipe overrides the `material_override` on every `MeshI
|
|
|
204
217
|
|
|
205
218
|
### 5. Wire it in (MCP calls)
|
|
206
219
|
|
|
207
|
-
This recipe is
|
|
220
|
+
This recipe is script-driven — no scene mutation. Write the two files with
|
|
221
|
+
`summer_write_file` (one of `create_only` or `expected_sha256` is required), plus the
|
|
222
|
+
`.gdshaderinc`, then call from gameplay code.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
summer_write_file(path="res://addons/vfx/_building-blocks/noise-3d-fbm.gdshaderinc", content="<from this skill>", create_only=true)
|
|
226
|
+
summer_write_file(path="res://addons/vfx/dissolve/dissolve.gdshader", content="<section 2>", create_only=true)
|
|
227
|
+
summer_write_file(path="res://addons/vfx/dissolve/dissolve_controller.gd", content="<section 3>", create_only=true)
|
|
228
|
+
```
|
|
208
229
|
|
|
209
230
|
```gdscript
|
|
210
231
|
# When the enemy dies:
|
|
@@ -225,6 +246,20 @@ DissolveController.dissolve_object(target, 1.2, Color(1, 0.55, 0.1), 6.0, false)
|
|
|
225
246
|
DissolveController.materialize_object(target, 0.8)
|
|
226
247
|
```
|
|
227
248
|
|
|
249
|
+
### 5b. Verify
|
|
250
|
+
|
|
251
|
+
Both files are compiled code. Check them before claiming the dissolve works.
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
summer_get_script_errors # dissolve_controller.gd parsed?
|
|
255
|
+
summer_play
|
|
256
|
+
summer_get_debugger_errors # shader compile errors surface here at runtime
|
|
257
|
+
summer_stop
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A missing `noise-3d-fbm.gdshaderinc` fails the whole shader, and the mesh then
|
|
261
|
+
renders with the fallback material rather than dissolving.
|
|
262
|
+
|
|
228
263
|
### 6. Parameters to tune
|
|
229
264
|
|
|
230
265
|
| Parameter | Range | Effect |
|
|
@@ -234,7 +269,7 @@ DissolveController.materialize_object(target, 0.8)
|
|
|
234
269
|
| `edge_color` | Color | rim glow tint; orange = fire, blue = magic, green = poison, white = holy |
|
|
235
270
|
| `edge_emission` | 0.0–16.0 | rim bloom strength; needs Bloom in WorldEnvironment |
|
|
236
271
|
| `noise_scale` | 0.5–12.0 | small = big patches dissolving; large = fine pixel-grain dust |
|
|
237
|
-
| `use_object_space` | bool | true = pattern attached to mesh; false = pattern stays in world (cool for moving objects) |
|
|
272
|
+
| `use_object_space` | bool | true = pattern attached to mesh; false = pattern stays in world (cool for moving objects). Both branches derive their position from `INV_VIEW_MATRIX * vec4(VERTEX, 1.0)`, because `VERTEX` in `fragment()` is view space |
|
|
238
273
|
|
|
239
274
|
## Cookbook — named variants
|
|
240
275
|
|
|
@@ -304,13 +339,15 @@ noise_scale = 4.0
|
|
|
304
339
|
- **Dissolving a particle system (e.g., torch flame).** The shader is `spatial`, not `particles`. Stop the particles, don't try to dissolve them.
|
|
305
340
|
- **Forgetting to seed `noise_offset` per instance.** Two enemies dissolving at the same frame use identical patterns → they look like clones. The included controller randomizes per-instance.
|
|
306
341
|
- **Using world-space noise on a moving target.** The dissolve pattern slides over the surface as the mesh moves. Use `use_object_space = true` (default).
|
|
342
|
+
- **Treating `VERTEX` in `fragment()` as world space.** It is view space. Sampling noise from it directly makes the pattern slide as the *camera* moves, and `inverse(MODEL_MATRIX) * vec4(VERTEX, 1.0)` is not object space either. Go through `INV_VIEW_MATRIX` first, as section 2 does.
|
|
307
343
|
- **Skipping the texture inheritance.** The mesh dissolves but goes white because `base_color` defaults to white and no texture is set. The controller pulls from the original `BaseMaterial3D`; if your mesh uses a custom shader, manually wire the textures.
|
|
308
344
|
- **Dissolving a `Skeleton3D` directly.** It's not a mesh — recurse into `MeshInstance3D` children. The controller's `_collect_meshes` does this.
|
|
309
345
|
|
|
310
346
|
## Performance notes
|
|
311
347
|
|
|
312
|
-
- Per fragment: one FBM noise call + one branch
|
|
313
|
-
- Multiple meshes on a character (head, body, weapon, cape): each gets its own override
|
|
348
|
+
- Per fragment: one FBM noise call (4 octaves, 8 hashes each) + one branch + two 4×4 inversions. Sub-millisecond per dissolving object at 1080p covering 10% of the screen — an order-of-magnitude expectation, not a measurement. Profile if you need a number.
|
|
349
|
+
- Multiple meshes on a character (head, body, weapon, cape): each gets its own override, and the cost scales with the number of materials. Still fine for a handful.
|
|
350
|
+
- `inverse(MODEL_MATRIX)` runs per fragment. If you are dissolving a large screen-filling mesh and profiling says this hurts, hoist the object-space position into a `varying` computed in `vertex()`.
|
|
314
351
|
- `discard` defeats early-Z. For a screen full of dissolving enemies, consider `cull_back` (already enabled) and avoid stacking 20+ dissolves at once.
|
|
315
352
|
- Mobile: drop `noise_scale` to a value the FBM can compute in 1–2 octaves, or replace the `fbm3` include with a single-octave noise.
|
|
316
353
|
|
|
@@ -324,11 +361,16 @@ noise_scale = 4.0
|
|
|
324
361
|
|
|
325
362
|
## Fallback (no MCP)
|
|
326
363
|
|
|
327
|
-
|
|
364
|
+
Section 5 is fully automatable — `summer_write_file` writes all three files and
|
|
365
|
+
`summer_get_script_errors` proves they parsed. Do not hand these steps to the user
|
|
366
|
+
when the MCP tools are available.
|
|
367
|
+
|
|
368
|
+
Without the MCP connection there is no engine to drive, so the user does it themselves:
|
|
328
369
|
|
|
329
|
-
1. Create `addons/vfx/dissolve/` and write the
|
|
370
|
+
1. Create `addons/vfx/_building-blocks/` and `addons/vfx/dissolve/` and write the three files above.
|
|
330
371
|
2. From any gameplay script, call `DissolveController.dissolve_object(target_node)`.
|
|
331
372
|
3. The controller handles material override, texture inheritance, tween, and freeing.
|
|
373
|
+
4. Check the Errors dock — a missing `.gdshaderinc` fails the whole shader.
|
|
332
374
|
|
|
333
375
|
## Handoff
|
|
334
376
|
|
|
@@ -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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
103
|
-
|
|
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(
|
|
124
|
-
float th =
|
|
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
|
-
(
|
|
173
|
+
(rnd(s + 13u) - 0.5) * spread_angle,
|
|
130
174
|
1.0,
|
|
131
|
-
(
|
|
175
|
+
(rnd(s + 17u) - 0.5) * spread_angle
|
|
132
176
|
);
|
|
133
177
|
VELOCITY = normalize(dir) * rise_speed;
|
|
134
178
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
CUSTOM.
|
|
139
|
-
CUSTOM
|
|
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.
|
|
146
|
-
VELOCITY.z += cos(TIME * 2.3 + CUSTOM.
|
|
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
|
|
228
|
-
└──
|
|
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
|
-
|
|
237
|
-
|
|
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
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
#
|
|
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",
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
summer_set_prop(path="./World/Torch/
|
|
251
|
-
summer_set_prop(path="./World/Torch/
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
364
|
-
2.
|
|
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 `
|
|
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`).
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|