1use glam::Vec3;
8use std::collections::HashMap;
9
10use super::{Timeline, TimelineAction};
11
12#[derive(Clone, Copy, Debug, Default)]
16pub struct ScriptCursor {
17 pub time: f32,
18}
19
20impl ScriptCursor {
21 pub fn advance(&mut self, dt: f32) -> f32 {
22 self.time += dt;
23 self.time
24 }
25
26 pub fn current(&self) -> f32 { self.time }
27}
28
29pub struct CutsceneScript {
45 name: String,
46 cursor: ScriptCursor,
47 entries: Vec<(f32, TimelineAction)>,
48 looping: bool,
49 speed: f32,
50}
51
52impl CutsceneScript {
53 pub fn new(name: impl Into<String>) -> Self {
54 Self {
55 name: name.into(),
56 cursor: ScriptCursor::default(),
57 entries: Vec::new(),
58 looping: false,
59 speed: 1.0,
60 }
61 }
62
63 pub fn at(mut self, time: f32) -> Self {
67 self.cursor.time = time;
68 self
69 }
70
71 pub fn wait(mut self, dt: f32) -> Self {
73 let t = self.cursor.time;
74 self.entries.push((t, TimelineAction::Wait { duration: dt }));
75 self.cursor.advance(dt);
76 self
77 }
78
79 pub fn label(mut self, name: impl Into<String>) -> Self {
81 let t = self.cursor.time;
82 self.entries.push((t, TimelineAction::GotoLabel { label: format!("__label_{}", name.into()) }));
83 self
84 }
85
86 pub fn fade_in(mut self, duration: f32) -> Self {
89 let t = self.cursor.time;
90 self.entries.push((t, TimelineAction::FadeIn { duration }));
91 self.cursor.advance(duration);
92 self
93 }
94
95 pub fn fade_out(mut self, duration: f32) -> Self {
96 let t = self.cursor.time;
97 self.entries.push((t, TimelineAction::FadeOut {
98 duration,
99 color: [0.0, 0.0, 0.0, 1.0],
100 }));
101 self.cursor.advance(duration);
102 self
103 }
104
105 pub fn fade_to(mut self, color: [f32; 4], duration: f32) -> Self {
106 let t = self.cursor.time;
107 self.entries.push((t, TimelineAction::FadeOut { color, duration }));
108 self.cursor.advance(duration);
109 self
110 }
111
112 pub fn flash(mut self, color: [f32; 4], duration: f32, intensity: f32) -> Self {
113 let t = self.cursor.time;
114 self.entries.push((t, TimelineAction::Flash { color, duration, intensity }));
115 self
117 }
118
119 pub fn bloom(mut self, enabled: bool, intensity: f32, duration: f32) -> Self {
120 let t = self.cursor.time;
121 self.entries.push((t, TimelineAction::SetBloom { enabled, intensity, duration }));
122 self
123 }
124
125 pub fn chromatic_aberration(mut self, amount: f32, duration: f32) -> Self {
126 let t = self.cursor.time;
127 self.entries.push((t, TimelineAction::SetChromaticAberration { amount, duration }));
128 self
129 }
130
131 pub fn film_grain(mut self, amount: f32) -> Self {
132 let t = self.cursor.time;
133 self.entries.push((t, TimelineAction::SetFilmGrain { amount }));
134 self
135 }
136
137 pub fn vignette(mut self, radius: f32, softness: f32, intensity: f32) -> Self {
138 let t = self.cursor.time;
139 self.entries.push((t, TimelineAction::SetVignette { radius, softness, intensity }));
140 self
141 }
142
143 pub fn camera_move(mut self, target: Vec3, duration: f32) -> Self {
146 let t = self.cursor.time;
147 self.entries.push((t, TimelineAction::CameraMoveTo { target, duration }));
148 self.cursor.advance(duration);
149 self
150 }
151
152 pub fn camera_look_at(mut self, target: Vec3, duration: f32) -> Self {
153 let t = self.cursor.time;
154 self.entries.push((t, TimelineAction::CameraLookAt { target, duration }));
155 self
156 }
157
158 pub fn camera_shake(mut self, intensity: f32, duration: f32, frequency: f32) -> Self {
159 let t = self.cursor.time;
160 self.entries.push((t, TimelineAction::CameraShake { intensity, duration, frequency }));
161 self
162 }
163
164 pub fn camera_zoom(mut self, zoom: f32, duration: f32) -> Self {
165 let t = self.cursor.time;
166 self.entries.push((t, TimelineAction::CameraZoom { zoom, duration }));
167 self
168 }
169
170 pub fn spawn(mut self, blueprint: impl Into<String>, position: Vec3) -> Self {
173 let t = self.cursor.time;
174 self.entries.push((t, TimelineAction::SpawnEntity {
175 blueprint: blueprint.into(),
176 position,
177 tag: None,
178 }));
179 self
180 }
181
182 pub fn spawn_tagged(mut self, blueprint: impl Into<String>, position: Vec3, tag: impl Into<String>) -> Self {
183 let t = self.cursor.time;
184 self.entries.push((t, TimelineAction::SpawnEntity {
185 blueprint: blueprint.into(),
186 position,
187 tag: Some(tag.into()),
188 }));
189 self
190 }
191
192 pub fn despawn_tag(mut self, tag: impl Into<String>) -> Self {
193 let t = self.cursor.time;
194 self.entries.push((t, TimelineAction::DespawnTag { tag: tag.into() }));
195 self
196 }
197
198 pub fn play_sfx(mut self, name: impl Into<String>, volume: f32) -> Self {
201 let t = self.cursor.time;
202 self.entries.push((t, TimelineAction::PlaySfx {
203 name: name.into(),
204 volume,
205 position: None,
206 }));
207 self
208 }
209
210 pub fn play_sfx_at(mut self, name: impl Into<String>, volume: f32, position: Vec3) -> Self {
211 let t = self.cursor.time;
212 self.entries.push((t, TimelineAction::PlaySfx {
213 name: name.into(),
214 volume,
215 position: Some(position),
216 }));
217 self
218 }
219
220 pub fn music_vibe(mut self, vibe: impl Into<String>) -> Self {
221 let t = self.cursor.time;
222 self.entries.push((t, TimelineAction::SetMusicVibe { vibe: vibe.into() }));
223 self
224 }
225
226 pub fn master_volume(mut self, volume: f32, duration: f32) -> Self {
227 let t = self.cursor.time;
228 self.entries.push((t, TimelineAction::SetMasterVolume { volume, duration }));
229 self
230 }
231
232 pub fn say(mut self, speaker: impl Into<String>, text: impl Into<String>, duration: f32) -> Self {
236 let t = self.cursor.time;
237 self.entries.push((t, TimelineAction::Dialogue {
238 speaker: speaker.into(),
239 text: text.into(),
240 duration: Some(duration),
241 }));
242 self.cursor.advance(duration);
243 self
244 }
245
246 pub fn say_async(mut self, speaker: impl Into<String>, text: impl Into<String>) -> Self {
248 let t = self.cursor.time;
249 self.entries.push((t, TimelineAction::Dialogue {
250 speaker: speaker.into(),
251 text: text.into(),
252 duration: None,
253 }));
254 self
255 }
256
257 pub fn title_card(mut self, text: impl Into<String>, subtitle: impl Into<String>, duration: f32) -> Self {
258 let t = self.cursor.time;
259 self.entries.push((t, TimelineAction::TitleCard {
260 text: text.into(),
261 subtitle: subtitle.into(),
262 duration,
263 }));
264 self.cursor.advance(duration);
265 self
266 }
267
268 pub fn notify(mut self, text: impl Into<String>, duration: f32) -> Self {
269 let t = self.cursor.time;
270 self.entries.push((t, TimelineAction::Notify { text: text.into(), duration }));
271 self
272 }
273
274 pub fn hide_dialogue(mut self) -> Self {
275 let t = self.cursor.time;
276 self.entries.push((t, TimelineAction::HideDialogue));
277 self
278 }
279
280 pub fn set_flag(mut self, name: impl Into<String>, value: bool) -> Self {
283 let t = self.cursor.time;
284 self.entries.push((t, TimelineAction::SetFlag { name: name.into(), value }));
285 self
286 }
287
288 pub fn callback(mut self, name: impl Into<String>) -> Self {
289 let t = self.cursor.time;
290 self.entries.push((t, TimelineAction::Callback {
291 name: name.into(),
292 args: HashMap::new(),
293 }));
294 self
295 }
296
297 pub fn callback_with_args(mut self, name: impl Into<String>, args: HashMap<String, String>) -> Self {
298 let t = self.cursor.time;
299 self.entries.push((t, TimelineAction::Callback { name: name.into(), args }));
300 self
301 }
302
303 pub fn parallel(mut self, actions: Vec<TimelineAction>) -> Self {
305 let t = self.cursor.time;
306 self.entries.push((t, TimelineAction::Parallel { actions }));
307 self
308 }
309
310 pub fn set_looping(mut self) -> Self { self.looping = true; self }
311 pub fn set_speed(mut self, s: f32) -> Self { self.speed = s; self }
312
313 pub fn build(mut self) -> Timeline {
315 let end_t = self.cursor.time;
317 self.entries.push((end_t, TimelineAction::End));
318
319 let mut tl = Timeline::new()
320 .named(self.name)
321 .with_speed(self.speed);
322 if self.looping { tl = tl.looping(); }
323
324 for (time, action) in self.entries {
325 tl.at(time, action);
326 }
327 tl
328 }
329}
330
331pub struct DialogueSequence {
335 script: CutsceneScript,
336 chars_per_second: f32,
337 pause_after: f32, }
339
340impl DialogueSequence {
341 pub fn new(name: impl Into<String>) -> Self {
342 Self {
343 script: CutsceneScript::new(name),
344 chars_per_second: 25.0,
345 pause_after: 0.4,
346 }
347 }
348
349 pub fn with_speed(mut self, chars_per_second: f32) -> Self {
350 self.chars_per_second = chars_per_second;
351 self
352 }
353
354 pub fn with_pause(mut self, pause: f32) -> Self {
355 self.pause_after = pause;
356 self
357 }
358
359 pub fn line(mut self, speaker: impl Into<String>, text: impl Into<String>) -> Self {
361 let t = text.into();
362 let dur = (t.chars().count() as f32 / self.chars_per_second).max(1.0);
363 let pause = self.pause_after;
364 self.script = self.script.say(speaker, t, dur).wait(pause);
365 self
366 }
367
368 pub fn build(self) -> Timeline {
369 self.script.hide_dialogue().build()
370 }
371}
372
373#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn script_builds_timeline() {
381 let tl = CutsceneScript::new("test")
382 .fade_in(1.0)
383 .wait(0.5)
384 .fade_out(0.5)
385 .build();
386 assert!(!tl.cues.is_empty());
387 assert!(tl.duration() > 0.0);
388 }
389
390 #[test]
391 fn script_cursor_advances() {
392 let tl = CutsceneScript::new("test")
393 .fade_in(1.0) .wait(2.0) .fade_out(0.5) .build();
397 let fade_out_time = tl.cues.iter().find(|c| {
399 matches!(&c.action, TimelineAction::FadeOut { .. })
400 }).map(|c| c.time).unwrap();
401 assert!((fade_out_time - 3.0).abs() < 0.01);
402 }
403
404 #[test]
405 fn dialogue_sequence() {
406 let tl = DialogueSequence::new("intro_dialogue")
407 .with_speed(30.0)
408 .line("Hero", "Hello there.")
409 .line("Villain", "I've been expecting you.")
410 .build();
411 let has_dialogue = tl.cues.iter().any(|c| matches!(&c.action, TimelineAction::Dialogue { .. }));
413 assert!(has_dialogue);
414 }
415
416 #[test]
417 fn script_at_positions_cursor() {
418 let tl = CutsceneScript::new("test")
419 .at(5.0)
420 .flash([1.0,0.0,0.0,1.0], 0.2, 1.0)
421 .build();
422 let flash_time = tl.cues.iter().find(|c| {
423 matches!(&c.action, TimelineAction::Flash { .. })
424 }).map(|c| c.time).unwrap();
425 assert!((flash_time - 5.0).abs() < 0.01);
426 }
427}