Crookfinger's voice: he babbles in a squeaky imp language in the rhythm of his hints, important messages and the mission briefings (can be switched off in the sound settings)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
776c9ad9de
commit
ada14951c4
9 files changed
+118
-3
No files matched your search
+11
-1
@@ -54,14 +54,24 @@ pub enum Sfx {
|
||||
Convert,
|
||||
Slap,
|
||||
Punch,
|
||||
// Crookfinger's voice: squeaky syllables
|
||||
Voice1,
|
||||
Voice2,
|
||||
Voice3,
|
||||
Voice4,
|
||||
Voice5,
|
||||
Voice6,
|
||||
}
|
||||
|
||||
pub const SFX_ALL: [Sfx; 36] = [
|
||||
pub const VOICE_SYLLABLES: [Sfx; 6] = [Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6];
|
||||
|
||||
pub const SFX_ALL: [Sfx; 42] = [
|
||||
Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit,
|
||||
Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive,
|
||||
Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp,
|
||||
Sfx::Pickup, Sfx::Drop, Sfx::Payday, Sfx::FlagSet, Sfx::Victory, Sfx::Defeat, Sfx::Click, Sfx::Error,
|
||||
Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, Sfx::Slap, Sfx::Punch,
|
||||
Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6,
|
||||
];
|
||||
|
||||
impl Sfx {
|
||||
|
||||
+19
@@ -30,6 +30,7 @@ mod sound;
|
||||
mod spells;
|
||||
mod touch;
|
||||
mod ui;
|
||||
mod voice;
|
||||
mod wilds;
|
||||
mod xm;
|
||||
|
||||
@@ -402,6 +403,8 @@ async fn main() {
|
||||
// until the mouse has really moved its position is (0, 0): no edge scrolling before that
|
||||
let mut mouse_moved = false; // automatic mode: touch interface after a touch, desktop after a mouse click
|
||||
let mut autosave_t = 0.0f32;
|
||||
let mut voice = voice::Voice::new();
|
||||
let mut was_briefing = false;
|
||||
let mut cache_t = 0.0f32;
|
||||
|
||||
loop {
|
||||
@@ -1028,8 +1031,24 @@ async fn main() {
|
||||
);
|
||||
music.update(&app.game, on_title, (app.cam.tx, app.cam.tz));
|
||||
for m in app.game.msgs.drain(..) {
|
||||
if app.settings.voice && voice::speaks(m.key) {
|
||||
voice.say(&i18n::trf(m.key, &m.args), now, 12);
|
||||
}
|
||||
ui.log.push((m, now));
|
||||
}
|
||||
// a briefing opens: Crookfinger tells the story
|
||||
let briefing = matches!(menu.page, Some(Page::Briefing { .. }));
|
||||
if briefing && !was_briefing && app.settings.voice {
|
||||
if let Some(Page::Briefing { mission, .. }) = menu.page {
|
||||
let key = campaign::missions().get(mission).map(|d| format!("camp.{}.story", d.key)).unwrap_or_default();
|
||||
voice.say(&i18n::tr(&key), now, 22);
|
||||
}
|
||||
}
|
||||
if !briefing && was_briefing {
|
||||
voice.stop();
|
||||
}
|
||||
was_briefing = briefing;
|
||||
voice.update(now, if app.settings.muted { 0.0 } else { app.settings.volume });
|
||||
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, (mx, my), touch_mode, now);
|
||||
next_frame().await
|
||||
|
||||
+5
-1
@@ -50,12 +50,13 @@ pub enum Item {
|
||||
GameSpeed,
|
||||
Hints,
|
||||
TouchUi,
|
||||
Voice,
|
||||
}
|
||||
|
||||
fn tab_items(tab: usize) -> &'static [Item] {
|
||||
use Item::*;
|
||||
match tab {
|
||||
0 => &[Language, Volume, MusicVolume, Mute],
|
||||
0 => &[Language, Volume, MusicVolume, Mute, Voice],
|
||||
1 => &[RotateSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
|
||||
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
|
||||
3 => &[GameSpeed, Hints, TouchUi],
|
||||
@@ -81,6 +82,7 @@ fn item_label(it: Item) -> String {
|
||||
Item::GameSpeed => "set.game_speed",
|
||||
Item::Hints => "set.hints",
|
||||
Item::TouchUi => "set.touch_ui",
|
||||
Item::Voice => "set.voice",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ fn item_value(it: Item, s: &Settings) -> String {
|
||||
Item::Volume => pct(s.volume),
|
||||
Item::MusicVolume => pct(s.music_volume),
|
||||
Item::Mute => onoff(s.muted),
|
||||
Item::Voice => onoff(s.voice),
|
||||
Item::RotateSpeed => pct(s.rotate_speed),
|
||||
Item::ZoomSpeed => pct(s.zoom_speed),
|
||||
Item::EdgeScroll => onoff(s.edge_scroll),
|
||||
@@ -155,6 +158,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
Item::Volume => s.volume = step(s.volume, d, 0.0, 1.0, 0.1),
|
||||
Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1),
|
||||
Item::Mute => s.muted = !s.muted,
|
||||
Item::Voice => s.voice = !s.voice,
|
||||
Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25),
|
||||
Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25),
|
||||
Item::EdgeScroll => s.edge_scroll = !s.edge_scroll,
|
||||
|
||||
@@ -136,6 +136,7 @@ pub struct Settings {
|
||||
pub volume: f32,
|
||||
pub music_volume: f32,
|
||||
pub muted: bool,
|
||||
pub voice: bool, // Crookfinger's babbling voice
|
||||
// camera
|
||||
pub rotate_speed: f32,
|
||||
pub zoom_speed: f32,
|
||||
@@ -165,6 +166,7 @@ impl Default for Settings {
|
||||
volume: 0.7,
|
||||
music_volume: 0.6,
|
||||
muted: false,
|
||||
voice: true,
|
||||
rotate_speed: 1.0,
|
||||
zoom_speed: 1.0,
|
||||
edge_scroll: true,
|
||||
|
||||
@@ -157,6 +157,13 @@ fn def(s: Sfx, r: f32) -> Vec<f32> {
|
||||
Sfx::Convert => l(&arp(Saw, &[220.0, 262.0, 330.0, 440.0], 0.1, 0.22), 47),
|
||||
Sfx::Slap => mix(vec![l(&[seg(Noise, 5000.0, 2500.0, 0.06, 0.5, 40.0)], 48), l(&[seg(Square(0.5), 900.0, 500.0, 0.05, 0.12, 30.0)], 49)]),
|
||||
Sfx::Punch => mix(vec![l(&[seg(Tri, 160.0, 60.0, 0.15, 0.8, 18.0)], 50), l(&[seg(Noise, 3000.0, 800.0, 0.08, 0.4, 30.0)], 51)]),
|
||||
// Crookfinger's syllables: a nasal squeak gliding up or down, some with a hissy consonant
|
||||
Sfx::Voice1 => mix(vec![l(&[vib(seg(Square(0.2), 620.0, 760.0, 0.08, 0.16, 6.0), 0.04)], 60), l(&[seg(Noise, 6000.0, 4000.0, 0.02, 0.05, 40.0)], 61)]),
|
||||
Sfx::Voice2 => l(&[vib(seg(Square(0.18), 780.0, 600.0, 0.09, 0.16, 6.0), 0.05)], 62),
|
||||
Sfx::Voice3 => mix(vec![l(&[vib(seg(Square(0.22), 520.0, 640.0, 0.07, 0.15, 7.0), 0.03)], 63), l(&[seg(Tri, 260.0, 300.0, 0.07, 0.12, 8.0)], 64)]),
|
||||
Sfx::Voice4 => l(&[seg(Square(0.15), 900.0, 980.0, 0.05, 0.13, 10.0), vib(seg(Square(0.2), 700.0, 560.0, 0.06, 0.14, 8.0), 0.04)], 65),
|
||||
Sfx::Voice5 => mix(vec![l(&[vib(seg(Square(0.25), 470.0, 560.0, 0.1, 0.15, 5.0), 0.06)], 66), l(&[seg(Noise, 3000.0, 2000.0, 0.025, 0.06, 30.0)], 67)]),
|
||||
Sfx::Voice6 => l(&[vib(seg(Square(0.2), 680.0, 820.0, 0.06, 0.15, 9.0), 0.05), seg(Square(0.2), 820.0, 700.0, 0.04, 0.12, 12.0)], 68),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Crookfinger's voice: he "speaks" in a squeaky made-up imp language - short synthesised
|
||||
// syllables in the rhythm of the text (the text itself is shown as usual).
|
||||
|
||||
use crate::audio;
|
||||
use crate::game::VOICE_SYLLABLES;
|
||||
|
||||
pub struct Voice {
|
||||
queue: Vec<(f64, usize)>, // time, sound index
|
||||
busy_until: f64,
|
||||
}
|
||||
|
||||
// which messages Crookfinger says out loud
|
||||
pub fn speaks(key: &str) -> bool {
|
||||
key.starts_with("hint.")
|
||||
|| key.starts_with("msg.wave_warn")
|
||||
|| matches!(
|
||||
key,
|
||||
"msg.payday_soon" | "msg.researched" | "msg.mission_start" | "msg.mission_won" | "msg.rune_chest" | "msg.trap_made" | "msg.revenant_rises" | "msg.altar_gift" | "msg.altar_blessing" | "msg.brawl" | "msg.alarm" | "msg.creature_freed" | "msg.spring_taken"
|
||||
)
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
pub fn new() -> Voice {
|
||||
Voice { queue: Vec::new(), busy_until: 0.0 }
|
||||
}
|
||||
|
||||
// babble a text: one syllable per vowel group, pauses at spaces and punctuation
|
||||
pub fn say(&mut self, text: &str, now: f64, max_syllables: usize) {
|
||||
if now < self.busy_until {
|
||||
return;
|
||||
}
|
||||
let mut t = now + 0.05;
|
||||
let mut n = 0;
|
||||
let mut h: u32 = 2166136261;
|
||||
let mut in_vowel = false;
|
||||
for ch in text.chars() {
|
||||
h = (h ^ ch as u32).wrapping_mul(16777619);
|
||||
let vowel = "aeiouäöüyAEIOUÄÖÜY".contains(ch);
|
||||
if vowel && !in_vowel {
|
||||
let v = VOICE_SYLLABLES[(h >> 7) as usize % VOICE_SYLLABLES.len()];
|
||||
self.queue.push((t, v.index()));
|
||||
t += 0.085 + ((h >> 13) % 4) as f64 * 0.012;
|
||||
n += 1;
|
||||
if n >= max_syllables {
|
||||
break;
|
||||
}
|
||||
}
|
||||
in_vowel = vowel;
|
||||
match ch {
|
||||
' ' => t += 0.035,
|
||||
',' | ';' | '-' => t += 0.14,
|
||||
'.' | '!' | '?' => t += 0.28,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.busy_until = t + 4.0;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: f64, volume: f32) {
|
||||
let due: Vec<usize> = self.queue.iter().filter(|q| q.0 <= now).map(|q| q.1).collect();
|
||||
self.queue.retain(|q| q.0 > now);
|
||||
for i in due {
|
||||
audio::play_sfx(i, 0.75 * volume.max(0.0), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user