Audio: mood music, ambience, sound effects and creature voices (Kenney CC0 + synthesised); protocol 13

This commit is contained in:
Paul Bailey
2026-11-05 21:03:54 +00:00
parent 2a9d47630e
commit caa611fac6
159 changed files with 1814 additions and 3 deletions

View File

@@ -217,11 +217,14 @@ func step(dt: float) -> void:
_aggro = maxf(_aggro - dt, 0.0)
# Who it's reacting to: the player (or on a server, the nearest one).
var t := _pick_target()
var was := state
var cd0 := _attack_cd
if t != null:
player = t
_think(dt)
elif state in ["flee", "charge"]:
state = "idle" # nobody about
_voice(dt, was, cd0)
_move(dt)
_animate(dt)
@@ -410,6 +413,13 @@ func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
hp -= damage
if not embed_id.is_empty():
embedded[embed_id] = embedded.get(embed_id, 0) + 1
Sfx.at("hit_bone" if bool(def.get("bone", false)) else "hit_flesh", global_position + Vector3(0, 0.6, 0))
var cry := String(VOICES.get(kind, {}).get("hurt", ""))
if not cry.is_empty() and _hurt_voice_cd <= 0.0:
Sfx.at(cry, global_position + Vector3(0, 0.8, 0))
_hurt_voice_cd = 0.6 # (a flurry of arrows: one cry, not five)
if hp <= 0.0:
Sfx.at("creature_die", global_position)
_flash = 0.15
_set_material(_hit_mat)
_burst()
@@ -491,3 +501,60 @@ func _burst() -> void:
p.global_position = global_position + Vector3(0, Meshes.build(kind).height * 0.6, 0)
p.emitting = true
p.finished.connect(p.queue_free)
# --- voices (sfx.gd) ---------------------------------------------------------------------
## What each creature sounds like: "alert" when it turns on someone, "angry"
## now and then while it's after them, "attack" as it strikes, "hurt" when hit,
## and "idle" now and then while it's minding its own business. Played by
## whoever runs the creature (single-player, or the server, which sends them on).
const VOICES := {
"wolf": {"alert": "wolf_growl", "angry": "wolf_growl", "attack": "wolf_snarl", "hurt": "wolf_yelp"},
"alpha": {"alert": "alpha_growl", "angry": "alpha_growl", "attack": "alpha_snarl", "hurt": "alpha_yelp"},
"spirit_wolf": {"alert": "spirit_growl", "angry": "spirit_growl", "attack": "spirit_snarl", "hurt": "spirit_growl"},
"bear": {"alert": "bear_roar", "angry": "bear_grunt", "attack": "bear_grunt", "hurt": "bear_hurt", "idle": "bear_grunt"},
"boar": {"alert": "boar_grunt", "angry": "boar_grunt", "attack": "boar_grunt", "hurt": "boar_squeal", "idle": "boar_grunt"},
"old_stag": {"alert": "stag_bellow", "angry": "stag_bellow", "attack": "stag_snort", "hurt": "stag_snort"},
"guard": {"alert": "guard_rattle", "angry": "guard_rattle", "attack": "guard_rattle"},
"crow": {"alert": "crow_caw", "hurt": "crow_caw", "idle": "crow_caw"},
"fox": {"alert": "fox_yip", "hurt": "fox_yip"},
}
## States a creature is in when it's after someone (music.gd's list too).
const ANGRY := ["charge", "stalk", "lunge", "bash", "stomp", "telegraph", "warn", "raid", "summon"]
const ANGRY_EVERY := [4.0, 8.0]
const IDLE_EVERY := [18.0, 45.0]
var _voice_left := -1.0
var _hurt_voice_cd := 0.0
## Its own dice, so voices never change what the creature does (the
## behaviour tests replay the same choices from the same seed).
var _voice_rng := RandomNumberGenerator.new()
func _voice(dt: float, was: String, cd0: float) -> void:
_hurt_voice_cd = maxf(_hurt_voice_cd - dt, 0.0)
var v: Dictionary = VOICES.get(kind, {})
if v.is_empty():
return
var mouth := global_position + Vector3(0, 0.8, 0)
if _attack_cd > cd0 + 0.05 and v.has("attack"):
Sfx.at(String(v.attack), mouth)
_voice_left = maxf(_voice_left, 1.5)
return
var angry := state in ANGRY
if angry and not (was in ANGRY) and v.has("alert"):
Sfx.at(String(v.alert), mouth)
_voice_left = _voice_rng.randf_range(ANGRY_EVERY[0], ANGRY_EVERY[1])
return
if _voice_left < 0.0:
_voice_rng.seed = get_instance_id()
_voice_left = _voice_rng.randf_range(2.0, IDLE_EVERY[1]) # (a new creature doesn't call out at once)
_voice_left -= dt
if _voice_left > 0.0:
return
if angry and v.has("angry"):
Sfx.at(String(v.angry), mouth)
_voice_left = _voice_rng.randf_range(ANGRY_EVERY[0], ANGRY_EVERY[1])
elif not angry and v.has("idle") and state != "rest" and state != "flee":
Sfx.at(String(v.idle), mouth)
_voice_left = _voice_rng.randf_range(IDLE_EVERY[0], IDLE_EVERY[1])
else:
_voice_left = _voice_rng.randf_range(ANGRY_EVERY[0], ANGRY_EVERY[1])