mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 14:42:06 +00:00
feat(audio): add procedural gameplay sound foundation
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
extends Node
|
||||
|
||||
# Dependency-free audio foundation. Authored assets can replace these tones
|
||||
# later without changing gameplay call sites or the multiplayer event flow.
|
||||
|
||||
const SAMPLE_RATE := 44100
|
||||
const MAX_INTENSITY := 1.0
|
||||
|
||||
var enabled := true
|
||||
|
||||
|
||||
func play_ui_click() -> void:
|
||||
_play_tone(880.0, 0.045, 0.10)
|
||||
|
||||
|
||||
func play_countdown(count: int) -> void:
|
||||
if count <= 0:
|
||||
_play_tone(1046.5, 0.12, 0.18)
|
||||
else:
|
||||
_play_tone(countdown_frequency(count), 0.08, 0.14)
|
||||
|
||||
|
||||
func play_impact(intensity: float) -> void:
|
||||
var amount := clamp_intensity(intensity)
|
||||
if amount <= 0.0:
|
||||
return
|
||||
_play_tone(150.0 + 180.0 * amount, 0.06 + 0.08 * amount, 0.08 + 0.18 * amount)
|
||||
|
||||
|
||||
func play_goal() -> void:
|
||||
_play_tone(523.25, 0.22, 0.22)
|
||||
_play_tone(783.99, 0.30, 0.18)
|
||||
|
||||
|
||||
static func clamp_intensity(value: float) -> float:
|
||||
if not is_finite(value):
|
||||
return 0.0
|
||||
return clampf(value, 0.0, MAX_INTENSITY)
|
||||
|
||||
|
||||
static func countdown_frequency(count: int) -> float:
|
||||
return 440.0 + float(clampi(count, 1, 9)) * 55.0
|
||||
|
||||
|
||||
func _play_tone(frequency: float, duration: float, volume: float) -> void:
|
||||
if not enabled or frequency <= 0.0 or duration <= 0.0 or volume <= 0.0:
|
||||
return
|
||||
var stream := AudioStreamWAV.new()
|
||||
stream.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
stream.mix_rate = SAMPLE_RATE
|
||||
stream.stereo = false
|
||||
stream.data = _tone_data(frequency, duration, volume)
|
||||
var player := AudioStreamPlayer.new()
|
||||
player.stream = stream
|
||||
add_child(player)
|
||||
player.finished.connect(player.queue_free)
|
||||
player.play()
|
||||
|
||||
|
||||
func _tone_data(frequency: float, duration: float, volume: float) -> PackedByteArray:
|
||||
var frames := maxi(1, int(duration * SAMPLE_RATE))
|
||||
var data := PackedByteArray()
|
||||
data.resize(frames * 2)
|
||||
for index in frames:
|
||||
var envelope := minf(1.0, float(index) / 256.0) * minf(1.0, float(frames - index) / 1024.0)
|
||||
var sample := int(sin(TAU * frequency * float(index) / SAMPLE_RATE) * volume * envelope * 32767.0)
|
||||
if sample < 0:
|
||||
sample += 65536
|
||||
data[index * 2] = sample & 0xff
|
||||
data[index * 2 + 1] = (sample >> 8) & 0xff
|
||||
return data
|
||||
@@ -136,6 +136,7 @@ func _play_goal_celebration(scoring_team: int, conceding_team: int) -> void:
|
||||
# real-time presentation delay between episodes.
|
||||
if DisplayServer.get_name() == "headless" or not is_instance_valid(_camera_rig):
|
||||
return
|
||||
AudioManager.play_goal()
|
||||
var goal_position := Vector3.ZERO
|
||||
for goal in arena.get_goals():
|
||||
if goal.team == conceding_team:
|
||||
|
||||
@@ -122,6 +122,7 @@ func _run_kickoff_countdown() -> void:
|
||||
_set_frozen(true)
|
||||
for count in range(KICKOFF_COUNTDOWN_SECONDS, 0, -1):
|
||||
kickoff_countdown.emit(count)
|
||||
AudioManager.play_countdown(count)
|
||||
# process_always=false: if full-time fires mid-countdown (see
|
||||
# _on_match_timer_timeout's get_tree().paused = true), this stalls
|
||||
# harmlessly in lockstep with the pause instead of ticking a
|
||||
@@ -132,6 +133,7 @@ func _run_kickoff_countdown() -> void:
|
||||
if _match_over:
|
||||
return
|
||||
kickoff_countdown.emit(0)
|
||||
AudioManager.play_countdown(0)
|
||||
_set_frozen(false)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user