@@ -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.
|
||||||
@@ -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/
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user