extends Node ## The music and the ambience, chosen from what's going on around you. The ## tracks are synthesised by tools/audio/make_audio.py (assets/audio/music): ## ## boss the Old Stag is up, within 70 m ## danger a wolf, bear, boar or barrow guard is coming for someone nearby ## (and for 10 s after it stops) ## storm a storm or blizzard, or the red wolf moon ## night after dark ## calm by day (and on the title screen) ## ## Calm, night and storm play once and then rest for a while, with just the ## ambience (birds by day, crickets at night), so the music doesn't wear ## thin; danger and boss loop until it's over. Changes crossfade. Indoors the ## music and ambience are quieter. It only reads things every game already ## has (the clock, the weather, the creatures it can see), so it works the ## same online. Headless, it still works out the mood (`mood`) but plays ## nothing. const DIR := "res://assets/audio/music/" const AMB := "res://assets/audio/ambience/" const MOODS := ["boss", "danger", "storm", "night", "calm"] const LOOPS := ["danger", "boss"] ## States a creature is in when it's after someone. const AGGRO := ["charge", "stalk", "lunge", "bash", "stomp", "telegraph", "warn", "raid", "summon", "alert", "defend"] const DANGER_RANGE := 26.0 const BOSS_RANGE := 70.0 const DANGER_HOLD := 10.0 const FADE := 3.0 const FADE_FAST := 1.2 const REST := [35.0, 80.0] ## seconds of quiet between calm/night/storm plays var world: Node3D = null var mood := "" ## what the music is for now var playing := "" ## the track that's sounding ("" between plays) var _danger_until := -1.0 var _clock := 0.0 var _tick := 0.0 var _rest_left := 0.0 var _a: AudioStreamPlayer var _b: AudioStreamPlayer var _cur: AudioStreamPlayer var _birds: AudioStreamPlayer var _crickets: AudioStreamPlayer var _owl: AudioStreamPlayer3D var _owl_left := 20.0 var _owl_calls: Array = [] ## a few different calls, so it doesn't repeat itself var owls := 0 ## hoots so far (tests) const OWL_EVERY := [35.0, 110.0] var _tracks: Dictionary = {} var _rng := RandomNumberGenerator.new() var _quiet := false var _fading := 0 ## crossfades in progress var duck := false ## Old Tam is talking (narrator.gd): the music dips func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS # (the pause menu stops the game, not the music) _rng.randomize() _quiet = DisplayServer.get_name() == "headless" if _quiet: return _a = _player("Music") _b = _player("Music") _cur = _a _birds = _player("Ambience", _load(AMB + "birds.ogg", true)) _crickets = _player("Ambience", _load(AMB + "crickets.ogg", true)) _owl = AudioStreamPlayer3D.new() _owl.bus = "Ambience" for i in range(2): var call: Variant = _load(AMB + "owl_%d.ogg" % i, false) if call is AudioStream: _owl_calls.append(call) _owl.unit_size = 30.0 # (far off in the trees, but clearly heard) _owl.max_distance = 120.0 add_child(_owl) for p in [_a, _b]: p.finished.connect(_on_finished.bind(p)) func _player(bus: String, stream: AudioStream = null) -> AudioStreamPlayer: var p := AudioStreamPlayer.new() p.bus = bus p.volume_db = -80.0 p.stream = stream add_child(p) return p static func _load(path: String, loop: bool) -> AudioStream: var s: Variant = load(path) if s is AudioStreamOggVorbis: (s as AudioStreamOggVorbis).loop = loop return s func start(w: Node3D) -> void: world = w mood = "" _danger_until = -1.0 _tick = 0.0 if not _quiet: for p in [_birds, _crickets]: if not p.playing: p.play() func stop(w: Node3D = null) -> void: if w != null and w != world: return world = null func _process(delta: float) -> void: step(delta) func step(dt: float) -> void: _clock += dt _tick -= dt if _tick <= 0.0: _tick = 0.5 var m := decide() if m != mood: _change(m) if not _quiet and playing.is_empty() and not mood.is_empty(): _rest_left -= dt if _rest_left <= 0.0: _start_track(mood, FADE) _ambience(dt) ## What the music should be for, right now. func decide() -> String: if world == null or not is_instance_valid(world): return "calm" if world.get("menu_mode"): return "calm" var me: Node3D = world.get("player") if me == null or not is_instance_valid(me): return "calm" var here := me.global_position var boss := false var danger := false for c in get_tree().get_nodes_in_group("creature"): if not is_instance_valid(c) or not (c is Node3D) or (c.has_method("is_dead") and c.is_dead()): continue var d: float = (c as Node3D).global_position.distance_to(here) var kind := String(c.get("kind")) if c.get("kind") != null else "" if kind == "old_stag" and d < BOSS_RANGE: boss = true elif d < DANGER_RANGE and String(c.get("state")) in AGGRO and _hostile(c): danger = true if boss: return "boss" if danger: _danger_until = _clock + DANGER_HOLD if _clock < _danger_until: return "danger" var dn: Node = world.get("day_night") var wx: Node = world.get("weather") var night: bool = dn != null and dn.is_night() if wx != null and String(wx.get("state")) in ["storm", "blizzard"]: return "storm" if night and dn.wolf_moon_now(): return "storm" return "night" if night else "calm" static func _hostile(c: Node) -> bool: var def: Variant = c.get("def") return def is Dictionary and (def as Dictionary).has("damage") func _change(m: String) -> void: var was := mood mood = m if _quiet: return # Into a fight: straight in. Out of one: let the fight music fade, then a # short rest before the calmer music comes back. if m in LOOPS: _start_track(m, FADE_FAST) elif was in LOOPS or was.is_empty(): _fade_out(FADE) _rest_left = 4.0 if not was.is_empty() else 1.5 elif not playing.is_empty(): _start_track(m, FADE) # (a calm piece mid-play crossfades to the night one) else: _rest_left = minf(_rest_left, 6.0) func _start_track(m: String, fade: float) -> void: if not _tracks.has(m): _tracks[m] = _load(DIR + m + ".ogg", m in LOOPS) var next: AudioStreamPlayer = _b if _cur == _a else _a var old := _cur next.stream = _tracks[m] next.volume_db = -40.0 next.play() _fade(next, _target_db(), fade) if old.playing: _fade(old, -60.0, fade, true) _cur = next playing = m func _fade_out(fade: float) -> void: if _cur != null and _cur.playing: _fade(_cur, -60.0, fade, true) playing = "" func _fade(p: AudioStreamPlayer, db: float, secs: float, then_stop: bool = false) -> void: _fading += 1 var tw := create_tween() tw.tween_property(p, "volume_db", db, secs) tw.tween_callback(func(): _fading -= 1) if then_stop: tw.tween_callback(p.stop) func _on_finished(p: AudioStreamPlayer) -> void: if p == _cur: playing = "" _rest_left = _rng.randf_range(REST[0], REST[1]) ## Indoors everything's a little further away. func _indoors() -> bool: if world == null or not is_instance_valid(world) or world.get("player") == null: return false var sv: Node = world.player.get("survival") return sv != null and bool(sv.get("sheltered")) func _target_db() -> float: return (-6.0 if _indoors() else -2.0) - (7.0 if duck else 0.0) func _ambience(dt: float) -> void: if _quiet or _birds == null: return var day := 1.0 var wet := 0.0 var snow := false if world != null and is_instance_valid(world) and world.get("day_night") != null: day = float(world.day_night.daylight) var s := String(world.weather.get("state")) if world.get("weather") != null else "clear" wet = 1.0 if s in ["rain", "storm", "blizzard"] else 0.0 snow = s in ["snow", "blizzard"] var inside := 0.35 if _indoors() else 1.0 if world == null or not is_instance_valid(world) or world.get("menu_mode"): inside = 0.0 # (the title screen: music only, no birds or crickets) var birds := clampf((day - 0.35) / 0.4, 0.0, 1.0) * (1.0 - wet) * inside var crickets := clampf((0.45 - day) / 0.3, 0.0, 1.0) * (0.0 if snow else 1.0) * (1.0 - wet * 0.8) * inside _owl_step(dt, crickets > 0.5, inside) var k := clampf(dt * 0.8, 0.0, 1.0) _birds.volume_db = lerpf(_birds.volume_db, linear_to_db(maxf(birds * 0.28, 0.0001)), k) # (in the background: the recording is full-scale) _crickets.volume_db = lerpf(_crickets.volume_db, linear_to_db(maxf(crickets * 0.4, 0.0001)), k) # (a recording, full-scale) if _cur != null and _cur.playing and _fading == 0: _cur.volume_db = lerpf(_cur.volume_db, _target_db(), k) ## Now and then at night (when the crickets are out: dark, dry, no snow), an ## owl hoots from somewhere 25-45 m off in a random direction. Only you hear ## your owl; online, everyone's night has its own. func _owl_step(dt: float, night_out: bool, inside: float) -> void: if _owl == null or _owl_calls.is_empty() or not night_out: _owl_left = maxf(_owl_left, 8.0) return _owl_left -= dt if _owl_left > 0.0 or _owl.playing: return _owl_left = _rng.randf_range(OWL_EVERY[0], OWL_EVERY[1]) var me: Node3D = world.get("player") if world != null else null if me == null: return var a := _rng.randf() * TAU var r := _rng.randf_range(25.0, 45.0) _owl.global_position = me.global_position + Vector3(cos(a) * r, _rng.randf_range(6.0, 12.0), sin(a) * r) _owl.stream = _owl_calls[_rng.randi() % _owl_calls.size()] _owl.pitch_scale = _rng.randf_range(0.96, 1.04) _owl.volume_db = -8.0 + linear_to_db(maxf(inside, 0.05)) _owl.play() owls += 1