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.
- package/{marketplace.json → .claude-plugin/marketplace.json} +3 -3
- package/.claude-plugin/plugin.json +52 -11
- package/.codex-plugin/plugin.json +69 -8
- package/.cursor-plugin/plugin.json +53 -12
- package/.factory-plugin/plugin.json +20 -0
- package/.opencode/INSTALL.md +75 -0
- package/.opencode/plugins/summer.js +9 -6
- package/AGENTS.md +51 -0
- package/CHANGELOG.md +31 -1
- package/CLAUDE.md +5 -0
- package/GEMINI.md +56 -0
- package/README.md +409 -80
- package/_persona/summer/SKILL.md +113 -0
- package/dist/bin/summer.js +0 -0
- package/dist/commands/assets.d.ts +2 -0
- package/dist/commands/assets.js +189 -0
- package/dist/commands/create.js +55 -17
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +127 -0
- package/dist/commands/install.js +2 -2
- package/dist/commands/list.js +24 -5
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +170 -0
- package/dist/commands/scene.d.ts +2 -0
- package/dist/commands/scene.js +290 -0
- package/dist/lib/banner.js +2 -2
- package/dist/lib/log-filters.d.ts +53 -0
- package/dist/lib/log-filters.js +130 -0
- package/dist/lib/plugin-manifests.test.d.ts +1 -0
- package/dist/lib/plugin-manifests.test.js +134 -0
- package/dist/lib/remote-templates.d.ts +25 -0
- package/dist/lib/remote-templates.js +86 -0
- package/dist/lib/skills-registry.d.ts +283 -3
- package/dist/lib/skills-registry.js +522 -2
- package/dist/lib/telemetry.d.ts +13 -0
- package/dist/lib/telemetry.js +63 -0
- package/dist/lib/with-engine.d.ts +14 -0
- package/dist/lib/with-engine.js +109 -0
- package/dist/mcp/tools/asset-tools.js +12 -13
- package/dist/mcp/tools/debug-tools.js +32 -9
- package/dist/mcp/tools/generate-tools.js +101 -0
- package/dist/mcp/tools/with-engine.js +4 -0
- package/gemini-extension.json +2 -2
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +11 -5
- package/skills/2d-assets/character-portrait/SKILL.md +196 -0
- package/skills/2d-assets/concept-art/SKILL.md +152 -0
- package/skills/2d-assets/pixel-art/SKILL.md +180 -0
- package/skills/2d-assets/skybox-panorama/SKILL.md +207 -0
- package/skills/2d-assets/sprite-sheet/SKILL.md +239 -0
- package/skills/2d-assets/tileable-texture/SKILL.md +205 -0
- package/skills/2d-assets/ui-graphics/SKILL.md +210 -0
- package/skills/3d-assets/character-model/SKILL.md +225 -0
- package/skills/3d-assets/environment-kit/SKILL.md +164 -0
- package/skills/3d-assets/organic-model/SKILL.md +192 -0
- package/skills/3d-assets/prop-model/SKILL.md +133 -0
- package/skills/3d-assets/vehicle-model/SKILL.md +202 -0
- package/skills/README.md +1 -1
- package/skills/_meta/skill-create/SKILL.md +1 -1
- package/skills/_meta/skill-improve/SKILL.md +1 -1
- package/skills/_meta/skill-test/SKILL.md +1 -1
- package/skills/_meta/using-summer/SKILL.md +130 -0
- package/skills/ai-and-npcs/design-npc/SKILL.md +3 -1
- package/skills/animation/animation-tree/SKILL.md +260 -0
- package/skills/animation/facial-and-lipsync/SKILL.md +282 -0
- package/skills/animation/generate-motion/SKILL.md +129 -0
- package/skills/animation/procedural-animation/SKILL.md +234 -0
- package/skills/animation/retarget/SKILL.md +172 -0
- package/skills/asset-pipeline/asset-strategy/SKILL.md +102 -212
- package/skills/audio/adaptive-music/SKILL.md +326 -0
- package/skills/audio/ambient-bed/SKILL.md +278 -0
- package/skills/audio/music-track/SKILL.md +268 -0
- package/skills/audio/sound-effect/SKILL.md +237 -0
- package/skills/audio/voice-line/SKILL.md +253 -0
- package/skills/catalog.yaml +266 -5
- package/skills/character-controllers/fps-controller/SKILL.md +1 -1
- package/skills/debugging/debug/SKILL.md +102 -82
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
- package/skills/scene-and-project/browse-templates/SKILL.md +105 -0
- package/skills/scene-and-project/make-game/SKILL.md +159 -346
- package/skills/scene-and-project/new-project/SKILL.md +106 -0
- package/skills/scene-and-project/scene-composition/SKILL.md +1 -1
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
- package/skills/video/animated-loop/SKILL.md +200 -0
- package/skills/video/cinematic-cutscene/SKILL.md +204 -0
- package/skills/video/trailer-shot/SKILL.md +203 -0
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +28 -0
- package/skills/visual-effects/_building-blocks/noise-3d-fbm.gdshaderinc +45 -0
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +51 -0
- package/skills/visual-effects/{vfx → game-feel}/SKILL.md +1 -1
- package/skills/visual-effects/recipes/dissolve/SKILL.md +349 -0
- package/skills/visual-effects/recipes/fire/SKILL.md +387 -0
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +330 -0
- package/skills/visual-effects/recipes/lightning/SKILL.md +372 -0
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +377 -0
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +326 -0
- package/skills/visual-effects/recipes/smoke/SKILL.md +381 -0
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +340 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adaptive-music
|
|
3
|
+
description: Use when wiring music stems to game state — combat / explore / boss / tension crossfades on a shared bus. Pairs generated stems with a state machine and AudioBus structure. Trigger on "adaptive music", "dynamic music", "music changes when combat starts", "layered music", "vertical music", "wire stems".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: audio
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Glob Write Edit summer_generate_audio summer_search_assets summer_import_from_url summer_add_node summer_set_prop summer_connect_signal summer_inspect_node summer_get_scene_tree summer_project_setting
|
|
9
|
+
paths: ["audio/music/**", "scripts/audio/**", "**/*.tscn", "default_bus_layout.tres"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /adaptive-music — Wire Stems to Game State
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
This is the **wiring** skill, not the generation skill. The job is to take 3–5 music stems (or full tracks) and connect them to a game state machine so the music shifts as the player's situation changes — exploration → tension → combat → victory → exploration — with crossfades, ducking, and bus routing the player never consciously notices but always feels.
|
|
17
|
+
|
|
18
|
+
This skill assumes the stems already exist. If they don't, run `/music-track` first for each state, or generate matching stems (same tempo, same key, same bar count) so they layer/crossfade cleanly.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- The audio bible specifies a dynamic music model (layered stems / vertical re-orchestration / horizontal sequencing).
|
|
23
|
+
- You have at least two music tracks for two different states and want the engine to switch between them.
|
|
24
|
+
- Player-state-driven crossfades, not random shuffling.
|
|
25
|
+
|
|
26
|
+
## When NOT to use
|
|
27
|
+
|
|
28
|
+
- Generating the stems themselves → `/music-track`.
|
|
29
|
+
- Single static loop with no state changes → `/music-track` and an `AudioStreamPlayer` is enough.
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
### 1. Read the audio bible's dynamic music plan
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Read .summer/audio-bible.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Quote the plan back. Example:
|
|
40
|
+
|
|
41
|
+
> Bible plan: Layered stems. States CALM / TENSION / COMBAT / VICTORY. CALM → TENSION on enemy detect (2s crossfade). TENSION → COMBAT on first damage (instant drum stem). COMBAT → VICTORY on last enemy dies (drum cuts, brass swell). VICTORY → CALM after 4s. Music ducks `-6 dB` on dialogue.
|
|
42
|
+
|
|
43
|
+
If the plan is absent, run `/audio-direction` step 6 first.
|
|
44
|
+
|
|
45
|
+
### 2. Pick the wiring model
|
|
46
|
+
|
|
47
|
+
| Model | When | Implementation |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **Horizontal sequencing** | 3–5 full tracks, one per state, crossfade | One `AudioStreamPlayer` per track on the `Music` bus, tween volumes on state change |
|
|
50
|
+
| **Layered stems** | One base loop + drum/brass/strings stems that turn on with intensity | All stems play simultaneously at silence; mute/unmute via volume tween |
|
|
51
|
+
| **Vertical re-orchestration** | Same melody, different orchestration per state, snap on bar | All variants play simultaneously muted; swap on next bar boundary |
|
|
52
|
+
| **Procedural / generative** | Rules generate music in real-time | Beyond this skill; needs a custom system |
|
|
53
|
+
|
|
54
|
+
For 90% of indie games, **horizontal sequencing** is correct. It's cheaper to author, easier to swap, and players don't notice the difference.
|
|
55
|
+
|
|
56
|
+
### 3. Confirm stems exist (or generate them)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
summer_search_assets(query="music", filter={ kind: "audio" })
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You need (typical case):
|
|
63
|
+
|
|
64
|
+
- `audio/music/calm.mp3` — exploration loop, sparse
|
|
65
|
+
- `audio/music/tension.mp3` — same tempo/key, denser, no drums
|
|
66
|
+
- `audio/music/combat.mp3` — same tempo/key, full drums, urgent
|
|
67
|
+
- (optional) `audio/music/boss.mp3` — distinct from combat
|
|
68
|
+
- (optional) `audio/music/victory.mp3` — short linear sting (4–8s)
|
|
69
|
+
|
|
70
|
+
If any are missing, run `/music-track` for each. Critically: **same BPM, same key, same time signature, same bar length** so they overlap cleanly.
|
|
71
|
+
|
|
72
|
+
### 4. Verify / extend the bus layout
|
|
73
|
+
|
|
74
|
+
The Music bus must exist and route to Master. If `/audio-direction` ran, it does. Verify:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Read default_bus_layout.tres # or wherever the project stores it
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You want this structure:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Master
|
|
84
|
+
├── Music (-8 dB; ducks -6 dB when Voice is active)
|
|
85
|
+
├── SFX (0 dB; routes to Reverb buses)
|
|
86
|
+
├── UI (-3 dB)
|
|
87
|
+
├── Voice (0 dB)
|
|
88
|
+
├── Ambient (-12 dB)
|
|
89
|
+
├── Reverb_Small
|
|
90
|
+
├── Reverb_Large
|
|
91
|
+
└── Reverb_Outdoor
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If `Music` bus is missing, run `/audio-direction` step 8.
|
|
95
|
+
|
|
96
|
+
For ducking, add a sidechain compressor on the `Music` bus with the `Voice` bus as the sidechain source. Or implement ducking in code (simpler — see step 7).
|
|
97
|
+
|
|
98
|
+
### 5. Build the player rig
|
|
99
|
+
|
|
100
|
+
For horizontal sequencing, instantiate one `AudioStreamPlayer` per state, all on the Music bus, all autoplay, all looping, all starting at `-60 dB` except the active state:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Calm")
|
|
104
|
+
summer_set_prop(path="/root/Game/Music/Calm", property="stream", value="res://audio/music/calm.mp3")
|
|
105
|
+
summer_set_prop(path="/root/Game/Music/Calm", property="bus", value="Music")
|
|
106
|
+
summer_set_prop(path="/root/Game/Music/Calm", property="volume_db", value=0.0)
|
|
107
|
+
summer_set_prop(path="/root/Game/Music/Calm", property="autoplay", value=true)
|
|
108
|
+
|
|
109
|
+
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Tension")
|
|
110
|
+
summer_set_prop(path="/root/Game/Music/Tension", property="stream", value="res://audio/music/tension.mp3")
|
|
111
|
+
summer_set_prop(path="/root/Game/Music/Tension", property="bus", value="Music")
|
|
112
|
+
summer_set_prop(path="/root/Game/Music/Tension", property="volume_db", value=-60.0)
|
|
113
|
+
summer_set_prop(path="/root/Game/Music/Tension", property="autoplay", value=true)
|
|
114
|
+
|
|
115
|
+
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Combat")
|
|
116
|
+
summer_set_prop(path="/root/Game/Music/Combat", property="stream", value="res://audio/music/combat.mp3")
|
|
117
|
+
summer_set_prop(path="/root/Game/Music/Combat", property="bus", value="Music")
|
|
118
|
+
summer_set_prop(path="/root/Game/Music/Combat", property="volume_db", value=-60.0)
|
|
119
|
+
summer_set_prop(path="/root/Game/Music/Combat", property="autoplay", value=true)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
All three play simultaneously, in sync, but only one is audible. State transitions tween volumes — the music never restarts, only fades.
|
|
123
|
+
|
|
124
|
+
This is why same BPM / same key / same bar length matter: all three are at the same playhead at all times.
|
|
125
|
+
|
|
126
|
+
### 6. The MusicDirector script
|
|
127
|
+
|
|
128
|
+
```gdscript
|
|
129
|
+
# scripts/audio/MusicDirector.gd
|
|
130
|
+
class_name MusicDirector
|
|
131
|
+
extends Node
|
|
132
|
+
|
|
133
|
+
enum State { CALM, TENSION, COMBAT, VICTORY, BOSS }
|
|
134
|
+
|
|
135
|
+
@export var calm: AudioStreamPlayer
|
|
136
|
+
@export var tension: AudioStreamPlayer
|
|
137
|
+
@export var combat: AudioStreamPlayer
|
|
138
|
+
@export var boss: AudioStreamPlayer
|
|
139
|
+
@export var victory_sting: AudioStreamPlayer # one-shot, not looping
|
|
140
|
+
|
|
141
|
+
@export var crossfade_seconds: float = 2.0
|
|
142
|
+
@export var instant_states: Array[State] = [State.COMBAT] # snap, no fade
|
|
143
|
+
@export var active_db: float = 0.0
|
|
144
|
+
@export var inactive_db: float = -60.0
|
|
145
|
+
|
|
146
|
+
var _state: State = State.CALM
|
|
147
|
+
var _tween: Tween
|
|
148
|
+
|
|
149
|
+
func _ready() -> void:
|
|
150
|
+
_apply_state(_state, true)
|
|
151
|
+
|
|
152
|
+
func set_state(new_state: State) -> void:
|
|
153
|
+
if new_state == _state:
|
|
154
|
+
return
|
|
155
|
+
var instant := new_state in instant_states
|
|
156
|
+
_state = new_state
|
|
157
|
+
_apply_state(new_state, instant)
|
|
158
|
+
if new_state == State.VICTORY:
|
|
159
|
+
victory_sting.play()
|
|
160
|
+
await get_tree().create_timer(4.0).timeout
|
|
161
|
+
set_state(State.CALM)
|
|
162
|
+
|
|
163
|
+
func _apply_state(state: State, instant: bool) -> void:
|
|
164
|
+
if _tween and _tween.is_valid():
|
|
165
|
+
_tween.kill()
|
|
166
|
+
_tween = create_tween().set_parallel(true)
|
|
167
|
+
var dur := 0.0 if instant else crossfade_seconds
|
|
168
|
+
for pair in [
|
|
169
|
+
[calm, state == State.CALM],
|
|
170
|
+
[tension, state == State.TENSION],
|
|
171
|
+
[combat, state == State.COMBAT],
|
|
172
|
+
[boss, state == State.BOSS],
|
|
173
|
+
]:
|
|
174
|
+
var player: AudioStreamPlayer = pair[0]
|
|
175
|
+
var should_be_active: bool = pair[1]
|
|
176
|
+
if player == null:
|
|
177
|
+
continue
|
|
178
|
+
var target_db := active_db if should_be_active else inactive_db
|
|
179
|
+
_tween.tween_property(player, "volume_db", target_db, dur)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Attach it under `/root/Game/Music`, hook the exported player slots to the four players. The director is the single point of truth for music state.
|
|
183
|
+
|
|
184
|
+
### 7. Drive state from gameplay signals
|
|
185
|
+
|
|
186
|
+
Where does `set_state()` get called from? The game state machine. Hook signals from combat / detection / encounter logic:
|
|
187
|
+
|
|
188
|
+
```gdscript
|
|
189
|
+
# scripts/audio/MusicHooks.gd
|
|
190
|
+
extends Node
|
|
191
|
+
@export var director: MusicDirector
|
|
192
|
+
|
|
193
|
+
func _ready() -> void:
|
|
194
|
+
EventBus.enemy_detected_player.connect(_on_tension)
|
|
195
|
+
EventBus.player_took_damage.connect(_on_combat)
|
|
196
|
+
EventBus.encounter_cleared.connect(_on_victory)
|
|
197
|
+
EventBus.boss_started.connect(_on_boss)
|
|
198
|
+
EventBus.boss_ended.connect(_on_calm)
|
|
199
|
+
EventBus.dialogue_started.connect(_on_duck)
|
|
200
|
+
EventBus.dialogue_ended.connect(_on_unduck)
|
|
201
|
+
|
|
202
|
+
func _on_tension() -> void: director.set_state(MusicDirector.State.TENSION)
|
|
203
|
+
func _on_combat() -> void: director.set_state(MusicDirector.State.COMBAT)
|
|
204
|
+
func _on_victory() -> void: director.set_state(MusicDirector.State.VICTORY)
|
|
205
|
+
func _on_boss() -> void: director.set_state(MusicDirector.State.BOSS)
|
|
206
|
+
func _on_calm() -> void: director.set_state(MusicDirector.State.CALM)
|
|
207
|
+
|
|
208
|
+
func _on_duck() -> void:
|
|
209
|
+
var bus_idx := AudioServer.get_bus_index(&"Music")
|
|
210
|
+
create_tween().tween_method(
|
|
211
|
+
func(db: float) -> void: AudioServer.set_bus_volume_db(bus_idx, db),
|
|
212
|
+
AudioServer.get_bus_volume_db(bus_idx), -14.0, 0.3
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
func _on_unduck() -> void:
|
|
216
|
+
var bus_idx := AudioServer.get_bus_index(&"Music")
|
|
217
|
+
create_tween().tween_method(
|
|
218
|
+
func(db: float) -> void: AudioServer.set_bus_volume_db(bus_idx, db),
|
|
219
|
+
AudioServer.get_bus_volume_db(bus_idx), -8.0, 0.5
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If your project doesn't have an `EventBus` autoload yet, add one — adaptive music depends on event-driven signals, not polling.
|
|
224
|
+
|
|
225
|
+
Wire the connections via `summer_connect_signal` so they survive a scene reload.
|
|
226
|
+
|
|
227
|
+
### 8. Layered stems variant (alternative to step 6)
|
|
228
|
+
|
|
229
|
+
If the bible says **layered stems** instead of horizontal sequencing, the rig is the same (multiple `AudioStreamPlayer`s, all looping in sync, all on Music bus) but the volume map is different:
|
|
230
|
+
|
|
231
|
+
| State | Base | Drums | Strings | Brass |
|
|
232
|
+
|---|---|---|---|---|
|
|
233
|
+
| CALM | 0 | -60 | -60 | -60 |
|
|
234
|
+
| TENSION | 0 | -60 | -10 | -60 |
|
|
235
|
+
| COMBAT | 0 | 0 | -3 | -10 |
|
|
236
|
+
| BOSS | -60 | 0 | 0 | 0 |
|
|
237
|
+
|
|
238
|
+
The director method is the same `_apply_state` pattern — just more players and per-player target_db tables.
|
|
239
|
+
|
|
240
|
+
### 9. Bar-locked snap (vertical re-orchestration)
|
|
241
|
+
|
|
242
|
+
For the third model, snapping must happen on a bar boundary (not mid-phrase). All players run in sync; on state change, schedule the snap for the next bar:
|
|
243
|
+
|
|
244
|
+
```gdscript
|
|
245
|
+
const BPM := 128.0
|
|
246
|
+
const BEATS_PER_BAR := 4
|
|
247
|
+
const SECONDS_PER_BAR := (60.0 / BPM) * BEATS_PER_BAR
|
|
248
|
+
|
|
249
|
+
func snap_at_next_bar(callable: Callable) -> void:
|
|
250
|
+
var pos := calm.get_playback_position()
|
|
251
|
+
var into_bar := fmod(pos, SECONDS_PER_BAR)
|
|
252
|
+
var wait := SECONDS_PER_BAR - into_bar
|
|
253
|
+
get_tree().create_timer(wait).timeout.connect(callable)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Use this for boss reveals, story beats — anywhere a snap-on-bar is more dramatic than a crossfade.
|
|
257
|
+
|
|
258
|
+
### 10. Test the transitions
|
|
259
|
+
|
|
260
|
+
Run the game (`summer_play`) and trigger each transition. Listen for:
|
|
261
|
+
|
|
262
|
+
- Crossfade artifacts at `crossfade_seconds = 2.0`. Too fast: fades are choppy. Too slow: state changes feel disconnected from gameplay.
|
|
263
|
+
- Out-of-sync stems. If combat enters and the drums hit on the wrong beat, the stems weren't generated at the same BPM/bar length. Regenerate.
|
|
264
|
+
- Ducking that's too aggressive (music disappears) or too subtle (voice still buried). The `-6 dB` is a starting value; tune in step 7.
|
|
265
|
+
- Victory sting that loops (it shouldn't). Set `victory_sting` to a non-loop AudioStream.
|
|
266
|
+
|
|
267
|
+
### 11. Edge case: scene transitions
|
|
268
|
+
|
|
269
|
+
When the player changes scenes, the `MusicDirector` either:
|
|
270
|
+
|
|
271
|
+
a) **Persists** as an autoload. New scenes call `MusicDirector.set_state()` to take over.
|
|
272
|
+
b) **Per-scene**, fades out the previous before the scene changes (`set_state(CALM)` then crossfade to `-60 dB` on all then change scene).
|
|
273
|
+
|
|
274
|
+
Persistent is preferred for open-world; per-scene for level-based games.
|
|
275
|
+
|
|
276
|
+
## Reference card — bus map
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
Master
|
|
280
|
+
├── Music -8 dB (ducks to -14 dB on Voice)
|
|
281
|
+
│ └── (one AudioStreamPlayer per state, all looping in sync)
|
|
282
|
+
├── SFX 0 dB
|
|
283
|
+
│ └── routes to Reverb_Small / Reverb_Large / Reverb_Outdoor sends
|
|
284
|
+
├── UI -3 dB
|
|
285
|
+
├── Voice 0 dB
|
|
286
|
+
├── Ambient -12 dB
|
|
287
|
+
├── Reverb_Small (return)
|
|
288
|
+
├── Reverb_Large (return)
|
|
289
|
+
└── Reverb_Outdoor (return)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Anti-patterns
|
|
293
|
+
|
|
294
|
+
- **Stems generated at different BPM/key.** They will not crossfade cleanly. Same BPM, same key, same bar count.
|
|
295
|
+
- **Restarting tracks on state change.** Players hear the restart and the magic dies. Always crossfade volumes; never `play()` mid-state.
|
|
296
|
+
- **No autoplay on the silent stems.** They can't fade in to phase if they never started.
|
|
297
|
+
- **Polling state in `_process`.** Use signals. Music director listens to `EventBus`, not to game state every frame.
|
|
298
|
+
- **Hardcoding volumes in `set_state`.** Use the table-driven `_apply_state` so adding a new state doesn't fork the function.
|
|
299
|
+
- **Ducking music to silence on dialogue.** The bible says `-6 dB` (so music drops from `-8` to `-14`), not muted. Voice should sit on a present bed, not a dead one.
|
|
300
|
+
- **Boss music as a louder combat track.** Distinct, named instrument; ideally different tempo or key. The bible's `Boss music is its own track` rule.
|
|
301
|
+
|
|
302
|
+
## Edge cases
|
|
303
|
+
|
|
304
|
+
- **Player triggers TENSION → COMBAT in 0.2s.** The `instant_states = [State.COMBAT]` makes COMBAT snap so it lands on the swing connect. The intermediate fade is wrong — combat must feel immediate.
|
|
305
|
+
- **Player flees combat back to TENSION → CALM.** Use the regular `crossfade_seconds = 2.0`; the de-escalation can breathe.
|
|
306
|
+
- **Multiple enemies aware → one dies → none aware.** The state machine should hysteresis: only fall back to CALM after `no_enemies_aware AND no_recent_damage_for=8s`.
|
|
307
|
+
- **Audio service not ready on `_ready`.** If `AudioServer.get_bus_index("Music")` returns `-1`, the bus layout failed to load. Verify `default_bus_layout.tres`.
|
|
308
|
+
- **Stems generated at 60s but you want a 5-minute combat.** They loop. Crossfade is volume-only, not time-based, so loops are invisible to the director.
|
|
309
|
+
|
|
310
|
+
## Fallback (no MCP)
|
|
311
|
+
|
|
312
|
+
Print the bus layout, the node tree, and the GDScript. User constructs in the Godot editor manually.
|
|
313
|
+
|
|
314
|
+
## Handoff
|
|
315
|
+
|
|
316
|
+
> MusicDirector wired. Calm / Tension / Combat play in sync, only one audible. State driven by `EventBus`. Music ducks `-6 dB` on dialogue. Next:
|
|
317
|
+
> - Run `summer_play` and trigger each transition; listen for sync issues.
|
|
318
|
+
> - If COMBAT lands on the wrong beat, the stems are off-tempo — regenerate via `/music-track` with identical BPM and bar count.
|
|
319
|
+
> - For boss-specific music, generate `boss.mp3` with `/music-track` and the director picks it up automatically.
|
|
320
|
+
|
|
321
|
+
## See also
|
|
322
|
+
|
|
323
|
+
- `audio/audio-direction` — defines the dynamic music plan and bus layout
|
|
324
|
+
- `audio/music-track` — generates the stems
|
|
325
|
+
- `audio/voice-line` — the voice bus that triggers ducking
|
|
326
|
+
- `_shared/godot-version.md` — Godot 4.5 AudioServer and Tween API
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ambient-bed
|
|
3
|
+
description: Use when generating a long looping location ambience — forest at dawn, dungeon air, city street, spaceship hum, cave drip. Non-melodic, low-energy, never draws attention. Wires as a looping AudioStreamPlayer on the Ambient bus with a marked seamless loop. Trigger on "ambient bed", "room tone", "background ambience", "forest ambience", "dungeon air", "make it sound like a cave".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: audio
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Glob Write Edit summer_generate_audio summer_search_assets summer_import_from_url summer_add_node summer_set_prop summer_inspect_node
|
|
9
|
+
paths: ["audio/ambient/**", "**/*.tscn", "**/*.import"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /ambient-bed — Long Looping Location Ambience
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
An ambient bed is the *air* of a place. Forest at dawn, dungeon corridor, spaceship hum, market street, ocean cave. It plays at low energy under everything else — never melodic, never drawing attention, but its absence is immediately felt. Players don't notice an ambient bed; they notice the silence when you forget one.
|
|
17
|
+
|
|
18
|
+
This is **not** music. Use `summer_generate_audio({capability: 'sound_effects'})`, not `{capability: 'music'}`. SFX renders textures; Music renders melody. You want texture.
|
|
19
|
+
|
|
20
|
+
Generated at 15–22 seconds (the SFX duration ceiling) and looped seamlessly with a Godot loop point and optional crossfade.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- Location bed for a level, room, biome, or zone.
|
|
25
|
+
- Background hum / drone under dialogue scenes.
|
|
26
|
+
- Atmosphere layer that complements a music track.
|
|
27
|
+
|
|
28
|
+
## When NOT to use
|
|
29
|
+
|
|
30
|
+
- Short event SFX (<5s) → `audio/sound-effect`.
|
|
31
|
+
- Music with melody / chord progression → `audio/music-track`.
|
|
32
|
+
- Voice barks → `audio/voice-line`.
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
### 1. Read the audio bible
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Read .summer/audio-bible.md
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The bible's `Ambient layer` SFX class names the character — usually "continuous, low presence, no looping melody". Honor it. The mix rules sit ambient at the bus level the bible defined (typically `Ambient` bus around `-12 dB`).
|
|
43
|
+
|
|
44
|
+
### 2. Identify the location
|
|
45
|
+
|
|
46
|
+
Ask if it's not pinned by context:
|
|
47
|
+
|
|
48
|
+
> Which location's ambience? Be specific — "forest at dawn with light wind and distant birds" beats "outdoors". The model needs the time of day, the weather, and 2–3 sound sources.
|
|
49
|
+
|
|
50
|
+
### 3. Build the prompt — environment + time + weather + 2–3 sources + density + duration
|
|
51
|
+
|
|
52
|
+
ElevenLabs SFX renders ambient textures well when the prompt names sources, not vibes.
|
|
53
|
+
|
|
54
|
+
Prompts that work:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
forest at dawn, gentle wind through leaves, distant bird calls every 6 seconds,
|
|
58
|
+
soft creaking branches, no music, no human sound, 22s seamless loop
|
|
59
|
+
|
|
60
|
+
stone dungeon corridor, distant water drip, low rumble, faint draft, no music,
|
|
61
|
+
no creature sound, 22s seamless loop
|
|
62
|
+
|
|
63
|
+
spaceship corridor, low hum at 60Hz, occasional electronic chirp, ventilation
|
|
64
|
+
whoosh, no human voices, 22s seamless loop
|
|
65
|
+
|
|
66
|
+
medieval marketplace at noon, distant chatter, footsteps on stone, occasional
|
|
67
|
+
horse, distant blacksmith hammer, no recognisable words, 22s loop
|
|
68
|
+
|
|
69
|
+
ocean cave, slow waves entering, low rumble, water drip echoes, no seagulls,
|
|
70
|
+
22s seamless loop
|
|
71
|
+
|
|
72
|
+
night swamp, crickets, distant owl, slow water lapping, occasional frog,
|
|
73
|
+
22s seamless loop
|
|
74
|
+
|
|
75
|
+
abandoned office building, fluorescent hum, distant air conditioning, faint
|
|
76
|
+
paper rustle from a vent, no human sound, 22s loop
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Prompts that DON'T work:
|
|
80
|
+
|
|
81
|
+
| Bad prompt | Failure mode |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `forest ambience` | No time / weather / sources. Returns generic forest with random energy spikes. |
|
|
84
|
+
| `creepy dungeon` | Vibe word, not a description. Model adds horror stings you don't want. |
|
|
85
|
+
| `peaceful background music` | Triggers musical interpretation. Use SFX-shaped prompts. |
|
|
86
|
+
| `loud bustling market` | Energy too high for a bed; use as a one-shot SFX layer instead. |
|
|
87
|
+
| `forest with wolves howling` | Wolf howl is an event, not a bed; promotes itself in the mix. |
|
|
88
|
+
|
|
89
|
+
### 4. Density — keep it low
|
|
90
|
+
|
|
91
|
+
A bed should be **boring**. Ask for sparse events.
|
|
92
|
+
|
|
93
|
+
- `every 6 seconds` rather than `often`.
|
|
94
|
+
- `occasional` not `frequent`.
|
|
95
|
+
- `distant` not `close`.
|
|
96
|
+
- `low` not `loud`.
|
|
97
|
+
|
|
98
|
+
If the user wants a busier bed (market, party), still prevent foreground events: `no recognisable words`, `no individual loud sounds`, `general crowd murmur`.
|
|
99
|
+
|
|
100
|
+
### 5. Generate at the duration ceiling
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
summer_generate_audio(
|
|
104
|
+
capability: "sound_effects",
|
|
105
|
+
prompt: "forest at dawn, gentle wind through leaves, distant bird calls every 6 seconds, soft creaking branches, no music, no human sound, 22s seamless loop",
|
|
106
|
+
durationSeconds: 22
|
|
107
|
+
)
|
|
108
|
+
// Result: { asset: { fileUrl, ... } }
|
|
109
|
+
// Then: summer_import_from_url(url: "<fileUrl>", path: "res://audio/ambient/forest_dawn.wav")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
22s is the SFX ceiling. Always generate at the ceiling for ambient — longer cycle means less recognized repetition.
|
|
113
|
+
|
|
114
|
+
If the model returns a clip with a loud event near the start or end (a sudden bird right at 0:00), regenerate. That kills loop seamlessness.
|
|
115
|
+
|
|
116
|
+
### 6. Mark the seamless loop in Godot's import dock
|
|
117
|
+
|
|
118
|
+
This step is what makes the bed sound like a place rather than a 22-second sample.
|
|
119
|
+
|
|
120
|
+
In the Godot editor, select the `.wav`, open the Import dock:
|
|
121
|
+
|
|
122
|
+
| Setting | Value |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Loop | On |
|
|
125
|
+
| Loop Mode | Forward |
|
|
126
|
+
| Loop Begin | 0 (or 0.5 if there's a fade-in artifact) |
|
|
127
|
+
|
|
128
|
+
The `.import` file:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
[params]
|
|
132
|
+
loop = true
|
|
133
|
+
loop_offset = 0.0
|
|
134
|
+
loop_mode = 1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
After changing, click `Reimport`.
|
|
138
|
+
|
|
139
|
+
### 7. Crossfade-loop pattern for seamlessness
|
|
140
|
+
|
|
141
|
+
ElevenLabs SFX returns are not gapless. Even with `loop = true`, you'll hear a tiny click on the loop point. Fix it with a two-player crossfade in code:
|
|
142
|
+
|
|
143
|
+
```gdscript
|
|
144
|
+
# scripts/audio/AmbientBed.gd
|
|
145
|
+
extends Node
|
|
146
|
+
@export var stream: AudioStream
|
|
147
|
+
@export var fade_seconds: float = 1.5
|
|
148
|
+
@export var bus: StringName = &"Ambient"
|
|
149
|
+
@export var volume_db: float = -12.0
|
|
150
|
+
var _a: AudioStreamPlayer
|
|
151
|
+
var _b: AudioStreamPlayer
|
|
152
|
+
var _active: AudioStreamPlayer
|
|
153
|
+
|
|
154
|
+
func _ready() -> void:
|
|
155
|
+
_a = _make_player()
|
|
156
|
+
_b = _make_player()
|
|
157
|
+
_active = _a
|
|
158
|
+
_active.play()
|
|
159
|
+
_schedule_next()
|
|
160
|
+
|
|
161
|
+
func _make_player() -> AudioStreamPlayer:
|
|
162
|
+
var p := AudioStreamPlayer.new()
|
|
163
|
+
p.stream = stream
|
|
164
|
+
p.bus = bus
|
|
165
|
+
p.volume_db = volume_db
|
|
166
|
+
add_child(p)
|
|
167
|
+
return p
|
|
168
|
+
|
|
169
|
+
func _schedule_next() -> void:
|
|
170
|
+
var len := stream.get_length()
|
|
171
|
+
var t := get_tree().create_timer(len - fade_seconds)
|
|
172
|
+
t.timeout.connect(_crossfade)
|
|
173
|
+
|
|
174
|
+
func _crossfade() -> void:
|
|
175
|
+
var next := _b if _active == _a else _a
|
|
176
|
+
next.volume_db = -60.0
|
|
177
|
+
next.play()
|
|
178
|
+
var tw := create_tween().set_parallel(true)
|
|
179
|
+
tw.tween_property(next, "volume_db", volume_db, fade_seconds)
|
|
180
|
+
tw.tween_property(_active, "volume_db", -60.0, fade_seconds)
|
|
181
|
+
tw.chain().tween_callback(func() -> void: _active.stop())
|
|
182
|
+
_active = next
|
|
183
|
+
_schedule_next()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This eliminates the click and adds variation through phase-overlap (the loop overlaps itself by `fade_seconds` so the bed never repeats identically).
|
|
187
|
+
|
|
188
|
+
### 8. Wire the bed as `AudioStreamPlayer` on the Ambient bus
|
|
189
|
+
|
|
190
|
+
For most beds, non-positional is correct (the player is *in* the place; the bed isn't an emitter):
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
summer_add_node(parentPath="/root/Game/Level", type="AudioStreamPlayer", name="AmbientBed")
|
|
194
|
+
summer_set_prop(path="/root/Game/Level/AmbientBed", property="stream", value="res://audio/ambient/forest_dawn.wav")
|
|
195
|
+
summer_set_prop(path="/root/Game/Level/AmbientBed", property="bus", value="Ambient")
|
|
196
|
+
summer_set_prop(path="/root/Game/Level/AmbientBed", property="volume_db", value=-12.0)
|
|
197
|
+
summer_set_prop(path="/root/Game/Level/AmbientBed", property="autoplay", value=true)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or attach the script in step 7 to a Node, set its `stream` to the imported audio, and skip the `AudioStreamPlayer` node — the script provides its own.
|
|
201
|
+
|
|
202
|
+
For positional beds (ambience emitting from a fountain, a campfire, a vent) use `AudioStreamPlayer3D` with attenuation. The bible's `3D attenuation: inverse-square, 25m` rule applies.
|
|
203
|
+
|
|
204
|
+
### 9. Layer multiple beds for depth
|
|
205
|
+
|
|
206
|
+
Real places have layered ambiences. A forest at dawn might be:
|
|
207
|
+
|
|
208
|
+
- Bed A (loud): wind through leaves loop, `-12 dB`
|
|
209
|
+
- Bed B (medium): distant bird calls loop, `-18 dB`
|
|
210
|
+
- Bed C (occasional): one-shot SFX of a single bird call, fired by a Timer with random 8–20s interval
|
|
211
|
+
|
|
212
|
+
This is more expensive (3 generations) but produces a bed that doesn't sound like a 22-second loop. Reserve for hub locations or extended scenes.
|
|
213
|
+
|
|
214
|
+
## Reference card — prompts by environment
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
Forest dawn: forest at dawn, gentle wind, distant birds every 6s,
|
|
218
|
+
creaking branches, 22s loop
|
|
219
|
+
Forest night: forest at night, crickets, distant owl every 10s, soft
|
|
220
|
+
breeze, no music, 22s loop
|
|
221
|
+
Stone dungeon: stone dungeon, distant water drip every 4s, low rumble,
|
|
222
|
+
faint draft, no creatures, 22s loop
|
|
223
|
+
Cave: wet cave, water drip echoes, low air movement, distant
|
|
224
|
+
rumble, 22s loop
|
|
225
|
+
Spaceship: spaceship corridor, 60Hz hum, occasional electronic chirp,
|
|
226
|
+
ventilation whoosh, no voices, 22s loop
|
|
227
|
+
Marketplace: medieval marketplace at noon, distant chatter no recognisable
|
|
228
|
+
words, footsteps on stone, occasional horse, distant
|
|
229
|
+
blacksmith, 22s loop
|
|
230
|
+
Tavern: medieval tavern interior, low chatter, fireplace crackle,
|
|
231
|
+
distant lute, occasional mug clink, 22s loop
|
|
232
|
+
Ocean cave: ocean cave, slow waves, low rumble, water drip echoes,
|
|
233
|
+
no seagulls, 22s loop
|
|
234
|
+
Office: empty office at night, fluorescent hum, distant AC,
|
|
235
|
+
faint paper rustle, 22s loop
|
|
236
|
+
Snowy plain: snowy plain in wind, low howling wind, faint distant
|
|
237
|
+
branches, no music, no creatures, 22s loop
|
|
238
|
+
Underwater: underwater, low rumble, distant bubbles, soft mid hum,
|
|
239
|
+
no marine life, 22s loop
|
|
240
|
+
Boss arena: vast stone arena, low ominous drone, distant rumble,
|
|
241
|
+
occasional debris fall, no melody, 22s loop
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Anti-patterns
|
|
245
|
+
|
|
246
|
+
- **Using `summer_generate_audio({capability: 'music'})` for an ambient bed.** Music model adds melody. You don't want melody.
|
|
247
|
+
- **Foreground events in the bed.** A wolf howl, an explosion, a clear word — these promote themselves and the bed becomes annoying.
|
|
248
|
+
- **Bed louder than `-10 dB` on its bus.** Beds belong under everything; default is `-12 dB` to `-15 dB`.
|
|
249
|
+
- **No loop point set.** The clip plays once and silence follows.
|
|
250
|
+
- **Single-clip looping without crossfade.** Audible click every 22s.
|
|
251
|
+
- **Same bed everywhere.** Different rooms / biomes need different beds. The transition (room A → room B) is what sells the world.
|
|
252
|
+
|
|
253
|
+
## Edge cases
|
|
254
|
+
|
|
255
|
+
- **Bed too busy on regen.** Add `sparse`, `quiet`, `low`, `distant` to the prompt.
|
|
256
|
+
- **Bed has musical content you didn't ask for.** Add `no music, no melody, no chords`.
|
|
257
|
+
- **Bed has voices when you wanted abstract.** Add `no voices, no recognisable words`.
|
|
258
|
+
- **Loop click after import dock setup.** Use the crossfade script (step 7).
|
|
259
|
+
- **Need positional bed (a campfire crackle).** Use `AudioStreamPlayer3D` with `max_distance` matching the campfire's reasonable hearing radius (4–8m typical).
|
|
260
|
+
- **Multi-room transitions.** Use Areas; on player enter, fade in destination bed and fade out source bed over 1.5s.
|
|
261
|
+
|
|
262
|
+
## Fallback (no MCP)
|
|
263
|
+
|
|
264
|
+
Print the SFX prompt and the import dock settings. User runs via the Summer dashboard and imports manually.
|
|
265
|
+
|
|
266
|
+
## Handoff
|
|
267
|
+
|
|
268
|
+
> Bed `forest_dawn.wav` wired to `Level/AmbientBed`, looping with crossfade. Next:
|
|
269
|
+
> - Generate the night counterpart for time-of-day transitions.
|
|
270
|
+
> - Add a layered occasional bird-call SFX with a randomized Timer for depth.
|
|
271
|
+
> - For multi-room transitions, gate beds with Area3D triggers and fade with the same crossfade pattern.
|
|
272
|
+
|
|
273
|
+
## See also
|
|
274
|
+
|
|
275
|
+
- `audio/audio-direction` — defines the Ambient layer class
|
|
276
|
+
- `audio/sound-effect` — short event SFX layered over the bed
|
|
277
|
+
- `audio/music-track` — melodic music layered over the bed
|
|
278
|
+
- `audio/adaptive-music` — state-driven crossfades use the same pattern
|