From 3e55a2297d4968cb00bc3429194849ccd7351d55 Mon Sep 17 00:00:00 2001 From: alpacamannn Date: Tue, 21 Apr 2026 17:06:31 +0200 Subject: [PATCH] feat: BH bass, celestial music events, antimatter rework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Voice 4: sub-bass sine synced to BPM, proximity-driven by nearest BH; SMBH drops frequency one octave lower via 1-pole IIR glide - Music stinger system: short synthesized events overlaid on music for galaxy consumed, SMBH spawn, neutron star beam, white hole eject, quasar jet boost, planet captured, comet pass - Antimatter: spawn probability 10%→3%, swarm size 6-12→3-5, spawn interval 180-360s→300-600s with 1.5s pulsing warning ring - New SFX: antimatter_warn, comet_whistle, planet_impact Co-Authored-By: Claude Sonnet 4.6 --- scripts/cosmic_objects.gd | 4 +- scripts/game_world.gd | 82 +++++++++++++++++++--- scripts/music_player.gd | 142 +++++++++++++++++++++++++++++++++++++- 3 files changed, 216 insertions(+), 12 deletions(-) diff --git a/scripts/cosmic_objects.gd b/scripts/cosmic_objects.gd index 5215962..3231e4c 100644 --- a/scripts/cosmic_objects.gd +++ b/scripts/cosmic_objects.gd @@ -576,8 +576,8 @@ class AntimatterStar extends RefCounted: var alpha: float = clamp(life / MAX_LIFE, 0.0, 1.0) var cv := Vector2(x, y) # 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 + 3.0, Color(0.6, 0.0, 0.85, p * 0.18 * 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.09 * alpha)) # Dark antimatter core canvas.draw_circle(cv, radius, Color(0.15, 0.0, 0.22, alpha)) # Rotating ring diff --git a/scripts/game_world.gd b/scripts/game_world.gd index 5f2bb47..33b4778 100644 --- a/scripts/game_world.gd +++ b/scripts/game_world.gd @@ -14,8 +14,8 @@ const BH_MAX_COUNT := 8 const WHITE_HOLE_MAX := 2 # cap white holes so they don't pile up const COMET_SPAWN_MIN := 3.0 const COMET_SPAWN_MAX := 10.0 -const ANTIMATTER_SPAWN_MIN := 180.0 -const ANTIMATTER_SPAWN_MAX := 360.0 +const ANTIMATTER_SPAWN_MIN := 300.0 +const ANTIMATTER_SPAWN_MAX := 600.0 const DIFFICULTY_RAMP_SECS := 300.0 # full difficulty at 5 minutes const OBJECT_WIPE_THRESHOLD := 500 const PHYS_DT := 1.0 / 60.0 @@ -48,6 +48,10 @@ var comet_timer: float = 0.0 var comet_next: float = 5.0 var antimatter_timer: float = 0.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 big_wipe: BigWipe = null @@ -312,12 +316,21 @@ func _tick(dt: float) -> void: c.init(W, H) comets.append(c) antimatter_timer += dt - var am_max_interval: float = lerp(float(ANTIMATTER_SPAWN_MAX), float(ANTIMATTER_SPAWN_MIN), difficulty) - if antimatter_timer >= antimatter_next: - antimatter_timer = 0.0 - 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)) - SoundManager.play_sfx("antimatter_swarm") + if not antimatter_warn_pending: + var am_max_interval: float = lerp(float(ANTIMATTER_SPAWN_MAX), 120.0, difficulty) + if antimatter_timer >= antimatter_next: + antimatter_timer = 0.0 + antimatter_next = randf_range(ANTIMATTER_SPAWN_MIN, am_max_interval) + 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") var all_dead: bool = true for player in players: var sp: Spaceship = player @@ -414,6 +427,11 @@ func _update_objects(delta: float) -> void: for comet in comets: var c: CosmicObjects.Comet = comet 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) for gal in galaxies: var g: CosmicObjects.Galaxy = gal @@ -426,9 +444,11 @@ func _update_objects(delta: float) -> void: g.consuming_bh = null g.respawning = true g.respawn_timer = randf_range(5.0, 10.0) + _fire_music_event("galaxy_consumed") if cbh.consumed >= 12 and not cbh.is_smbh: cbh.become_smbh() SoundManager.play_sfx("smbh_spawn") + _fire_music_event("bh_pulse") for qsr in quasars: var q: CosmicObjects.Quasar = qsr q.update(delta) @@ -459,6 +479,7 @@ func _update_objects(delta: float) -> void: if q.boost_sound_cd <= 0.0: q.boost_sound_cd = 1.2 SoundManager.play_sfx("quasar_boost") + _fire_music_event("quasar_jet") quasars = quasars.filter(func(q): return not q.dead) _merge_quasars() var new_stars_from_wh: Array = [] @@ -468,6 +489,8 @@ func _update_objects(delta: float) -> void: var result: Dictionary = wh.update(delta) new_stars_from_wh.append_array(result["stars"]) 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: var p: Spaceship = player if not p.dead: @@ -500,8 +523,11 @@ func _update_objects(delta: float) -> void: for player in players: var p: Spaceship = player 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) 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) for am_obj in antimatter: var am: CosmicObjects.Antimatter = am_obj @@ -537,6 +563,7 @@ func _update_objects(delta: float) -> void: if b != null: bullets.append(b) SoundManager.play_sfx("enemy_shoot") + _update_bh_proximity_audio() func _apply_gravity_all(_delta: float) -> void: 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) bh.trigger_flash(p.color, 0.8) p.dead = true + SoundManager.play_sfx("planet_impact") + _fire_music_event("planet_captured") var trigger: bool = bh.on_swallow() if trigger: _trigger_supernova(bh) @@ -1077,7 +1106,7 @@ func _spawn_universe_partial() -> void: planets.append(p) 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: var a: float = randf() * TAU 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) +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: for _i in count: 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: 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: for neb in nebulae: var n: CosmicObjects.Nebula = neb diff --git a/scripts/music_player.gd b/scripts/music_player.gd index 5a39adb..ff6f27f 100644 --- a/scripts/music_player.gd +++ b/scripts/music_player.gd @@ -11,7 +11,7 @@ extends Node # Voice 2 · Bass · Sawtooth (boss2: +detuneter 2. Layer für Schwebung) # Voice 3 · Schlagzeug · Hi-Hat+Kick+Snare (normal/boss1) · # 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 @@ -249,6 +249,16 @@ var _bass_phase := 0.0 # Bass-Zweit-Oszillator (nur boss_mode=2, detunet für Schwebung) 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 var _section := 0 var _chord := 0 @@ -284,6 +294,7 @@ func play() -> void: _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.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 _player.play() _playback = _player.get_stream_playback() @@ -297,6 +308,7 @@ func play_boss() -> void: _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.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: _playing = true _player.play() @@ -313,6 +325,7 @@ func play_boss_leviathan(phase: int = 1) -> void: _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.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: _playing = true _player.play() @@ -336,6 +349,7 @@ func play_normal() -> void: _arp_step = 0; _arp_pos = 0; _arp_phase = 0.0 _bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.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() func stop_music() -> void: @@ -353,6 +367,83 @@ func set_muted(muted: bool) -> void: if idx != -1: 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 ───────────────────────────────────────────────────────── func _process(_delta: float) -> void: if _playing: @@ -562,6 +653,54 @@ func _fill_buffer() -> void: var snare_env := 1.0 - float(snare_pos) / 340.0 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)) _mel_pos += 1 _arp_pos += 1 @@ -595,6 +734,7 @@ func _load_mel() -> void: _bass_freq = FREQ.get(bass_dict[_bass_chord], 0.0) _bass_phase = 0.0 _bass2_phase = 0.0 + _v4_freq = _get_v4_freq() func _load_arp() -> void: var arp_dict: Dictionary