Old Tam speaks (Kokoro); CC0 birdsong, crickets and owls; no plants through floors; menu and marker fixes
This commit is contained in:
@@ -121,6 +121,7 @@ func start(w: Node3D, from_saved: Dictionary = {}) -> void:
|
||||
|
||||
func stop() -> void:
|
||||
_clear_outline()
|
||||
Narrator.stop()
|
||||
active = false
|
||||
world = null
|
||||
|
||||
@@ -139,9 +140,11 @@ func chapter_title(step: Dictionary) -> String:
|
||||
func _enter() -> void:
|
||||
if index >= steps.size():
|
||||
finished.emit()
|
||||
Narrator.say("tutorial_finish", String(finish.get("text", "")))
|
||||
return
|
||||
_setup(steps[index].get("setup", {}))
|
||||
step_started.emit(index, steps[index])
|
||||
Narrator.say("tutorial_" + String(steps[index].id), String(steps[index].text))
|
||||
_marker_t = 0.0
|
||||
_check() # (done early? it ticks straight away)
|
||||
|
||||
@@ -346,6 +349,14 @@ func marker_position() -> Vector3:
|
||||
|
||||
## Where the current step's marker goes: the nearest thing it asks for, or
|
||||
## INF (no marker, or you've got what it was for).
|
||||
## Whether the part of `goal` about `kind`:`id` (e.g. gained: stone) is done.
|
||||
func _part_met(goal: Dictionary, kind: String, id: String) -> bool:
|
||||
var parts: Array = goal.get("all", [goal])
|
||||
for p in parts:
|
||||
if p is Dictionary and String(p.get(kind, "")) == id:
|
||||
return _met(p)
|
||||
return false
|
||||
|
||||
func find_marker() -> Vector3:
|
||||
var s := current()
|
||||
if s.is_empty() or not s.has("marker") or world == null:
|
||||
@@ -360,9 +371,14 @@ func find_marker() -> Vector3:
|
||||
var best_d := INF
|
||||
var cands: Array = []
|
||||
if what.begins_with("pickup:"):
|
||||
var id := what.substr(7)
|
||||
# "pickup:stick|stone": the nearest of whichever you still need (the
|
||||
# step's "gained" goal for that item isn't met yet).
|
||||
var ids: Array = []
|
||||
for id in what.substr(7).split("|", false):
|
||||
if not _part_met(s.get("goal", {}), "gained", id):
|
||||
ids.append(id)
|
||||
for pk in world.props.pickups.values():
|
||||
if is_instance_valid(pk) and not pk.taken and pk.item_id == id:
|
||||
if is_instance_valid(pk) and not pk.taken and pk.item_id in ids:
|
||||
cands.append(pk)
|
||||
elif what in ["tree", "rock"]:
|
||||
for n in world.props.resource_nodes.values():
|
||||
|
||||
@@ -103,6 +103,7 @@ func _show(h: Dictionary) -> void:
|
||||
save_prefs()
|
||||
_cooldown = GAP
|
||||
shown.emit(h)
|
||||
Narrator.say("hint_" + String(h.id), String(h.text))
|
||||
|
||||
func _met(when: String) -> bool:
|
||||
var player: Node = world.player
|
||||
|
||||
@@ -42,10 +42,16 @@ 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)
|
||||
@@ -58,6 +64,15 @@ func _ready() -> void:
|
||||
_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))
|
||||
|
||||
@@ -204,7 +219,7 @@ func _indoors() -> bool:
|
||||
return sv != null and bool(sv.get("sheltered"))
|
||||
|
||||
func _target_db() -> float:
|
||||
return -6.0 if _indoors() else -2.0
|
||||
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:
|
||||
@@ -218,10 +233,36 @@ func _ambience(dt: float) -> void:
|
||||
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.8, 0.0001)), k)
|
||||
_crickets.volume_db = lerpf(_crickets.volume_db, linear_to_db(maxf(crickets * 0.7, 0.0001)), k)
|
||||
_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
|
||||
|
||||
66
scripts/core/narrator.gd
Normal file
66
scripts/core/narrator.gd
Normal file
@@ -0,0 +1,66 @@
|
||||
extends Node
|
||||
## Old Tam's voice: the recorded line for a tutorial step, his goodbye or one
|
||||
## of his tips (assets/audio/voice, made by tools/voice/make_voice.sh). A line
|
||||
## only plays if it was made from exactly the text on screen (lines.json), so
|
||||
## after an edit it stays quiet rather than saying the old words. A new line
|
||||
## cuts off the one before; the music dips while he talks. The Voice volume
|
||||
## (pause menu) turns him down or off. It pauses with the game.
|
||||
|
||||
const DIR := "res://assets/audio/voice/"
|
||||
|
||||
var lines: Dictionary = {} ## line id -> the key of the text it was made from
|
||||
var spoken: Array = [] ## ids said (tests; headless nothing is heard)
|
||||
var speaking := ""
|
||||
var _p: AudioStreamPlayer
|
||||
var _quiet := false
|
||||
|
||||
func _ready() -> void:
|
||||
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(DIR + "lines.json")) \
|
||||
if FileAccess.file_exists(DIR + "lines.json") else null
|
||||
if d is Dictionary:
|
||||
lines = d
|
||||
_quiet = DisplayServer.get_name() == "headless"
|
||||
if _quiet:
|
||||
return
|
||||
_p = AudioStreamPlayer.new()
|
||||
_p.bus = "Voice"
|
||||
add_child(_p)
|
||||
_p.finished.connect(_done)
|
||||
|
||||
## The same key make_voice.py writes: the first 16 hex digits of the text's SHA-1.
|
||||
static func text_key(text: String) -> String:
|
||||
return text.sha1_text().substr(0, 16)
|
||||
|
||||
## Whether there's a recording of `text` for `id`.
|
||||
func has_line(id: String, text: String) -> bool:
|
||||
return String(lines.get(id, "")) == text_key(text)
|
||||
|
||||
## Says line `id` if it's the recording of `text`. True if it's being said.
|
||||
func say(id: String, text: String) -> bool:
|
||||
if not has_line(id, text):
|
||||
return false
|
||||
spoken.append(id)
|
||||
speaking = id
|
||||
if _quiet:
|
||||
return true
|
||||
var st: Variant = load(DIR + id + ".ogg")
|
||||
if not (st is AudioStream):
|
||||
speaking = ""
|
||||
return false
|
||||
_p.stop()
|
||||
_p.stream = st
|
||||
_p.play()
|
||||
Music.duck = true
|
||||
return true
|
||||
|
||||
func stop() -> void:
|
||||
if _p != null:
|
||||
_p.stop()
|
||||
_done()
|
||||
|
||||
func is_speaking() -> bool:
|
||||
return _p != null and _p.playing
|
||||
|
||||
func _done() -> void:
|
||||
speaking = ""
|
||||
Music.duck = false
|
||||
@@ -88,8 +88,8 @@ const SOUNDS := {
|
||||
"crow_caw": {"set": [C + "crow_caw_%d.ogg", 2], "db": -4.0, "pitch": [0.92, 1.1], "reach": 40.0},
|
||||
"fox_yip": {"set": [C + "fox_yip_%d.ogg", 2], "db": -4.0, "pitch": [0.92, 1.12], "reach": 30.0},
|
||||
}
|
||||
const BUSES := ["Music", "SFX", "Ambience"]
|
||||
const DEFAULT_VOLUMES := {"Master": 0.85, "Music": 0.6, "SFX": 0.9, "Ambience": 0.7}
|
||||
const BUSES := ["Music", "SFX", "Ambience", "Voice"]
|
||||
const DEFAULT_VOLUMES := {"Master": 0.85, "Music": 0.6, "SFX": 0.9, "Ambience": 0.7, "Voice": 1.0}
|
||||
const POOL_3D := 20
|
||||
const POOL_2D := 6
|
||||
|
||||
|
||||
Reference in New Issue
Block a user