add ci build
/ build (push) Failing after 2m12s

This commit is contained in:
2026-04-21 15:32:59 +02:00
parent edc40f9008
commit 98eaa77175
6 changed files with 446 additions and 12 deletions
+146
View File
@@ -0,0 +1,146 @@
# Godot MCP Pro - AI Assistant Instructions
You have access to the Godot MCP Pro toolset for building and testing Godot games through the editor. Follow these rules carefully.
## Critical: Editor vs Runtime Tools
Tools are split into two categories. **Using a runtime tool without starting the game will always fail.**
### Editor Tools (always available)
These work on the currently open scene in the Godot editor:
- **Scene**: `get_scene_tree`, `create_scene`, `open_scene`, `save_scene`, `delete_scene`, `add_scene_instance`, `get_scene_file_content`, `get_scene_exports`
- **Nodes**: `add_node`, `delete_node`, `duplicate_node`, `move_node`, `rename_node`, `update_property`, `get_node_properties`, `add_resource`, `set_anchor_preset`, `connect_signal`, `disconnect_signal`, `get_node_groups`, `set_node_groups`, `find_nodes_in_group`
- **Scripts**: `create_script`, `read_script`, `edit_script`, `validate_script`, `attach_script`, `get_open_scripts`, `list_scripts`
- **Project**: `get_project_info`, `get_project_settings`, `set_project_setting`, `get_project_statistics`, `get_filesystem_tree`, `get_input_actions`, `set_input_action`
- **Editor**: `execute_editor_script`, `get_editor_errors`, `get_output_log`, `get_editor_screenshot`, `clear_output`, `reload_plugin`, `reload_project`
- **Resources**: `create_resource`, `read_resource`, `edit_resource`, `get_resource_preview`
- **Batch**: `batch_add_nodes`, `batch_set_property`, `find_nodes_by_type`, `find_signal_connections`, `find_node_references`, `get_scene_dependencies`, `cross_scene_set_property`
- **3D**: `add_mesh_instance`, `setup_environment`, `setup_lighting`, `setup_camera_3d`, `setup_collision`, `setup_physics_body`, `set_material_3d`, `add_raycast`, `add_gridmap`
- **Animation**: `create_animation`, `add_animation_track`, `set_animation_keyframe`, `list_animations`, `get_animation_info`, `remove_animation`
- **Animation Tree**: `create_animation_tree`, `get_animation_tree_structure`, `add_state_machine_state`, `add_state_machine_transition`, `remove_state_machine_state`, `remove_state_machine_transition`, `set_blend_tree_node`, `set_tree_parameter`
- **Audio**: `add_audio_player`, `add_audio_bus`, `add_audio_bus_effect`, `set_audio_bus`, `get_audio_bus_layout`, `get_audio_info`
- **Navigation**: `setup_navigation_region`, `setup_navigation_agent`, `bake_navigation_mesh`, `set_navigation_layers`, `get_navigation_info`
- **Particles**: `create_particles`, `set_particle_material`, `set_particle_color_gradient`, `apply_particle_preset`, `get_particle_info`
- **Physics**: `get_physics_layers`, `set_physics_layers`, `get_collision_info`
- **Shader**: `create_shader`, `read_shader`, `edit_shader`, `assign_shader_material`, `get_shader_params`, `set_shader_param`
- **Theme**: `create_theme`, `get_theme_info`, `set_theme_color`, `set_theme_font_size`, `set_theme_constant`, `set_theme_stylebox`
- **Tilemap**: `tilemap_get_info`, `tilemap_set_cell`, `tilemap_get_cell`, `tilemap_fill_rect`, `tilemap_clear`, `tilemap_get_used_cells`
- **Export**: `list_export_presets`, `get_export_info`, `export_project`
- **Analysis**: `analyze_scene_complexity`, `analyze_signal_flow`, `detect_circular_dependencies`, `find_unused_resources`, `get_performance_monitors`, `search_files`, `search_in_files`, `find_script_references`
- **Profiling**: `get_editor_performance`
### Runtime Tools (require `play_scene` first)
You MUST call `play_scene` before using any of these. They interact with the running game:
- **Game State**: `get_game_scene_tree`, `get_game_node_properties`, `set_game_node_property`, `execute_game_script`, `get_game_screenshot`, `get_autoload`, `find_nodes_by_script`
- **Input Simulation**: `simulate_key`, `simulate_mouse_click`, `simulate_mouse_move`, `simulate_action`, `simulate_sequence`
- **Capture/Recording**: `capture_frames`, `record_frames`, `monitor_properties`, `start_recording`, `stop_recording`, `replay_recording`, `batch_get_properties`
- **UI Interaction**: `find_ui_elements`, `click_button_by_text`, `wait_for_node`, `find_nearby_nodes`, `navigate_to`, `move_to`
- **Testing**: `run_test_scenario`, `assert_node_state`, `assert_screen_text`, `run_stress_test`, `get_test_report`
- **Screenshots**: `get_game_screenshot`, `compare_screenshots`
- **Control**: `play_scene`, `stop_scene`
## Workflow Patterns
### Building a scene from scratch
1. `create_scene` or `open_scene`
2. Use `add_node` or `batch_add_nodes` to add nodes
3. `create_script` + `attach_script` for behavior
4. `save_scene`
### Testing gameplay
1. Build scene with editor tools (above)
2. `play_scene` to start the game
3. Use `simulate_key`/`simulate_mouse_click` for input
4. `get_game_screenshot` or `capture_frames` to observe results
5. `stop_scene` when done
### Inspecting a project
1. `get_project_info` for overview
2. `get_scene_tree` for current scene structure
3. `read_script` to read code
4. `get_node_properties` for specific node details
### Migrating code properties to inspector
When a script hardcodes visual properties (colors, sizes, positions, theme overrides) that should be in the inspector:
1. `read_script` to find hardcoded property assignments (e.g. `modulate = Color(...)`, `add_theme_color_override(...)`)
2. `get_node_properties` to see current inspector values
3. `update_property` to set the same values as node properties in the inspector
4. `edit_script` to remove the hardcoded lines from the script
5. `save_scene` to persist the inspector changes
6. `validate_script` to verify the script still works
## Formatting Rules
### execute_editor_script
The `code` parameter must be valid GDScript. Use `_mcp_print(value)` to return output.
```
# Correct
_mcp_print("hello")
# Correct - multi-line
var nodes = []
for child in EditorInterface.get_edited_scene_root().get_children():
nodes.append(child.name)
_mcp_print(str(nodes))
```
### execute_game_script
Same as above but runs inside the running game. Additional rules:
- No nested functions (`func` inside `func` is invalid GDScript)
- Use `.get("property")` instead of `.property` for safe access
- Runs in a temporary node — use `get_tree()` to access the scene tree
### batch_add_nodes
Pass an array of node definitions. Nodes are processed in order, so earlier nodes can be parents for later ones:
```json
{
"nodes": [
{"type": "Node2D", "name": "Container", "parent_path": "."},
{"type": "Sprite2D", "name": "Icon", "parent_path": "Container"},
{"type": "Label", "name": "Title", "parent_path": "Container", "properties": {"text": "Hello"}}
]
}
```
## Best Practices
1. **Prefer inspector properties over code** — When changing visual properties (colors, sizes, theme overrides, transforms, etc.), use `update_property` to set them directly on the node. This keeps values visible in the Godot inspector and easy to tweak. Only use GDScript when the property isn't available in the inspector or needs to be dynamic at runtime.
## Common Pitfalls
1. **Never edit project.godot directly** — Use `set_project_setting` instead. The Godot editor overwrites the file.
2. **GDScript type inference** — Use explicit type annotations in for-loops: `for item: String in array` instead of `for item in array`.
3. **Reload after script changes** — After `create_script`, call `reload_project` if the script doesn't take effect.
4. **Property values as strings** — Properties like position accept string format: `"Vector2(100, 200)"`, `"Color(1, 0, 0, 1)"`.
5. **simulate_key duration** — Use short durations (0.3-0.5s) for precise movement. Integer seconds (1, 2, 3) cause overshooting.
6. **compare_screenshots** — Pass file paths (`user://screenshot.png`), not base64 data.
## CLI Mode (Alternative to MCP Tools)
If MCP tools are unavailable or you have a terminal/bash tool, you can control Godot via the CLI.
The CLI requires the server to be built first (`node build/setup.js install` in the server directory).
```bash
# Discover available command groups
node /path/to/server/build/cli.js --help
# Discover commands in a group
node /path/to/server/build/cli.js scene --help
# Discover options for a specific command
node /path/to/server/build/cli.js node add --help
# Execute commands
node /path/to/server/build/cli.js project info
node /path/to/server/build/cli.js scene tree
node /path/to/server/build/cli.js node add --type CharacterBody3D --name Player --parent /root/Main
node /path/to/server/build/cli.js script read --path res://player.gd
node /path/to/server/build/cli.js scene play
node /path/to/server/build/cli.js input key --key W --duration 0.5
node /path/to/server/build/cli.js runtime tree
```
**Command groups**: project, scene, node, script, editor, input, runtime
Always start by running `--help` to discover available commands. Use the CLI when MCP tools are not loaded or when you need to reduce context usage.
+34
View File
@@ -0,0 +1,34 @@
on: [push]
jobs:
build:
runs-on: ubuntu-latest
container:
image: barichello/godot-ci:4.6.2
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get build number
run: echo "BUILD_NUM=$(git rev-list --count HEAD)" >> $GITHUB_ENV
- name: Prepare build dir
run: mkdir -p build
- name: Build Windows
run: godot --headless --export-release "Windows Desktop" build/spacel.exe
- name: Build Linux
run: godot --headless --export-release "Linux" build/spacel.x86_64
- name: Build Android
run: godot --headless --export-release "Android" build/spacel.apk
- name: Build Web
run: godot --headless --export-release "Web" build/spacel.html
- uses: actions/upload-artifact@v4
with:
name: build-${{ env.BUILD_NUM }}
path: build/
+50
View File
@@ -342,3 +342,53 @@ permissions/write_sms=false
permissions/write_social_stream=false permissions/write_social_stream=false
permissions/write_sync_settings=false permissions/write_sync_settings=false
permissions/write_user_dictionary=false permissions/write_user_dictionary=false
[preset.3]
name="Web"
platform="Web"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/build9/spacel.html"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.3.options]
custom_template/debug=""
custom_template/release=""
variant/extensions_support=false
variant/thread_support=false
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1)
threads/emscripten_pool_size=8
threads/godot_pool_size=4
+2 -2
View File
@@ -576,8 +576,8 @@ class AntimatterStar extends RefCounted:
var alpha: float = clamp(life / MAX_LIFE, 0.0, 1.0) var alpha: float = clamp(life / MAX_LIFE, 0.0, 1.0)
var cv := Vector2(x, y) var cv := Vector2(x, y)
# Outer magenta glow # Outer magenta glow
canvas.draw_circle(cv, radius + 8.0, Color(0.75, 0.0, 0.9, p * 0.10 * alpha)) canvas.draw_circle(cv, radius + 8.0, Color(0.75, 0.0, 0.9, p * 0.05 * alpha))
canvas.draw_circle(cv, radius + 3.0, Color(0.6, 0.0, 0.85, p * 0.18 * alpha)) canvas.draw_circle(cv, radius + 3.0, Color(0.6, 0.0, 0.85, p * 0.09 * alpha))
# Dark antimatter core # Dark antimatter core
canvas.draw_circle(cv, radius, Color(0.15, 0.0, 0.22, alpha)) canvas.draw_circle(cv, radius, Color(0.15, 0.0, 0.22, alpha))
# Rotating ring # Rotating ring
+69 -5
View File
@@ -14,8 +14,8 @@ const BH_MAX_COUNT := 8
const WHITE_HOLE_MAX := 2 # cap white holes so they don't pile up const WHITE_HOLE_MAX := 2 # cap white holes so they don't pile up
const COMET_SPAWN_MIN := 3.0 const COMET_SPAWN_MIN := 3.0
const COMET_SPAWN_MAX := 10.0 const COMET_SPAWN_MAX := 10.0
const ANTIMATTER_SPAWN_MIN := 180.0 const ANTIMATTER_SPAWN_MIN := 300.0
const ANTIMATTER_SPAWN_MAX := 360.0 const ANTIMATTER_SPAWN_MAX := 600.0
const DIFFICULTY_RAMP_SECS := 300.0 # full difficulty at 5 minutes const DIFFICULTY_RAMP_SECS := 300.0 # full difficulty at 5 minutes
const OBJECT_WIPE_THRESHOLD := 500 const OBJECT_WIPE_THRESHOLD := 500
const PHYS_DT := 1.0 / 60.0 const PHYS_DT := 1.0 / 60.0
@@ -48,6 +48,10 @@ var comet_timer: float = 0.0
var comet_next: float = 5.0 var comet_next: float = 5.0
var antimatter_timer: float = 0.0 var antimatter_timer: float = 0.0
var antimatter_next: float = 50.0 var antimatter_next: float = 50.0
var antimatter_warn_pending: bool = false
var antimatter_warn_pos: Vector2 = Vector2.ZERO
var antimatter_warn_timer: float = 0.0
const ANTIMATTER_WARN_DURATION := 1.5
var credit_tick_timer: float = 0.0 var credit_tick_timer: float = 0.0
var big_wipe: BigWipe = null var big_wipe: BigWipe = null
@@ -312,11 +316,20 @@ func _tick(dt: float) -> void:
c.init(W, H) c.init(W, H)
comets.append(c) comets.append(c)
antimatter_timer += dt antimatter_timer += dt
var am_max_interval: float = lerp(float(ANTIMATTER_SPAWN_MAX), float(ANTIMATTER_SPAWN_MIN), difficulty) if not antimatter_warn_pending:
var am_max_interval: float = lerp(float(ANTIMATTER_SPAWN_MAX), 120.0, difficulty)
if antimatter_timer >= antimatter_next: if antimatter_timer >= antimatter_next:
antimatter_timer = 0.0 antimatter_timer = 0.0
antimatter_next = randf_range(ANTIMATTER_SPAWN_MIN, am_max_interval) antimatter_next = randf_range(ANTIMATTER_SPAWN_MIN, am_max_interval)
_spawn_antimatter_swarm(W * 0.5 + randf_range(-100, 100), H * 0.5 + randf_range(-100, 100)) antimatter_warn_pos = Vector2(W * 0.5 + randf_range(-100, 100), H * 0.5 + randf_range(-100, 100))
antimatter_warn_pending = true
antimatter_warn_timer = 0.0
SoundManager.play_sfx("antimatter_warn")
if antimatter_warn_pending:
antimatter_warn_timer += dt
if antimatter_warn_timer >= ANTIMATTER_WARN_DURATION:
antimatter_warn_pending = false
_spawn_antimatter_swarm(antimatter_warn_pos.x, antimatter_warn_pos.y)
SoundManager.play_sfx("antimatter_swarm") SoundManager.play_sfx("antimatter_swarm")
var all_dead: bool = true var all_dead: bool = true
for player in players: for player in players:
@@ -414,6 +427,11 @@ func _update_objects(delta: float) -> void:
for comet in comets: for comet in comets:
var c: CosmicObjects.Comet = comet var c: CosmicObjects.Comet = comet
c.update(W, H) c.update(W, H)
for comet in comets:
var c: CosmicObjects.Comet = comet
if c.dead:
SoundManager.play_sfx("comet_whistle")
_fire_music_event("comet_pass")
comets = comets.filter(func(c): return not c.dead) comets = comets.filter(func(c): return not c.dead)
for gal in galaxies: for gal in galaxies:
var g: CosmicObjects.Galaxy = gal var g: CosmicObjects.Galaxy = gal
@@ -426,9 +444,11 @@ func _update_objects(delta: float) -> void:
g.consuming_bh = null g.consuming_bh = null
g.respawning = true g.respawning = true
g.respawn_timer = randf_range(5.0, 10.0) g.respawn_timer = randf_range(5.0, 10.0)
_fire_music_event("galaxy_consumed")
if cbh.consumed >= 12 and not cbh.is_smbh: if cbh.consumed >= 12 and not cbh.is_smbh:
cbh.become_smbh() cbh.become_smbh()
SoundManager.play_sfx("smbh_spawn") SoundManager.play_sfx("smbh_spawn")
_fire_music_event("bh_pulse")
for qsr in quasars: for qsr in quasars:
var q: CosmicObjects.Quasar = qsr var q: CosmicObjects.Quasar = qsr
q.update(delta) q.update(delta)
@@ -459,6 +479,7 @@ func _update_objects(delta: float) -> void:
if q.boost_sound_cd <= 0.0: if q.boost_sound_cd <= 0.0:
q.boost_sound_cd = 1.2 q.boost_sound_cd = 1.2
SoundManager.play_sfx("quasar_boost") SoundManager.play_sfx("quasar_boost")
_fire_music_event("quasar_jet")
quasars = quasars.filter(func(q): return not q.dead) quasars = quasars.filter(func(q): return not q.dead)
_merge_quasars() _merge_quasars()
var new_stars_from_wh: Array = [] var new_stars_from_wh: Array = []
@@ -468,6 +489,8 @@ func _update_objects(delta: float) -> void:
var result: Dictionary = wh.update(delta) var result: Dictionary = wh.update(delta)
new_stars_from_wh.append_array(result["stars"]) new_stars_from_wh.append_array(result["stars"])
new_planets_from_wh.append_array(result["planets"]) new_planets_from_wh.append_array(result["planets"])
if result["stars"].size() > 0 or result["planets"].size() > 0:
_fire_music_event("white_hole_eject")
for player in players: for player in players:
var p: Spaceship = player var p: Spaceship = player
if not p.dead: if not p.dead:
@@ -500,8 +523,11 @@ func _update_objects(delta: float) -> void:
for player in players: for player in players:
var p: Spaceship = player var p: Spaceship = player
if not p.dead: if not p.dead:
var prev_vx := p.vx; var prev_vy := p.vy
var nv: Vector2 = ns.push_if_in_beam(p.x, p.y, p.vx, p.vy) var nv: Vector2 = ns.push_if_in_beam(p.x, p.y, p.vx, p.vy)
p.vx = nv.x; p.vy = nv.y p.vx = nv.x; p.vy = nv.y
if nv.x != prev_vx or nv.y != prev_vy:
_fire_music_event("neutron_beam")
neutron_stars = neutron_stars.filter(func(ns): return not ns.dead) neutron_stars = neutron_stars.filter(func(ns): return not ns.dead)
for am_obj in antimatter: for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj var am: CosmicObjects.Antimatter = am_obj
@@ -537,6 +563,7 @@ func _update_objects(delta: float) -> void:
if b != null: if b != null:
bullets.append(b) bullets.append(b)
SoundManager.play_sfx("enemy_shoot") SoundManager.play_sfx("enemy_shoot")
_update_bh_proximity_audio()
func _apply_gravity_all(_delta: float) -> void: func _apply_gravity_all(_delta: float) -> void:
for bhole in black_holes: for bhole in black_holes:
@@ -578,6 +605,8 @@ func _apply_gravity_all(_delta: float) -> void:
_spawn_spiral_particles(p.x, p.y, bh.x, bh.y, int(p.radius * 3.0), p.color, 5.0) _spawn_spiral_particles(p.x, p.y, bh.x, bh.y, int(p.radius * 3.0), p.color, 5.0)
bh.trigger_flash(p.color, 0.8) bh.trigger_flash(p.color, 0.8)
p.dead = true p.dead = true
SoundManager.play_sfx("planet_impact")
_fire_music_event("planet_captured")
var trigger: bool = bh.on_swallow() var trigger: bool = bh.on_swallow()
if trigger: if trigger:
_trigger_supernova(bh) _trigger_supernova(bh)
@@ -1077,7 +1106,7 @@ func _spawn_universe_partial() -> void:
planets.append(p) planets.append(p)
func _spawn_antimatter_swarm(cx: float, cy: float) -> void: func _spawn_antimatter_swarm(cx: float, cy: float) -> void:
var count: int = randi_range(6, 12) var count: int = randi_range(3, 5)
for _i in count: for _i in count:
var a: float = randf() * TAU var a: float = randf() * TAU
var spd: float = randf_range(1.0, 3.0) var spd: float = randf_range(1.0, 3.0)
@@ -1086,6 +1115,34 @@ func _spawn_antimatter_swarm(cx: float, cy: float) -> void:
antimatter.append(am) antimatter.append(am)
func _fire_music_event(event_name: String) -> void:
if main_node and "music_player" in main_node and main_node.music_player:
main_node.music_player.play_music_event(event_name)
func _update_bh_proximity_audio() -> void:
var mp: Node = main_node.music_player if main_node and "music_player" in main_node else null
if mp == null or not is_playing or players.size() == 0:
if mp: mp.set_bh_proximity(0.0, 1.0, false)
return
var player: Spaceship = players[0]
if player.dead:
mp.set_bh_proximity(0.0, 1.0, false)
return
var max_prox := 0.0
var max_size := 1.0
var any_smbh := false
for bhole in black_holes:
var bh: BlackHole = bhole
if bh.dead or bh.pull_radius <= 0.0: continue
var dx := player.x - bh.x
var dy := player.y - bh.y
var prox: float = clamp(1.0 - sqrt(dx*dx + dy*dy) / bh.pull_radius, 0.0, 1.0)
if prox > max_prox:
max_prox = prox
max_size = bh.radius / 14.0
any_smbh = bh.is_smbh
mp.set_bh_proximity(max_prox, max_size, any_smbh)
func _spawn_explosion_particles(cx: float, cy: float, count: int, col: Color) -> void: func _spawn_explosion_particles(cx: float, cy: float, count: int, col: Color) -> void:
for _i in count: for _i in count:
var a: float = randf() * TAU var a: float = randf() * TAU
@@ -1115,6 +1172,13 @@ func _spawn_spiral_particles(cx: float, cy: float, bh_x: float, bh_y: float,
func _draw() -> void: func _draw() -> void:
draw_rect(get_viewport_rect(), Color("#0a0a14")) draw_rect(get_viewport_rect(), Color("#0a0a14"))
if antimatter_warn_pending:
var warn_t: float = antimatter_warn_timer / ANTIMATTER_WARN_DURATION
var pulse_a: float = abs(sin(antimatter_warn_timer * TAU * 3.5))
var ring_r: float = 25.0 + 8.0 * warn_t
var warn_alpha: float = 0.55 * pulse_a * (1.0 - warn_t * 0.25)
draw_arc(antimatter_warn_pos, ring_r, 0.0, TAU, 20, Color(1.0, 0.2, 0.8, warn_alpha), 2.0)
draw_arc(antimatter_warn_pos, ring_r - 5.0, 0.0, TAU, 14, Color(0.8, 0.0, 1.0, warn_alpha * 0.5), 1.0)
if Settings.nebula_enabled: if Settings.nebula_enabled:
for neb in nebulae: for neb in nebulae:
var n: CosmicObjects.Nebula = neb var n: CosmicObjects.Nebula = neb
+141 -1
View File
@@ -11,7 +11,7 @@ extends Node
# Voice 2 · Bass · Sawtooth (boss2: +detuneter 2. Layer für Schwebung) # Voice 2 · Bass · Sawtooth (boss2: +detuneter 2. Layer für Schwebung)
# Voice 3 · Schlagzeug · Hi-Hat+Kick+Snare (normal/boss1) · # Voice 3 · Schlagzeug · Hi-Hat+Kick+Snare (normal/boss1) ·
# Kick+Floor-Tom+Mid-Tom+Reverse-Whoosh (boss2) # Kick+Floor-Tom+Mid-Tom+Reverse-Whoosh (boss2)
# Voice 4 · Phase-2-Layer · Triangle 1 Oktave über Melodie (nur boss2 Phase 2) # Voice 4 · BH-Bass · Sine Sub-Bass, proximity-driven, beat-pulsed
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
@export var volume_db: float = -8.0 @export var volume_db: float = -8.0
@@ -249,6 +249,16 @@ var _bass_phase := 0.0
# Bass-Zweit-Oszillator (nur boss_mode=2, detunet für Schwebung) # Bass-Zweit-Oszillator (nur boss_mode=2, detunet für Schwebung)
var _bass2_phase := 0.0 var _bass2_phase := 0.0
# Voice 4: BH Proximity Sub-Bass
var _v4_freq: float = 0.0
var _v4_freq_cur: float = 0.0
var _v4_gain: float = 0.0
var _v4_phase: float = 0.0
var _v4_is_smbh: bool = false
# Music event stingers (short pitched overlays)
var _stingers: Array = []
# Globaler Zustand # Globaler Zustand
var _section := 0 var _section := 0
var _chord := 0 var _chord := 0
@@ -284,6 +294,7 @@ func play() -> void:
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 0; _chord = 0 _sample_g = 0; _section = 0; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
_playing = true _playing = true
_player.play() _player.play()
_playback = _player.get_stream_playback() _playback = _player.get_stream_playback()
@@ -297,6 +308,7 @@ func play_boss() -> void:
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 3; _chord = 0 _sample_g = 0; _section = 3; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
if not _playing: if not _playing:
_playing = true _playing = true
_player.play() _player.play()
@@ -313,6 +325,7 @@ func play_boss_leviathan(phase: int = 1) -> void:
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 3; _chord = 0 _sample_g = 0; _section = 3; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
if not _playing: if not _playing:
_playing = true _playing = true
_player.play() _player.play()
@@ -336,6 +349,7 @@ func play_normal() -> void:
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0 _sample_g = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
_load_mel() _load_mel()
func stop_music() -> void: func stop_music() -> void:
@@ -353,6 +367,83 @@ func set_muted(muted: bool) -> void:
if idx != -1: if idx != -1:
AudioServer.set_bus_mute(idx, muted) AudioServer.set_bus_mute(idx, muted)
func set_bh_proximity(proximity: float, size_factor: float, is_smbh: bool) -> void:
_v4_is_smbh = is_smbh
var sf := minf(size_factor * 0.12, 0.55)
_v4_gain = clamp(proximity * sf, 0.0, 0.50)
_v4_freq = _get_v4_freq()
func _get_v4_freq() -> float:
var root_freq: float
match _boss_mode:
2: root_freq = FREQ.get(BOSS2_BASS.get(_chord, "E2"), FREQ["E2"])
1: root_freq = FREQ.get(BOSS_BASS.get(_chord, "A2"), FREQ["A2"])
_: root_freq = FREQ.get(BASS.get(_chord, "A3"), FREQ["A3"])
var base := maxf(30.0, root_freq * 0.25)
if _v4_is_smbh:
return maxf(22.0, base * 0.5)
return base
func play_music_event(event_name: String) -> void:
var root_hz: float = _bass_freq if _bass_freq > 0.0 else FREQ["A3"]
match event_name:
"bh_pulse":
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 0.25, "freq_end": root_hz * 0.125,
"gain": 0.20, "pos": 0, "total": int(1.2 * SAMPLE_RATE),
"attack": int(0.05 * SAMPLE_RATE)})
"neutron_beam":
_stingers.append({"type": "noise", "phase": 0.0,
"freq": 0.0, "freq_end": 0.0,
"gain": 0.14, "pos": 0, "total": int(0.07 * SAMPLE_RATE)})
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 8.0, "freq_end": root_hz * 4.0,
"gain": 0.10, "pos": 0, "total": int(0.10 * SAMPLE_RATE)})
"white_hole_eject":
var arp_table: Dictionary
match _boss_mode:
2: arp_table = BOSS2_ARP
1: arp_table = BOSS_ARP
_: arp_table = ARP
var chord_notes: Array = arp_table.get(_chord, ["A4", "C5", "E5"])
for i in 3:
var note_str: String = chord_notes[i] if i < chord_notes.size() else "A4"
var note_hz: float = FREQ.get(note_str, root_hz * 2.0)
_stingers.append({"type": "triangle", "phase": 0.0,
"freq": note_hz, "freq_end": note_hz,
"gain": 0.10 - i * 0.025,
"pos": -int(i * 0.09 * SAMPLE_RATE),
"total": int(0.28 * SAMPLE_RATE)})
"quasar_jet":
var base_hz := root_hz * 2.0
var scale_mult := [1.0, 1.122, 1.260, 1.498]
for i in 4:
var note_hz: float = base_hz * float(scale_mult[i])
_stingers.append({"type": "sine", "phase": 0.0,
"freq": note_hz, "freq_end": note_hz * 1.04,
"gain": 0.08 + i * 0.01,
"pos": -int(i * 0.11 * SAMPLE_RATE),
"total": int(0.18 * SAMPLE_RATE)})
"galaxy_consumed":
var swell_freqs: Array[float] = [root_hz * 0.5, root_hz * 0.75, root_hz]
for i in 3:
_stingers.append({"type": "sine", "phase": 0.0,
"freq": swell_freqs[i], "freq_end": swell_freqs[i],
"gain": 0.18 - i * 0.04, "pos": 0,
"total": int(1.6 * SAMPLE_RATE),
"attack": int(0.5 * SAMPLE_RATE)})
"planet_captured":
_stingers.append({"type": "noise", "phase": 0.0,
"freq": 0.0, "freq_end": 0.0,
"gain": 0.20, "pos": 0, "total": int(0.10 * SAMPLE_RATE)})
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 2.0, "freq_end": root_hz * 0.5,
"gain": 0.18, "pos": 0, "total": int(0.45 * SAMPLE_RATE)})
"comet_pass":
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 14.0, "freq_end": root_hz * 7.0,
"gain": 0.06, "pos": 0, "total": int(0.16 * SAMPLE_RATE)})
# ── Hauptschleife ───────────────────────────────────────────────────────── # ── Hauptschleife ─────────────────────────────────────────────────────────
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
if _playing: if _playing:
@@ -562,6 +653,54 @@ func _fill_buffer() -> void:
var snare_env := 1.0 - float(snare_pos) / 340.0 var snare_env := 1.0 - float(snare_pos) / 340.0
s += randf_range(-1.0, 1.0) * snare_env * 0.13 s += randf_range(-1.0, 1.0) * snare_env * 0.13
# Voice 4: BH Proximity Sub-Bass — sine, beat-pulsed, frequency glide
var v4_glide := 0.9997
_v4_freq_cur = _v4_freq_cur * v4_glide + _v4_freq * (1.0 - v4_glide)
if _v4_gain > 0.004 and _v4_freq_cur > 0.0:
var beat_samples_v4 := int(_cur_beat * SAMPLE_RATE)
if beat_samples_v4 > 0:
var beat_pos_v4 := _sample_g % beat_samples_v4
var beat_t_v4 := float(beat_pos_v4) / float(beat_samples_v4)
var beat_pulse_v4: float
if beat_t_v4 < 0.06:
beat_pulse_v4 = beat_t_v4 / 0.06
else:
beat_pulse_v4 = 1.0 - (beat_t_v4 - 0.06) / 0.94 * 0.55
_v4_phase += _v4_freq_cur / SAMPLE_RATE
s += sin(_v4_phase * TAU) * _v4_gain * (0.45 + 0.55 * beat_pulse_v4)
# Stingers: short pitched musical overlays for celestial events
for st in _stingers:
st["pos"] = int(st["pos"]) + 1
var sp: int = int(st["pos"])
if sp <= 0:
continue
var stotal: int = int(st["total"])
if sp > stotal:
continue
var t_st := float(sp) / float(stotal)
var atk: int = int(st.get("attack", int(0.008 * SAMPLE_RATE)))
var env_st: float
if sp < atk:
env_st = float(sp) / float(atk)
else:
var remaining := stotal - atk
if remaining > 0:
env_st = maxf(0.0, 1.0 - float(sp - atk) / float(remaining))
else:
env_st = 0.0
var sf_st: float = lerp(float(st["freq"]), float(st["freq_end"]), t_st)
match st["type"]:
"sine":
st["phase"] = fmod(float(st["phase"]) + sf_st / SAMPLE_RATE, 1.0)
s += sin(float(st["phase"]) * TAU) * env_st * float(st["gain"])
"triangle":
st["phase"] = fmod(float(st["phase"]) + sf_st / SAMPLE_RATE, 1.0)
s += (1.0 - absf(4.0 * float(st["phase"]) - 2.0)) * env_st * float(st["gain"])
"noise":
s += randf_range(-1.0, 1.0) * env_st * float(st["gain"])
_stingers = _stingers.filter(func(st): return int(st["pos"]) <= int(st["total"]))
_playback.push_frame(Vector2(s, s)) _playback.push_frame(Vector2(s, s))
_mel_pos += 1 _mel_pos += 1
_arp_pos += 1 _arp_pos += 1
@@ -595,6 +734,7 @@ func _load_mel() -> void:
_bass_freq = FREQ.get(bass_dict[_bass_chord], 0.0) _bass_freq = FREQ.get(bass_dict[_bass_chord], 0.0)
_bass_phase = 0.0 _bass_phase = 0.0
_bass2_phase = 0.0 _bass2_phase = 0.0
_v4_freq = _get_v4_freq()
func _load_arp() -> void: func _load_arp() -> void:
var arp_dict: Dictionary var arp_dict: Dictionary