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 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
|
|
|
@@ -36,11 +36,16 @@ Smoke is the same particle pattern as fire — billboard quads emitted from a sm
|
|
|
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/smoke/smoke.gdshader
|
|
40
41
|
addons/vfx/smoke/smoke_controller.gd
|
|
41
42
|
addons/vfx/smoke/smoke.tscn
|
|
42
43
|
```
|
|
43
44
|
|
|
45
|
+
`smoke.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. Shader code
|
|
45
50
|
|
|
46
51
|
`addons/vfx/smoke/smoke.gdshader`:
|
|
@@ -59,20 +64,41 @@ uniform float noise_speed : hint_range(0.0, 2.0) = 0.5;
|
|
|
59
64
|
uniform float soft_edge : hint_range(0.01, 0.5) = 0.30;
|
|
60
65
|
uniform float depth_fade : hint_range(0.0, 4.0) = 1.0; // soft against geometry behind
|
|
61
66
|
|
|
67
|
+
// INSTANCE_CUSTOM is vertex-stage only, so carry what fragment() needs across.
|
|
68
|
+
varying float age;
|
|
69
|
+
varying float seed;
|
|
70
|
+
|
|
62
71
|
void vertex() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Billboard toward the camera, keeping per-particle rotation (INSTANCE_CUSTOM.x)
|
|
73
|
+
// and per-particle scale. The bare `VIEW_MATRIX * mat4(INV_VIEW_MATRIX[0..2],
|
|
74
|
+
// MODEL_MATRIX[3])` form throws both away — this mirrors what Godot's own
|
|
75
|
+
// BILLBOARD_PARTICLES generator emits (scene/resources/material.cpp:1313-1334).
|
|
76
|
+
mat4 mat_world = mat4(
|
|
77
|
+
normalize(INV_VIEW_MATRIX[0]),
|
|
78
|
+
normalize(INV_VIEW_MATRIX[1]),
|
|
79
|
+
normalize(INV_VIEW_MATRIX[2]),
|
|
80
|
+
MODEL_MATRIX[3]);
|
|
81
|
+
mat_world = mat_world * mat4(
|
|
82
|
+
vec4(cos(INSTANCE_CUSTOM.x), -sin(INSTANCE_CUSTOM.x), 0.0, 0.0),
|
|
83
|
+
vec4(sin(INSTANCE_CUSTOM.x), cos(INSTANCE_CUSTOM.x), 0.0, 0.0),
|
|
84
|
+
vec4(0.0, 0.0, 1.0, 0.0),
|
|
85
|
+
vec4(0.0, 0.0, 0.0, 1.0));
|
|
86
|
+
MODELVIEW_MATRIX = VIEW_MATRIX * mat_world * mat4(
|
|
87
|
+
vec4(length(MODEL_MATRIX[0].xyz), 0.0, 0.0, 0.0),
|
|
88
|
+
vec4(0.0, length(MODEL_MATRIX[1].xyz), 0.0, 0.0),
|
|
89
|
+
vec4(0.0, 0.0, length(MODEL_MATRIX[2].xyz), 0.0),
|
|
90
|
+
vec4(0.0, 0.0, 0.0, 1.0));
|
|
91
|
+
MODELVIEW_NORMAL_MATRIX = mat3(MODELVIEW_MATRIX);
|
|
92
|
+
|
|
93
|
+
// CUSTOM.x is the rotation angle, NOT the age. The age lives in CUSTOM.y
|
|
94
|
+
// (it counts up by DELTA/LIFETIME) and CUSTOM.w holds the lifetime scale.
|
|
95
|
+
age = clamp(INSTANCE_CUSTOM.y / max(INSTANCE_CUSTOM.w, 0.0001), 0.0, 1.0);
|
|
96
|
+
seed = fract(sin(float(INSTANCE_ID) * 12.9898) * 43758.5453);
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
void fragment() {
|
|
72
|
-
float age = CUSTOM.x;
|
|
73
|
-
|
|
74
100
|
// Distort UVs with slow noise.
|
|
75
|
-
vec3 np = vec3(UV * noise_scale, TIME * noise_speed +
|
|
101
|
+
vec3 np = vec3(UV * noise_scale, TIME * noise_speed + seed * 10.0);
|
|
76
102
|
float n = fbm3(np);
|
|
77
103
|
|
|
78
104
|
// Soft round mask, modulated by noise so the silhouette is wispy not circular.
|
|
@@ -162,6 +188,10 @@ func _apply() -> void:
|
|
|
162
188
|
|
|
163
189
|
func _make_scale_curve(end_mult: float) -> CurveTexture:
|
|
164
190
|
var c := Curve.new()
|
|
191
|
+
# Curve clamps every sample to max_value, which defaults to 1.0. Without this
|
|
192
|
+
# line every end multiplier above 1.0 is silently flattened back to 1.0 and the
|
|
193
|
+
# whole cookbook's growth numbers do nothing.
|
|
194
|
+
c.max_value = maxf(1.0, end_mult)
|
|
165
195
|
c.add_point(Vector2(0.0, 0.4))
|
|
166
196
|
c.add_point(Vector2(0.3, 1.0))
|
|
167
197
|
c.add_point(Vector2(1.0, end_mult))
|
|
@@ -188,30 +218,52 @@ func stop_smoke(fade_seconds: float = 1.5) -> void:
|
|
|
188
218
|
```
|
|
189
219
|
GPUParticles3D ("Smoke") [script: smoke_controller.gd]
|
|
190
220
|
├── process_material: ParticleProcessMaterial
|
|
191
|
-
└── draw_pass_1: QuadMesh (size 1
|
|
192
|
-
└──
|
|
221
|
+
└── draw_pass_1: QuadMesh (size Vector2(1, 1))
|
|
222
|
+
└── material: ShaderMaterial (shader: smoke.gdshader)
|
|
193
223
|
```
|
|
194
224
|
|
|
225
|
+
`process_material` and `draw_pass_1` are resource properties on the one node, not
|
|
226
|
+
child nodes, and `ParticleProcessMaterial` is a `Material` — you cannot
|
|
227
|
+
`summer_add_node` it.
|
|
228
|
+
|
|
195
229
|
### 5. Wire it in (MCP calls)
|
|
196
230
|
|
|
231
|
+
Every scene-mutating call takes an explicit `scenePath`. `summer_set_prop` uses `key`
|
|
232
|
+
(not `property`) and only accepts a string, number, or boolean.
|
|
233
|
+
|
|
197
234
|
For chimney smoke or fire pairing, parent the Smoke just above the heat source:
|
|
198
235
|
|
|
199
236
|
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
summer_set_prop(path="./World/Cottage/Chimney/Smoke",
|
|
206
|
-
|
|
237
|
+
summer_write_file(path="res://addons/vfx/_building-blocks/noise-3d-fbm.gdshaderinc", content="<from this skill>", create_only=true)
|
|
238
|
+
summer_write_file(path="res://addons/vfx/smoke/smoke.gdshader", content="<section 2>", create_only=true)
|
|
239
|
+
summer_write_file(path="res://addons/vfx/smoke/smoke_controller.gd", content="<section 3>", create_only=true)
|
|
240
|
+
|
|
241
|
+
summer_add_node(scenePath="res://main.tscn", parent="./World/Cottage/Chimney", type="GPUParticles3D", name="Smoke")
|
|
242
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Cottage/Chimney/Smoke", key="script", value="res://addons/vfx/smoke/smoke_controller.gd")
|
|
243
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Cottage/Chimney/Smoke", key="position", value="Vector3(0, 0.3, 0)")
|
|
244
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Cottage/Chimney/Smoke", key="rise_height", value=4.0)
|
|
245
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Cottage/Chimney/Smoke", key="particle_count", value=48)
|
|
246
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Cottage/Chimney/Smoke", key="wind_direction", value="Vector3(0.4, 0, 0.1)")
|
|
247
|
+
summer_save_scene(scenePath="res://main.tscn")
|
|
207
248
|
```
|
|
208
249
|
|
|
209
250
|
For pairing with a fire:
|
|
210
251
|
|
|
211
252
|
```
|
|
212
|
-
summer_add_node(parent="./World/Campfire", type="GPUParticles3D", name="Smoke")
|
|
213
|
-
summer_set_prop(path="./World/Campfire/Smoke",
|
|
214
|
-
summer_set_prop(path="./World/Campfire/Smoke",
|
|
253
|
+
summer_add_node(scenePath="res://main.tscn", parent="./World/Campfire", type="GPUParticles3D", name="Smoke")
|
|
254
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Campfire/Smoke", key="position", value="Vector3(0, 1.5, 0)") # above flame top
|
|
255
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Campfire/Smoke", key="rise_height", value=3.0)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 5b. Verify
|
|
259
|
+
|
|
260
|
+
The shader and the controller are compiled code. Check them before tuning anything.
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
summer_get_script_errors # smoke_controller.gd parsed?
|
|
264
|
+
summer_play
|
|
265
|
+
summer_get_debugger_errors # shader compile errors surface here at runtime
|
|
266
|
+
summer_stop
|
|
215
267
|
```
|
|
216
268
|
|
|
217
269
|
For a rocket trail (smoke follows the rocket):
|
|
@@ -323,13 +375,15 @@ color_old = Color(0.65, 0.68, 0.75)
|
|
|
323
375
|
- **Forgetting `cull_disabled`.** Billboarded quads disappear at certain camera angles otherwise.
|
|
324
376
|
- **`puff_lifetime < 1.0`.** Smoke needs time to spread out and dilute. Short-lifetime smoke looks like spray.
|
|
325
377
|
- **No scale curve.** Smoke that doesn't grow looks like a row of identical balls. The controller's `_make_scale_curve` ramps from 0.4× → 1.0× → end_mult.
|
|
378
|
+
- **A scale curve built without raising `Curve.max_value`.** `Curve` defaults to `max_value = 1.0` and silently clamps, so every `end_scale_multiplier` above 1.0 is inert and the smoke never grows. Set `c.max_value` before adding the points.
|
|
379
|
+
- **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`.
|
|
326
380
|
- **`emission_energy > 0` on the shader's quad material.** Smoke isn't bright. Keep `unshaded` and let `ALBEDO` carry the look. (The shader as written has no EMISSION line — correct.)
|
|
327
381
|
- **Smoke local-space when paired with a moving emitter (rocket).** Set `local_coords = false` so puffs stay where they were emitted.
|
|
328
|
-
- **Skipping the angular velocity.** Static-rotation smoke looks weird. The controller adds ±25 deg/s for organic tumbling.
|
|
382
|
+
- **Skipping the angular velocity.** Static-rotation smoke looks weird. The controller adds ±25 deg/s for organic tumbling — but that only reaches the screen if the vertex billboard applies the `INSTANCE_CUSTOM.x` rotation, which is why section 2 uses the full block rather than the bare `VIEW_MATRIX * mat4(INV_VIEW_MATRIX[0..2], MODEL_MATRIX[3])` form.
|
|
329
383
|
|
|
330
384
|
## Performance notes
|
|
331
385
|
|
|
332
|
-
- Default 48 particles per smoke source
|
|
386
|
+
- Default 48 particles per smoke source is sub-millisecond on desktop. That is an order-of-magnitude expectation, not a measurement — profile your own scene.
|
|
333
387
|
- Heavy `density` at high count (128 particles, density 0.85) gets fillrate-bound — overdraw on a screen full of smoke. Budget ~256 smoke particles total visible at once.
|
|
334
388
|
- Mobile: drop default to 24, drop `puff_lifetime` to 2.5, drop `density` to 0.35.
|
|
335
389
|
- LOD: scale `amount_ratio` by `clamp(1.5 - dist_to_camera / 40.0, 0.10, 1.0)` for distant chimneys.
|
|
@@ -356,13 +410,20 @@ ALPHA *= clamp(d * depth_fade, 0.0, 1.0);
|
|
|
356
410
|
|
|
357
411
|
## Fallback (no MCP)
|
|
358
412
|
|
|
359
|
-
|
|
413
|
+
Section 5 is fully automatable — `summer_write_file` writes the shader and the
|
|
414
|
+
controller, `summer_create_scene` + `summer_add_node` + `summer_set_prop` +
|
|
415
|
+
`summer_save_scene` build and save `smoke.tscn`. Do not hand these steps to the user
|
|
416
|
+
when the MCP tools are available.
|
|
417
|
+
|
|
418
|
+
Without the MCP connection there is no engine to drive, so the user does it
|
|
419
|
+
manually in Summer Engine:
|
|
360
420
|
|
|
361
|
-
1. Create `addons/vfx/smoke/` with the
|
|
362
|
-
2.
|
|
421
|
+
1. Create `addons/vfx/_building-blocks/` and `addons/vfx/smoke/` with the files above.
|
|
422
|
+
2. Add a `GPUParticles3D`, attach `smoke_controller.gd`.
|
|
363
423
|
3. Set `process_material` to a new `ParticleProcessMaterial` (script configures it).
|
|
364
|
-
4. Set `draw_pass_1` to a new `QuadMesh` with `
|
|
424
|
+
4. Set `draw_pass_1` to a new `QuadMesh` with its `material` = `ShaderMaterial(smoke.gdshader)`.
|
|
365
425
|
5. Save as `smoke.tscn`; instantiate at every smoke source.
|
|
426
|
+
6. Check the Errors dock — a missing `.gdshaderinc` fails the whole shader.
|
|
366
427
|
|
|
367
428
|
## Handoff
|
|
368
429
|
|
|
@@ -5,13 +5,13 @@ 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
|
|
|
12
12
|
# water-ripple — Impact Ripples on a Water Plane
|
|
13
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.
|
|
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.
|
|
15
15
|
|
|
16
16
|
## When to use
|
|
17
17
|
|
|
@@ -60,6 +60,12 @@ uniform sampler2D normal_map : hint_normal;
|
|
|
60
60
|
// Per-ripple data: (origin.xz, start_time, amplitude * (1 if active else 0))
|
|
61
61
|
uniform vec4 ripples[MAX_RIPPLES];
|
|
62
62
|
|
|
63
|
+
// Ripple ages run off the controller's own clock, pushed here every frame. They
|
|
64
|
+
// cannot run off TIME: shader TIME wraps at
|
|
65
|
+
// rendering/limits/time/time_rollover_secs (3600 by default, verified), which
|
|
66
|
+
// would send every live ripple to a negative age once an hour of uptime.
|
|
67
|
+
uniform float now = 0.0;
|
|
68
|
+
|
|
63
69
|
uniform float ripple_speed : hint_range(0.5, 8.0) = 2.5; // m/s expansion
|
|
64
70
|
uniform float ripple_wavelength : hint_range(0.05, 1.0) = 0.20; // distance between crests
|
|
65
71
|
uniform float ripple_lifetime : hint_range(0.5, 6.0) = 2.5; // seconds visible
|
|
@@ -72,7 +78,7 @@ void vertex() {
|
|
|
72
78
|
for (int i = 0; i < MAX_RIPPLES; i++) {
|
|
73
79
|
vec4 r = ripples[i];
|
|
74
80
|
if (r.w <= 0.0) continue;
|
|
75
|
-
float age =
|
|
81
|
+
float age = now - r.z;
|
|
76
82
|
if (age < 0.0 || age > ripple_lifetime) continue;
|
|
77
83
|
float dist = length(wpos.xz - r.xy);
|
|
78
84
|
float front = age * ripple_speed;
|
|
@@ -95,7 +101,7 @@ void fragment() {
|
|
|
95
101
|
for (int i = 0; i < MAX_RIPPLES; i++) {
|
|
96
102
|
vec4 r = ripples[i];
|
|
97
103
|
if (r.w <= 0.0) continue;
|
|
98
|
-
float age =
|
|
104
|
+
float age = now - r.z;
|
|
99
105
|
if (age < 0.0 || age > ripple_lifetime) continue;
|
|
100
106
|
vec2 d = wpos.xz - r.xy;
|
|
101
107
|
float dist = length(d);
|
|
@@ -108,7 +114,11 @@ void fragment() {
|
|
|
108
114
|
ripple_glow += ring * fade * r.w * 0.5;
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
NORMAL_MAP
|
|
117
|
+
// NORMAL_MAP is decoded by the engine (scene_forward_clustered.glsl:1427 —
|
|
118
|
+
// `normal_map.xy = normal_map.xy * 2.0 - 1.0`), so hand it the RAW [0,1] texel
|
|
119
|
+
// encoding. Assigning an already-decoded +/-1 vector makes a flat surface read
|
|
120
|
+
// as permanent maximum tilt.
|
|
121
|
+
NORMAL_MAP = normalize(base_n + vec3(ripple_n.xy, 0.0)) * 0.5 + 0.5;
|
|
112
122
|
NORMAL_MAP_DEPTH = 0.6;
|
|
113
123
|
|
|
114
124
|
// Fresnel-tinted color.
|
|
@@ -142,6 +152,7 @@ const MAX_RIPPLES := 16
|
|
|
142
152
|
var _ripples: Array[Vector4] = []
|
|
143
153
|
var _next_slot: int = 0
|
|
144
154
|
var _mat: ShaderMaterial
|
|
155
|
+
var _time: float = 0.0
|
|
145
156
|
|
|
146
157
|
func _ready() -> void:
|
|
147
158
|
_ripples.resize(MAX_RIPPLES)
|
|
@@ -168,21 +179,33 @@ func _apply() -> void:
|
|
|
168
179
|
## Spawn a ripple at a world position.
|
|
169
180
|
## amplitude: 0..1 (raindrop ~0.2, footstep ~0.5, body splash ~1.0)
|
|
170
181
|
func add_ripple(world_pos: Vector3, amplitude: float = 0.5) -> void:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
_ripples[_next_slot] = Vector4(world_pos.x, world_pos.z,
|
|
182
|
+
# Stamp with our own clock, not Time.get_ticks_msec(): the shader compares
|
|
183
|
+
# against the same `now` uniform, so the two only line up if one source drives both.
|
|
184
|
+
_ripples[_next_slot] = Vector4(world_pos.x, world_pos.z, _time, clamp(amplitude, 0.0, 1.0))
|
|
174
185
|
_next_slot = (_next_slot + 1) % MAX_RIPPLES
|
|
175
186
|
_push()
|
|
176
187
|
|
|
188
|
+
## True when `world_pos` is over this surface (XZ against the mesh's local AABB).
|
|
189
|
+
## Use this rather than a containment method on the caller — there isn't one.
|
|
190
|
+
func contains_point(world_pos: Vector3) -> bool:
|
|
191
|
+
if mesh == null:
|
|
192
|
+
return false
|
|
193
|
+
var local := to_local(world_pos)
|
|
194
|
+
var box := mesh.get_aabb()
|
|
195
|
+
return local.x >= box.position.x and local.x <= box.end.x \
|
|
196
|
+
and local.z >= box.position.z and local.z <= box.end.z
|
|
197
|
+
|
|
177
198
|
func _push() -> void:
|
|
178
199
|
if _mat == null: return
|
|
179
200
|
_mat.set_shader_parameter("ripples", _ripples)
|
|
180
201
|
|
|
181
|
-
func
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
func _process(delta: float) -> void:
|
|
203
|
+
# Drive ripple age from this clock and push it every frame. Using the shader's
|
|
204
|
+
# TIME instead would silently kill every ripple after
|
|
205
|
+
# rendering/limits/time/time_rollover_secs (3600 s default) of uptime.
|
|
206
|
+
_time += delta
|
|
207
|
+
if _mat:
|
|
208
|
+
_mat.set_shader_parameter("now", _time)
|
|
186
209
|
# Re-push so editor updates pick up changes; cheap.
|
|
187
210
|
if Engine.is_editor_hint():
|
|
188
211
|
_push()
|
|
@@ -198,20 +221,48 @@ MeshInstance3D ("Water") [script: water_ripple_surface.gd]
|
|
|
198
221
|
|
|
199
222
|
### 5. Wire it in (MCP calls)
|
|
200
223
|
|
|
224
|
+
Every scene-mutating call takes an explicit `scenePath`. `summer_set_prop` uses `key`
|
|
225
|
+
(not `property`); `summer_set_resource_property` always needs a `subProperty` and is
|
|
226
|
+
for reaching *into* a resource, not for assigning one.
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
summer_write_file(path="res://addons/vfx/water-ripple/water_ripple.gdshader", content="<section 2>", create_only=true)
|
|
230
|
+
summer_write_file(path="res://addons/vfx/water-ripple/water_ripple_surface.gd", content="<section 3>", create_only=true)
|
|
231
|
+
|
|
232
|
+
summer_add_node(scenePath="res://main.tscn", parent="./World", type="MeshInstance3D", name="Puddle")
|
|
233
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Puddle", key="script", value="res://addons/vfx/water-ripple/water_ripple_surface.gd")
|
|
234
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Puddle", key="position", value="Vector3(0, 0.01, 0)")
|
|
235
|
+
|
|
236
|
+
# Whole resources are assigned by path with summer_set_prop.
|
|
237
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Puddle", key="mesh", value="res://addons/vfx/water-ripple/plane.tres")
|
|
238
|
+
# `normal_texture` is an exported script property on the controller, not a
|
|
239
|
+
# MeshInstance3D property — MeshInstance3D has no `normal_texture`.
|
|
240
|
+
summer_set_prop(scenePath="res://main.tscn", path="./World/Puddle", key="normal_texture", value="res://addons/vfx/water-ripple/water_normals.png")
|
|
241
|
+
|
|
242
|
+
# subProperty is required — this is how you reach into the PlaneMesh itself.
|
|
243
|
+
summer_set_resource_property(scenePath="res://main.tscn", nodePath="./World/Puddle", resourceProperty="mesh", subProperty="subdivide_width", value=32)
|
|
244
|
+
summer_set_resource_property(scenePath="res://main.tscn", nodePath="./World/Puddle", resourceProperty="mesh", subProperty="subdivide_depth", value=32)
|
|
245
|
+
|
|
246
|
+
summer_save_scene(scenePath="res://main.tscn")
|
|
201
247
|
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
248
|
+
|
|
249
|
+
### 5b. Verify
|
|
250
|
+
|
|
251
|
+
The shader and the controller are compiled code, and the controller `preload`s the
|
|
252
|
+
shader — a shader that failed to write takes the script down at parse time.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
summer_get_script_errors # water_ripple_surface.gd parsed?
|
|
256
|
+
summer_play
|
|
257
|
+
summer_get_debugger_errors # shader compile errors surface here at runtime
|
|
258
|
+
summer_stop
|
|
208
259
|
```
|
|
209
260
|
|
|
210
261
|
Then trigger ripples from any system. Footstep:
|
|
211
262
|
|
|
212
263
|
```gdscript
|
|
213
264
|
# In your character controller's footstep callback:
|
|
214
|
-
if puddle and puddle.
|
|
265
|
+
if puddle and puddle.contains_point(global_position):
|
|
215
266
|
puddle.add_ripple(global_position, 0.5)
|
|
216
267
|
```
|
|
217
268
|
|
|
@@ -295,13 +346,15 @@ ripple_wavelength= 0.40
|
|
|
295
346
|
- **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
347
|
- **Ripple position in local instead of world space.** The shader expects world XZ. Always convert in the GDScript before pushing.
|
|
297
348
|
- **No normal map assigned.** The surface looks like flat-shaded plastic. Always assign a tileable water normals texture (any blue noise normals will do).
|
|
349
|
+
- **Writing a decoded ±1 vector into `NORMAL_MAP`.** The engine decodes it again (`normal_map.xy = normal_map.xy * 2.0 - 1.0`), so a flat surface reads as permanent maximum tilt. Re-encode with `* 0.5 + 0.5` before assigning, as section 2 does.
|
|
350
|
+
- **Timing ripples against shader `TIME`.** `TIME` wraps at `rendering/limits/time/time_rollover_secs` (3600 s default) and is not `Time.get_ticks_msec() / 1000`. Push your own accumulated clock as a uniform and compare against that; otherwise every ripple silently vanishes after an hour of uptime.
|
|
298
351
|
- **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
352
|
- **`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
353
|
- **Triggering ripples in `_process` without spacing.** Spamming 60/sec floods the buffer. Throttle to ~5 ripples/sec for rain unless tuned.
|
|
301
354
|
|
|
302
355
|
## Performance notes
|
|
303
356
|
|
|
304
|
-
- Cost is O(MAX_RIPPLES × pixels_drawn ×
|
|
357
|
+
- Cost is O(MAX_RIPPLES × pixels_drawn) in the fragment loop plus O(MAX_RIPPLES × verts) in the vertex loop. 16 ripples on a 5×5 m puddle at 1080p is cheap — an order-of-magnitude expectation, not a measurement. Profile if you need a number.
|
|
305
358
|
- The vertex displacement loop runs per vertex; raising plane subdivision past 64 starts to hurt. 32×32 is plenty.
|
|
306
359
|
- 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
360
|
- Mobile: drop `MAX_RIPPLES` to 8, disable vertex displacement (set `ripple_height = 0` and skip the vertex loop), and rely on normal-only ripples.
|
|
@@ -316,12 +369,19 @@ ripple_wavelength= 0.40
|
|
|
316
369
|
|
|
317
370
|
## Fallback (no MCP)
|
|
318
371
|
|
|
319
|
-
|
|
372
|
+
Section 5 is fully automatable — `summer_write_file` writes the shader and the
|
|
373
|
+
controller, and `summer_add_node` + `summer_set_prop` +
|
|
374
|
+
`summer_set_resource_property` + `summer_save_scene` build the plane including its
|
|
375
|
+
subdivision. Do not hand these steps to the user when the MCP tools are available.
|
|
376
|
+
|
|
377
|
+
Without the MCP connection there is no engine to drive, so the user does it
|
|
378
|
+
manually in Summer Engine:
|
|
320
379
|
|
|
321
380
|
1. Create `addons/vfx/water-ripple/` and write the three files above.
|
|
322
|
-
2.
|
|
381
|
+
2. Add a `MeshInstance3D` with a high-subdivision `PlaneMesh` (32×32 minimum).
|
|
323
382
|
3. Attach `water_ripple_surface.gd` and assign a tileable water normals texture.
|
|
324
383
|
4. From your trigger code, call `$Puddle.add_ripple(impact_pos, amplitude)`.
|
|
384
|
+
5. Check the Errors dock — the controller `preload`s the shader, so a broken shader path fails the script too.
|
|
325
385
|
|
|
326
386
|
## Handoff
|
|
327
387
|
|
|
@@ -161,4 +161,4 @@ A browser-based companion for showing mockups, diagrams, and visual options duri
|
|
|
161
161
|
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
|
|
162
162
|
|
|
163
163
|
If they agree to the companion, read the detailed guide before proceeding:
|
|
164
|
-
`skills/brainstorming/visual-companion.md`
|
|
164
|
+
`skills/workflow/brainstorming/visual-companion.md`
|
|
@@ -11,7 +11,7 @@ description: Use when a gameplay feature works correctly but feels wrong — flo
|
|
|
11
11
|
|
|
12
12
|
**Core principle:** Game-feel bugs are tuning bugs, not logic bugs. Treat them with discipline — isolate one variable, compare to a reference, measure what you can, feel what you can't.
|
|
13
13
|
|
|
14
|
-
This skill is for the diagnostic phase. The polish-and-juice layer (screen shake, hit-stop, particles, sound design) is `summer:
|
|
14
|
+
This skill is for the diagnostic phase. The polish-and-juice layer (screen shake, hit-stop, particles, sound design) is `summer:game-feel` — go there once you've identified WHICH variable is wrong.
|
|
15
15
|
|
|
16
16
|
## When To Use
|
|
17
17
|
|
|
@@ -80,16 +80,36 @@ Two compare modes:
|
|
|
80
80
|
|
|
81
81
|
**Feel:** subjective. The user (or you, if you have eyes on the screen) reports "warmer" or "colder." This is fine. Most game-feel work is felt, not measured.
|
|
82
82
|
|
|
83
|
-
**Measure:** objective. When the feel is ambiguous, drop down to frame counts.
|
|
83
|
+
**Measure:** objective. When the feel is ambiguous, drop down to frame counts — and take them yourself with a `RunVerification` probe rather than asking the user to eyeball anything. The probe presses the input, samples per frame, and hands back an array.
|
|
84
84
|
|
|
85
85
|
| Question | Measurement |
|
|
86
86
|
|---|---|
|
|
87
|
-
| "Does the jump peak too fast?" | `
|
|
88
|
-
| "Is the camera too laggy?" |
|
|
89
|
-
| "Is the input dead?" |
|
|
90
|
-
| "Is the hit-stop too short?" |
|
|
87
|
+
| "Does the jump peak too fast?" | Probe: `press("jump")`, sample `player.velocity.y` every `process_frame`, count frames to the sign flip |
|
|
88
|
+
| "Is the camera too laggy?" | Probe: sample `camera.global_position - target.global_position` per frame while driving movement |
|
|
89
|
+
| "Is the input dead?" | Probe: report the frame input was injected and the frame the state machine changed; subtract |
|
|
90
|
+
| "Is the hit-stop too short?" | Probe: report frame at hit and frame at unfreeze |
|
|
91
|
+
|
|
92
|
+
Sketch:
|
|
93
|
+
|
|
94
|
+
```gdscript
|
|
95
|
+
extends SummerProbeBase
|
|
96
|
+
func _ready() -> void:
|
|
97
|
+
await super._ready()
|
|
98
|
+
await get_tree().process_frame
|
|
99
|
+
var p := get_tree().root.find_child("Player", true, false)
|
|
100
|
+
var trace: Array = []
|
|
101
|
+
await press("jump", 100)
|
|
102
|
+
for i in 40:
|
|
103
|
+
await get_tree().process_frame
|
|
104
|
+
trace.append(p.velocity.y)
|
|
105
|
+
report("vel_y_per_frame", trace)
|
|
106
|
+
save_frame("apex")
|
|
107
|
+
finish()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Send it with `summer_batch ops:[{"op":"RunVerification","probe_source":"<the probe>","max_seconds":20}]`.
|
|
91
111
|
|
|
92
|
-
The point of measurement is to ground the conversation in numbers when "feels off" stops being useful. You do not need to measure most tweaks — only the ambiguous ones.
|
|
112
|
+
The point of measurement is to ground the conversation in numbers when "feels off" stops being useful. You do not need to measure most tweaks — only the ambiguous ones. **The verdict on whether it now feels right stays with the user** — that part is genuinely subjective, and it is the only part of this loop you hand over.
|
|
93
113
|
|
|
94
114
|
### 5. Decide
|
|
95
115
|
|
|
@@ -144,7 +164,7 @@ These are diagnostic tools, not lecture material. When the user says "feels weig
|
|
|
144
164
|
| "The user said floaty so I'm jacking gravity" | "Floaty" can also mean weak air control, slow terminal velocity, or too-long apex hang. Confirm before tweaking. |
|
|
145
165
|
| Skipping the play step ("I'll just bump the number and trust the feel") | Game feel is felt, not predicted. Play every tweak. |
|
|
146
166
|
| Adding "while I'm here" tweaks to unrelated features | Game feel debugging is a focused activity. Stay on the one feature. |
|
|
147
|
-
| Going to `summer:
|
|
167
|
+
| Going to `summer:game-feel` before identifying the broken variable | Juice on top of a broken tuning makes the bug louder. Fix tuning first. |
|
|
148
168
|
| Spending >30 minutes on one variable without progress | The anchor is wrong, or the variable is wrong. Step back, re-anchor. |
|
|
149
169
|
|
|
150
170
|
## Rationalization Prevention
|
|
@@ -161,7 +181,7 @@ These are diagnostic tools, not lecture material. When the user says "feels weig
|
|
|
161
181
|
|
|
162
182
|
After diagnosis pins down which variable(s) are wrong, the tuning is done — but the feature may still want polish. Hand off to:
|
|
163
183
|
|
|
164
|
-
- `summer:
|
|
184
|
+
- `summer:game-feel` — for the screen-shake, hit-stop, particle, sound layer once tuning is locked.
|
|
165
185
|
- `summer:debug` — if mid-debug you discover an actual error (signal not firing, node freed) hiding under the feel issue.
|
|
166
186
|
|
|
167
187
|
## The Bottom Line
|
|
@@ -42,21 +42,40 @@ You cannot fix a regression you have not measured. Eyeballing "feels laggy" is n
|
|
|
42
42
|
|
|
43
43
|
### 1. Measure now
|
|
44
44
|
|
|
45
|
+
Measure in a `RunVerification` probe, not by eye. It spawns a hidden, disposable game instance with a real renderer, so `Performance.get_monitor(...)` returns the game's actual numbers and the user's editor session is untouched.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
summer_batch ops:[{"op":"RunVerification","probe_source":"<probe>","max_seconds":20}]
|
|
45
49
|
```
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
```gdscript
|
|
52
|
+
extends SummerProbeBase
|
|
53
|
+
func _ready() -> void:
|
|
54
|
+
await super._ready()
|
|
55
|
+
await get_tree().process_frame
|
|
56
|
+
var samples: Array = []
|
|
57
|
+
for i in 30: # ~5 s at 60 fps, sampled over time
|
|
58
|
+
await get_tree().create_timer(0.16).timeout
|
|
59
|
+
samples.append(Performance.get_monitor(Performance.TIME_FPS))
|
|
60
|
+
report("fps_samples", samples) # spike pattern lives here
|
|
61
|
+
report("draw_calls", Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
|
62
|
+
report("bodies_3d", Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS))
|
|
63
|
+
report("objects", Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME))
|
|
64
|
+
dump_tree()
|
|
65
|
+
finish()
|
|
51
66
|
```
|
|
52
67
|
|
|
53
68
|
What to capture:
|
|
54
69
|
|
|
55
70
|
| Metric | Source | Why |
|
|
56
71
|
|---|---|---|
|
|
57
|
-
| Avg FPS / frame time | `
|
|
58
|
-
| Spike pattern |
|
|
59
|
-
| Scene complexity | `summer_get_scene_tree`
|
|
72
|
+
| Avg FPS / frame time | `Performance.TIME_FPS` / `TIME_PROCESS` sampled in the probe | Headline number |
|
|
73
|
+
| Spike pattern | the per-sample FPS array above | Steady 30 vs intermittent stutters → different causes |
|
|
74
|
+
| Scene complexity | probe `dump_tree()` for the live tree; `summer_get_scene_tree` for the edited scene | Establishes baseline for instance/body count cliffs |
|
|
75
|
+
|
|
76
|
+
`summer_get_diagnostics` does **not** report performance. It returns counts of console errors, debugger errors, debugger warnings and script errors. Use it to confirm the slow run isn't also throwing, never as a source of FPS or draw-call numbers.
|
|
77
|
+
|
|
78
|
+
Discard every sample from the first second — `TIME_FPS` is a per-second average and reads exactly `1.0` until it has a second of history. A leading run of `1.0` in the array is warm-up, not a stall.
|
|
60
79
|
|
|
61
80
|
Write the bad-state number down. "Scene `Level3` runs at 31fps avg, drops to 12fps when 5+ enemies are on screen."
|
|
62
81
|
|
|
@@ -96,7 +115,7 @@ Read the list. Group commits into suspects:
|
|
|
96
115
|
|
|
97
116
|
If a single commit is the obvious suspect (e.g. "Added 200 enemies to spawner"), test it first. Otherwise actually `git bisect` — check out the midpoint, measure FPS, repeat. Don't theorize through it; the bisect is fast and definitive.
|
|
98
117
|
|
|
99
|
-
### 4. Match to one of the four
|
|
118
|
+
### 4. Match to one of the four Summer Engine performance cliffs
|
|
100
119
|
|
|
101
120
|
Godot 4.x regressions cluster heavily into four categories. Knowing them shortens diagnosis to minutes.
|
|
102
121
|
|
|
@@ -127,13 +146,7 @@ If no → that wasn't it. Restore. Try the next suspect.
|
|
|
127
146
|
|
|
128
147
|
The fix is not done until the same measurement that found the regression returns the good number. Same scene, same play duration, same warm-up.
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
summer_clear_console
|
|
132
|
-
summer_play
|
|
133
|
-
── 5-10 seconds of play ──
|
|
134
|
-
summer_get_diagnostics
|
|
135
|
-
summer_stop
|
|
136
|
-
```
|
|
149
|
+
Re-run the identical probe from step 1 — same scene, same sample count, same drive sequence. A number taken a different way is not a comparison.
|
|
137
150
|
|
|
138
151
|
State the result with the number: "Reverted the per-frame `get_tree().get_nodes_in_group()` call in `enemy_manager.gd`. Level3 back to 58fps avg." Not "should be faster now."
|
|
139
152
|
|
|
@@ -156,7 +169,9 @@ State the result with the number: "Reverted the per-frame `get_tree().get_nodes_
|
|
|
156
169
|
| Skipping the known-good measurement | Without a baseline, you don't know when you're done. |
|
|
157
170
|
| Reading the slow scene's code top-to-bottom looking for inefficiency | The regression is in what changed, not in what was already there. |
|
|
158
171
|
| Adding caching / pooling / LOD before identifying the cause | These are tuning moves. They can hide the regression instead of fixing it. |
|
|
159
|
-
| Trusting editor FPS as ground truth | Editor runs `@tool` scripts, debug overlays, and a different render pipeline. Measure in a
|
|
172
|
+
| Trusting editor FPS as ground truth | Editor runs `@tool` scripts, debug overlays, and a different render pipeline. Measure in a `RunVerification` probe or a built binary. |
|
|
173
|
+
| Profiling with `--headless` | No renderer: draw calls read `0.0` and the FPS figure is a bare loop rate. The verify instance is windowed-offscreen for exactly this reason. |
|
|
174
|
+
| Quoting FPS from `summer_get_diagnostics` | It returns error/warning counts only. That number did not come from where you said it did. |
|
|
160
175
|
| "Maybe it's just a Tuesday thing" | Performance does not regress randomly. There is a cause. |
|
|
161
176
|
|
|
162
177
|
## Rationalization Prevention
|
|
@@ -171,11 +186,21 @@ State the result with the number: "Reverted the per-frame `get_tree().get_nodes_
|
|
|
171
186
|
|
|
172
187
|
## When The Engine Isn't Running
|
|
173
188
|
|
|
174
|
-
If
|
|
189
|
+
If the MCP tools report the engine isn't running:
|
|
190
|
+
|
|
191
|
+
1. **Get it running.** `summer run` starts it and restores the probe route. That is one command, and it is almost always the right answer — do not degrade to interviewing the user because a tool returned "not running" once.
|
|
192
|
+
2. **If you have shell access, you can run a probe without the editor at all.** The same verify instance is a plain flag on the Summer binary:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
"<summer-binary>" --path <project-dir> \
|
|
196
|
+
--summer-verify <abs/path/probe.gd> \
|
|
197
|
+
--summer-verify-out <abs/out-dir> \
|
|
198
|
+
--summer-verify-max 20
|
|
199
|
+
```
|
|
175
200
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
201
|
+
Then read `<out-dir>/results.json` (`reports`, `frames`, `errors_seen`, `duration_ms`). Locate the binary rather than guessing a path — on macOS it is the executable inside the installed `Summer.app` bundle. The probe file must extend the probe base; when you invoke the binary directly, write the base alongside it and `extends` it by path.
|
|
202
|
+
3. Only if neither is possible: ask the user for an in-game perf overlay reading (FPS, frame time, draw call count) and walk the bisect with them — "run on commit X, tell me the FPS; run on commit Y, tell me the FPS."
|
|
203
|
+
4. Do not claim a fix is verified from code reading alone.
|
|
179
204
|
|
|
180
205
|
## The Bottom Line
|
|
181
206
|
|
|
@@ -17,9 +17,18 @@ This is the meta-skill: the loop that turns ad-hoc fixes into reusable expertise
|
|
|
17
17
|
|
|
18
18
|
Three repos, one ecosystem:
|
|
19
19
|
|
|
20
|
-
1. **SummerEngine repo** — the
|
|
20
|
+
1. **SummerEngine repo** — the Summer Engine desktop source plus the canonical
|
|
21
|
+
CLI. The C++ engine is maintained against the upstream Godot Engine codebase;
|
|
22
|
+
the Summer-owned module lives at `modules/1summer_engine/`. CLI source lives
|
|
23
|
+
at `tools/summer-cli/` and is normally invoked with
|
|
24
|
+
`npx -y summer-engine@latest`. It installs, signs in, creates, and runs Summer
|
|
25
|
+
projects and can install the current Summer skill bundle for supported
|
|
26
|
+
agents.
|
|
21
27
|
2. **PublicSummerEngine repo** — the web app at summerengine.com. Has its own AI skill stores at `src/lib/ai/skills/bundled/` and `public/knowledge/summer/skills/` for the in-browser chat agent. Also has its own consumers of the engine API in `src/lib/bridge/direct-executor.ts` and `src/lib/ai/tools/run-and-verify.ts` which call the same engine endpoints the CLI does.
|
|
22
|
-
3. **The active game project** — usually
|
|
28
|
+
3. **The active Summer game project** — usually
|
|
29
|
+
`~/development/<game-name>` or whatever game you were just working on. The
|
|
30
|
+
live compatibility line comes from Summer Engine, not this prompt. Real
|
|
31
|
+
working code in this repo is the gold standard for examples.
|
|
23
32
|
|
|
24
33
|
The skill system is the value flywheel. Every game shipped teaches lessons. Lessons become skills. Next user starts smarter. Your job here is to close that loop for what you just did.
|
|
25
34
|
|