Files
Cinderwild/scripts/creatures/creature.gd

568 lines
21 KiB
GDScript

extends AnimatableBody3D
## A wild animal: rabbit, deer or boar.
##
## Movement is steering over the heightmap, with no navmesh. The animal walks
## toward a heading, probes the ground ahead (water, cliffs, the world edge,
## trees, rocks and buildings) and turns away from anything it can't cross.
##
## States:
## - idle: stands and grazes.
## - wander: ambles in one direction.
## - flee: runs away from a threat. Rabbits zig-zag.
## - charge: a hit boar runs at you and gores you.
## - dead: a carcass. E butchers it for meat and hide, and gives back any
## arrows stuck in it.
##
## step(dt) holds all the logic so tests can drive it directly. Lives on
## physics layer 4 (value 8): hit by weapons and arrows, but the player walks
## through it. A carcass moves to layer 2, like other interactables.
signal died(creature: Node)
const Meshes = preload("res://scripts/creatures/creature_meshes.gd")
const LAYER := 8
## Tuning per kind. Speeds in m/s (the player walks at 5 and sprints at 8).
const KINDS := {
"rabbit": {"name": "Rabbit", "hp": 10.0, "walk": 1.4, "run": 7.0, "turn": 9.0,
"notice": 7.0, "notice_loud": 12.0, "calm": 18.0,
"drops": {"raw_meat": [1, 1]}},
"deer": {"name": "Deer", "hp": 40.0, "walk": 1.3, "run": 9.0, "turn": 4.5,
"notice": 16.0, "notice_loud": 30.0, "calm": 45.0,
"drops": {"raw_meat": [2, 3], "hide": [1, 2]}},
"boar": {"name": "Boar", "hp": 60.0, "walk": 1.2, "run": 6.6, "turn": 5.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 14.0, "attack_range": 1.7, "attack_cd": 1.6, "aggro": 18.0,
"drops": {"raw_meat": [3, 4], "hide": [1, 2]}},
# Wolves are driven by wolf.gd + wolf_pack.gd; "notice" is unused.
"wolf": {"name": "Wolf", "hp": 45.0, "walk": 1.6, "run": 8.6, "turn": 6.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 9.0, "attack_range": 1.8, "attack_cd": 1.2,
"drops": {"raw_meat": [1, 2], "wolf_pelt": [1, 1]}},
# The alpha: leads the pack on a wolf moon (wolf_pack.gd). Three wolves'
# worth of health, bites harder, isn't afraid of fire and breaks down
# wooden doors (wolf.gd).
"alpha": {"name": "Alpha Wolf", "hp": 135.0, "walk": 1.6, "run": 8.2, "turn": 5.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 16.0, "attack_range": 2.1, "attack_cd": 1.4, "door_damage": 22.0,
"drops": {"raw_meat": [2, 3], "alpha_pelt": [1, 1], "wolf_fang": [1, 2]}},
# Thieves (thieves.gd): crows by day and foxes at dusk and night come for
# food left lying on the ground.
"crow": {"name": "Crow", "hp": 6.0, "walk": 1.0, "run": 9.0, "turn": 8.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"drops": {"feather": [1, 2]}},
"fox": {"name": "Fox", "hp": 25.0, "walk": 1.5, "run": 7.6, "turn": 7.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"drops": {"raw_meat": [1, 1], "hide": [1, 1]}},
# The barrow's skeletal guards (guard.gd, barrow.gd). Slow (you can walk
# away from one) but they hit hard; bone, so blunt weapons do +50%.
"guard": {"name": "Barrow Guard", "hp": 80.0, "walk": 1.4, "run": 3.4, "turn": 4.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 20.0, "attack_range": 2.0, "attack_cd": 2.0, "bone": true,
"drops": {"iron_nugget": [1, 2]}},
# 0.6.0: the drowned crew (guard.gd, at a treasure dig: treasure.gd). The
# Cove's barrow guards: slow, hard-hitting, unafraid of fire.
"drowned": {"name": "Drowned Sailor", "hp": 70.0, "walk": 1.3, "run": 3.1, "turn": 4.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 16.0, "attack_range": 2.0, "attack_cd": 1.8, "bone": true,
"drops": {"salt": [0, 1]}},
# P5: the Old Stag (old_stag.gd), called by the standing stones, and the
# spirit wolves it summons (they just run at you: the base "charge").
"old_stag": {"name": "Old Stag", "hp": 900.0, "walk": 2.0, "run": 13.0, "turn": 3.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 60.0,
"damage": 30.0, "attack_range": 2.8, "attack_cd": 1.0, "drops": {}},
"spirit_wolf": {"name": "Spirit Wolf", "hp": 40.0, "walk": 1.6, "run": 7.4, "turn": 6.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 60.0,
"damage": 8.0, "attack_range": 1.8, "attack_cd": 1.3, "aggro": 9999.0, "drops": {}},
# One bear per island, in the deep forest by the rim (bear.gd, bear_range.gd).
# Slower than you sprinting, but it hits like a falling tree.
"bear": {"name": "Bear", "hp": 220.0, "walk": 1.3, "run": 7.0, "turn": 3.5,
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 24.0, "attack_range": 2.3, "attack_cd": 1.8,
"drops": {"raw_meat": [5, 7], "bear_hide": [1, 1], "bear_fat": [2, 3]}},
}
const CARCASS_SECONDS := 300.0
var kind := "deer"
var def: Dictionary
var hp := 40.0
var state := "idle"
var world: Node3D
var terrain: Node3D
var player: Node3D
var rng := RandomNumberGenerator.new()
var embedded := {} ## item id -> count (arrows stuck in it)
var carcass_left := CARCASS_SECONDS
var yaw := 0.0
var target_yaw := 0.0
var speed := 0.0
var _state_t := 0.0
var _threat := Vector3.ZERO
var _aggro := 0.0
var _attack_cd := 0.0
var _zig := 0.0
var _phase := 0.0
var _visual: Node3D
var _legs: Array[Node3D] = []
var _flash := 0.0
var _detour_t := 0.0 ## > 0: following a detour round an obstacle
var _detour_side := 1.0
## Sets the heading, unless the animal is in the middle of a detour.
func steer(y: float) -> void:
if _detour_t <= 0.0:
target_yaw = y
var _mat: StandardMaterial3D
var _hit_mat: StandardMaterial3D
func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
kind = k
def = KINDS[k]
hp = def.hp
world = w
terrain = w.terrain
player = w.player
rng.seed = seed
name = "%s_%d" % [k, absi(seed) % 100000]
collision_layer = LAYER
collision_mask = 0
sync_to_physics = false
add_to_group("creature")
var parts: Dictionary = Meshes.build(k)
_mat = ItemDB.item_material()
_hit_mat = StandardMaterial3D.new()
_hit_mat.vertex_color_use_as_albedo = true
_hit_mat.vertex_color_is_srgb = true
_hit_mat.albedo_color = Color(1.6, 0.5, 0.45)
_visual = Node3D.new()
add_child(_visual)
var body := MeshInstance3D.new()
body.mesh = parts.body
body.material_override = _mat
_visual.add_child(body)
if parts.has("eyes"):
# Eyes that catch the light: you see a wolf's eyes in the dark first.
var eyes := MeshInstance3D.new()
eyes.mesh = parts.eyes
var em := StandardMaterial3D.new()
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0), "drowned": Color(0.5, 1.0, 0.6), "old_stag": Color(0.55, 0.95, 1.0),
"spirit_wolf": Color(0.55, 0.95, 1.0)}.get(k, Color(1.0, 0.75, 0.3))
em.albedo_color = eye.lerp(Color.WHITE, 0.2)
em.emission_enabled = true
em.emission = eye
em.emission_energy_multiplier = 2.5
eyes.material_override = em
eyes.set_meta("keep_mat", true)
_visual.add_child(eyes)
for h: Vector3 in parts.hips:
var pivot := Node3D.new()
pivot.position = h
var leg := MeshInstance3D.new()
leg.mesh = parts.leg
leg.material_override = _mat
pivot.add_child(leg)
_visual.add_child(pivot)
_legs.append(pivot)
var cs := CollisionShape3D.new()
var cap := CapsuleShape3D.new()
cap.radius = parts.radius
cap.height = maxf(parts.height, parts.radius * 2.0 + 0.01)
cs.shape = cap
cs.position = Vector3(0, parts.height * 0.5, 0)
if not k in ["rabbit", "guard", "drowned"]: # (rabbits are round, guards stand upright)
# Long bodies: lay the capsule along the spine.
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3, "old_stag": 3.3, "spirit_wolf": 1.25}.get(k, 1.4)
cs.rotation.x = PI * 0.5
cs.position = Vector3(0, parts.height * 0.55, 0)
add_child(cs)
yaw = rng.randf() * TAU
target_yaw = yaw
_state_t = rng.randf_range(1.0, 4.0)
position = Vector3(pos.x, terrain.sample_height(pos.x, pos.z), pos.z) # (spawner sits at the origin)
rotation.y = yaw
func display_name() -> String:
return def.name
func is_dead() -> bool:
return state == "dead"
func interact_prompt(p: Node) -> String:
if is_dead():
return "%s carcass [E] Butcher" % def.name
var s: String = def.name
if kind == "boar":
s = "Boar (angry!)" if _aggro > 0.0 else "Boar (leaves you alone unless you hit it)"
var held: Dictionary = ItemDB.get_item(p.held_item_id())
if not Actions.melee_stats(p).armed and not held.has("bow"):
s += " (you need a weapon: club, spear, bow or axe)"
return s
func interact(p: Node) -> void:
if is_dead():
Actions.butcher(p, self)
func _physics_process(delta: float) -> void:
step(delta)
func step(dt: float) -> void:
if _flash > 0.0:
_flash -= dt
if _flash <= 0.0:
_set_material(_mat)
if puppet:
_puppet_step(dt)
return
if is_dead():
carcass_left -= dt
if carcass_left <= 0.0:
queue_free()
return
_attack_cd = maxf(_attack_cd - dt, 0.0)
_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)
func _pick_target() -> Node3D:
return world.nearest_player(global_position)
# --- on a client playing online: the server's animal, shown here ------------
## A puppet only shows what the server's copy is doing (creature_net.gd):
## no thinking, no damage dealt. It can still be hit (that's sent to the
## server) and butchered.
var puppet := false
var _net_pos := Vector3.ZERO
var _net_yaw := 0.0
func net_update(pos: Vector3, y: float, st: String, spd: float, new_hp: float) -> void:
if not puppet:
return
_net_pos = pos
_net_yaw = y
speed = spd
if new_hp < hp - 0.01 and st != "dead":
_flash = 0.15
_set_material(_hit_mat)
_burst()
hp = new_hp
if st == "dead" and state != "dead":
_burst()
_die(global_position + global_basis.z)
return
if state != "dead":
state = st
func _puppet_step(dt: float) -> void:
if is_dead():
return
var k := 1.0 - exp(-12.0 * dt)
var p := global_position.lerp(_net_pos, k)
if p.distance_to(_net_pos) > 6.0:
p = _net_pos # (a jump: a wolf slipping home to the den)
global_position = p
yaw = lerp_angle(yaw, _net_yaw, k)
rotation.y = yaw
_animate(dt)
## Whether the player is close enough to be noticed. Sprinting, or moving
## fast while swimming or jumping, is loud.
func _notices_player(d: float) -> bool:
if def.notice <= 0.0:
return false
var loud: bool = player.velocity.length() > 6.0
return d < (def.notice_loud if loud else def.notice)
func _think(dt: float) -> void:
var pp: Vector3 = player.global_position
var d := global_position.distance_to(pp)
_state_t -= dt
match state:
"idle", "wander":
if _notices_player(d):
_start_flee(pp)
elif _state_t <= 0.0:
if state == "idle":
state = "wander"
target_yaw = yaw + rng.randf_range(-1.6, 1.6)
_state_t = rng.randf_range(3.0, 8.0)
else:
state = "idle"
_state_t = rng.randf_range(2.0, 6.0)
"flee":
if _notices_player(d):
_threat = pp
var away := global_position - _threat
target_yaw = atan2(-away.x, -away.z) # heading along `away` (forward is -Z)
if kind == "rabbit":
_zig -= dt
if _zig <= 0.0:
_zig = rng.randf_range(0.3, 0.7)
target_yaw += rng.randf_range(-0.9, 0.9)
if d > def.calm and _state_t <= 0.0:
state = "idle"
_state_t = rng.randf_range(2.0, 5.0)
"charge":
var to := pp - global_position
target_yaw = atan2(-to.x, -to.z)
if _aggro <= 0.0 or d > 45.0:
state = "wander"
_state_t = 4.0
elif d < def.attack_range and _attack_cd <= 0.0:
_attack_cd = def.attack_cd
player.survival.damage(def.damage * world.difficulty_mult("damage"), global_position, "a " + String(def.name).to_lower())
func _start_flee(from: Vector3) -> void:
state = "flee"
_threat = from
_state_t = 4.0
func _desired_speed() -> float:
match state:
"wander": return def.walk
"flee", "charge": return def.run
return 0.0
func _move(dt: float) -> void:
var want := _desired_speed()
speed = move_toward(speed, want, dt * (def.run * 2.5))
yaw = rotate_toward(yaw, target_yaw, def.turn * dt)
if speed < 0.01:
return
_detour_t = maxf(_detour_t - dt, 0.0)
var dir := Vector3(-sin(yaw), 0, -cos(yaw))
if _blocked(dir):
# Turn away and keep to the new heading for a moment (a detour), so
# an animal heading somewhere slides round an obstacle instead of
# turning straight back into it. Keep turning the same way while
# still blocked.
if _detour_t <= 0.0:
_detour_side = 1.0 if rng.randf() < 0.5 else -1.0
var turn := rng.randf_range(0.8, 1.6) * _detour_side
target_yaw = yaw + turn
yaw += turn * 0.35
_detour_t = rng.randf_range(1.0, 1.8)
speed *= 0.5
if state == "charge":
return
dir = Vector3(-sin(yaw), 0, -cos(yaw))
if _blocked(dir):
return
var p := global_position + dir * speed * dt
p.y = terrain.sample_height(p.x, p.z)
global_position = p
rotation.y = yaw
## Something ahead the animal can't or won't cross.
func _blocked(dir: Vector3) -> bool:
var probe := global_position + dir * (1.4 + speed * 0.15)
if not terrain.inside_boundary(probe.x, probe.z, 6.0):
return true
var h: float = terrain.sample_height(probe.x, probe.z)
if h < terrain.water_level + 0.2:
return true
if absf(h - global_position.y) > 1.1:
return true
# Trees, rocks and buildings at chest height (the terrain isn't on this test).
var from := global_position + Vector3(0, 0.45, 0)
var q := PhysicsRayQueryParameters3D.create(from, Vector3(probe.x, h + 0.45, probe.z), 1)
q.exclude = [get_rid(), terrain.get_node("TerrainBody").get_rid()]
return not get_world_3d().direct_space_state.intersect_ray(q).is_empty()
func _animate(dt: float) -> void:
if state == "rest":
# Lying down: body lowered, legs folded under.
_visual.position.y = lerpf(_visual.position.y, -0.3, minf(dt * 3.0, 1.0))
_visual.rotation.x = lerpf(_visual.rotation.x, 0.0, minf(dt * 4.0, 1.0))
for l in _legs:
l.rotation.x = lerpf(l.rotation.x, 1.35 * (1.0 if l.position.z < 0.0 else -1.0), minf(dt * 3.0, 1.0))
return
var k := speed / maxf(def.run, 0.01)
_phase += dt * (3.0 + speed * 2.2)
var amp := clampf(speed / 2.0, 0.0, 1.0) * 0.6
for i in range(_legs.size()):
# Diagonal pairs move together (a trot); rabbits hop with both
# hind legs at once.
var off := 0.0 if i in [0, 3] else PI
if kind == "rabbit":
off = 0.0 if i < 2 else PI * 0.6
_legs[i].rotation.x = sin(_phase + off) * amp
_visual.position.y = absf(sin(_phase)) * (0.06 if kind == "rabbit" else 0.03) * k
if state == "idle" and kind != "rabbit":
# Grazing: head (whole body) dips a little now and then.
_visual.rotation.x = lerpf(_visual.rotation.x, 0.08 * maxf(sin(_state_t * 1.3), 0.0), dt * 2.0)
else:
_visual.rotation.x = lerpf(_visual.rotation.x, 0.0, dt * 4.0)
# --- combat -------------------------------------------------------------------
## Applies a hit (Actions validates who hit what). `from` = where it came
## from, for the reaction.
func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
if puppet:
return # (online the server's copy takes the hit)
if is_dead():
if not embed_id.is_empty():
embedded[embed_id] = embedded.get(embed_id, 0) + 1
return
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()
if hp <= 0.0:
_die(from)
return
_react(from)
## How a living animal responds to being hit (wolves override this).
func _react(from: Vector3) -> void:
if (kind == "boar" and hp > def.hp * 0.3) or kind == "spirit_wolf": # (spirit wolves never run)
state = "charge"
_aggro = def.aggro
_attack_cd = 0.4
else:
_start_flee(from)
_state_t = 6.0
func _die(from: Vector3) -> void:
state = "dead"
speed = 0.0
hp = 0.0
_flash = 0.0
_set_material(_mat)
remove_from_group("creature")
add_to_group("carcass")
collision_layer = 2
for l in _legs:
l.rotation.x = 0.4
if puppet:
_visual.rotation.z = -PI * 0.5
return
# Topple onto its side, away from the blow.
var side := 1.0 if (global_position - from).dot(global_basis.x) > 0.0 else -1.0
if is_inside_tree() and DisplayServer.get_name() != "headless":
var tw := create_tween()
tw.tween_property(_visual, "rotation:z", -side * PI * 0.5, 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
else:
_visual.rotation.z = -side * PI * 0.5
died.emit(self)
## What butchering gives (rolled once).
func butcher_drops() -> Dictionary:
var out := {}
for id in def.drops:
var r: Array = def.drops[id]
out[id] = rng.randi_range(int(r[0]), int(r[1]))
for id in embedded:
out[id] = out.get(id, 0) + int(embedded[id])
return out
func _set_material(m: Material) -> void:
for c in _visual.get_children():
if c is MeshInstance3D and not c.has_meta("keep_mat"):
c.material_override = m
for g in c.get_children():
if g is MeshInstance3D and not g.has_meta("keep_mat"):
g.material_override = m
func _burst() -> void:
if not is_inside_tree() or DisplayServer.get_name() == "headless":
return
var p := CPUParticles3D.new()
var box := BoxMesh.new()
box.size = Vector3.ONE * 0.05
var m := StandardMaterial3D.new()
m.albedo_color = Color(0.55, 0.08, 0.06)
box.material = m
p.mesh = box
p.amount = 12
p.one_shot = true
p.explosiveness = 0.95
p.lifetime = 0.6
p.spread = 60.0
p.initial_velocity_min = 1.5
p.initial_velocity_max = 3.0
p.gravity = Vector3(0, -10, 0)
world.add_child(p)
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"},
"drowned": {"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])