Skip to main content

proof_engine/tween/
tween_manager.rs

1//! TweenManager — central hub for managing active tweens across the entire game.
2//!
3//! The TweenManager owns a pool of `ActiveTween`s, each targeting a specific
4//! property in the scene (glyph position, camera FOV, bar fill, etc.).
5//! Every frame, `tick(dt)` advances all tweens and applies their values.
6//!
7//! Features:
8//!   - Start / start_delayed / cancel by ID
9//!   - TweenTarget enum covering glyphs, camera, screen, bars, and custom lambdas
10//!   - Chaining: on_complete callbacks that can start new tweens
11//!   - Group cancellation by tag
12//!   - Automatic cleanup of completed tweens
13
14use glam::{Vec2, Vec3, Vec4};
15use std::collections::HashMap;
16
17use super::easing::Easing;
18use super::{Tween, TweenState, Lerp};
19use crate::glyph::GlyphId;
20
21// ── TweenId ─────────────────────────────────────────────────────────────────
22
23/// Opaque handle to a running tween.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct TweenId(pub u64);
26
27// ── BarId ───────────────────────────────────────────────────────────────────
28
29/// Opaque handle to a UI bar (HP, MP, XP, etc.).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct BarId(pub u32);
32
33// ── TweenTarget ─────────────────────────────────────────────────────────────
34
35/// What property an active tween drives.
36pub enum TweenTarget {
37    // ── Glyph properties ────────────────────────────────────────────────
38    GlyphPositionX(GlyphId),
39    GlyphPositionY(GlyphId),
40    GlyphPositionZ(GlyphId),
41    GlyphScale(GlyphId),
42    GlyphScaleX(GlyphId),
43    GlyphScaleY(GlyphId),
44    GlyphAlpha(GlyphId),
45    GlyphEmission(GlyphId),
46    GlyphRotation(GlyphId),
47    GlyphColorR(GlyphId),
48    GlyphColorG(GlyphId),
49    GlyphColorB(GlyphId),
50    GlyphGlowRadius(GlyphId),
51    GlyphTemperature(GlyphId),
52    GlyphEntropy(GlyphId),
53
54    // ── Camera ──────────────────────────────────────────────────────────
55    CameraFov,
56    CameraPositionX,
57    CameraPositionY,
58    CameraPositionZ,
59    CameraTargetX,
60    CameraTargetY,
61    CameraTargetZ,
62    CameraTrauma,
63
64    // ── Screen ──────────────────────────────────────────────────────────
65    ScreenFade,
66    ScreenShake,
67    ScreenBloom,
68    ScreenChromaticAberration,
69    ScreenVignette,
70    ScreenSaturation,
71    ScreenHueShift,
72
73    // ── UI Bars ─────────────────────────────────────────────────────────
74    BarFillPercent(BarId),
75    BarGhostPercent(BarId),
76
77    // ── Custom ──────────────────────────────────────────────────────────
78    /// A custom target driven by a closure. The closure receives the current
79    /// tween value each frame.
80    Custom(Box<dyn FnMut(f32) + Send>),
81
82    /// Named float property — stored in a shared value map.
83    Named(String),
84}
85
86// ── ActiveTween ─────────────────────────────────────────────────────────────
87
88/// A single tween that is currently running or waiting (delayed).
89pub struct ActiveTween {
90    pub id: TweenId,
91    pub target: TweenTarget,
92    pub state: TweenState<f32>,
93    /// Optional delay before the tween starts.
94    pub delay_remaining: f32,
95    /// Optional tag for group operations (e.g., "combat", "menu", "screen").
96    pub tag: Option<String>,
97    /// Callback invoked when the tween completes.
98    pub on_complete: Option<Box<dyn FnOnce(&mut TweenManager) + Send>>,
99    /// If true, the tween has been cancelled and should be removed.
100    pub cancelled: bool,
101}
102
103// ── TweenManager ────────────────────────────────────────────────────────────
104
105/// Central tween manager. Owns all active tweens and provides the game-facing API.
106pub struct TweenManager {
107    /// Active tweens, keyed by ID for fast lookup.
108    tweens: Vec<ActiveTween>,
109    /// Monotonically increasing ID counter.
110    next_id: u64,
111    /// Named float values driven by `TweenTarget::Named`.
112    pub named_values: HashMap<String, f32>,
113    /// Screen-level values that the renderer can read each frame.
114    pub screen_fade: f32,
115    pub screen_shake: f32,
116    pub screen_bloom_override: Option<f32>,
117    pub screen_chromatic_override: Option<f32>,
118    pub screen_vignette_override: Option<f32>,
119    pub screen_saturation_override: Option<f32>,
120    pub screen_hue_shift: f32,
121    /// Bar fill values, keyed by BarId.
122    pub bar_values: HashMap<BarId, f32>,
123    pub bar_ghost_values: HashMap<BarId, f32>,
124    /// Camera overrides (None = no override, renderer uses defaults).
125    pub camera_fov_override: Option<f32>,
126    pub camera_position_override: [Option<f32>; 3],
127    pub camera_target_override: [Option<f32>; 3],
128    pub camera_trauma_add: f32,
129}
130
131impl TweenManager {
132    pub fn new() -> Self {
133        Self {
134            tweens: Vec::with_capacity(256),
135            next_id: 1,
136            named_values: HashMap::new(),
137            screen_fade: 0.0,
138            screen_shake: 0.0,
139            screen_bloom_override: None,
140            screen_chromatic_override: None,
141            screen_vignette_override: None,
142            screen_saturation_override: None,
143            screen_hue_shift: 0.0,
144            bar_values: HashMap::new(),
145            bar_ghost_values: HashMap::new(),
146            camera_fov_override: None,
147            camera_position_override: [None; 3],
148            camera_target_override: [None; 3],
149            camera_trauma_add: 0.0,
150        }
151    }
152
153    // ── Starting tweens ─────────────────────────────────────────────────
154
155    /// Start a tween immediately.
156    pub fn start(
157        &mut self,
158        target: TweenTarget,
159        from: f32,
160        to: f32,
161        duration: f32,
162        easing: Easing,
163    ) -> TweenId {
164        self.start_inner(target, from, to, duration, easing, 0.0, None, None)
165    }
166
167    /// Start a tween with a delay.
168    pub fn start_delayed(
169        &mut self,
170        target: TweenTarget,
171        from: f32,
172        to: f32,
173        duration: f32,
174        delay: f32,
175        easing: Easing,
176    ) -> TweenId {
177        self.start_inner(target, from, to, duration, easing, delay, None, None)
178    }
179
180    /// Start a tween with a completion callback.
181    pub fn start_with_callback(
182        &mut self,
183        target: TweenTarget,
184        from: f32,
185        to: f32,
186        duration: f32,
187        easing: Easing,
188        on_complete: impl FnOnce(&mut TweenManager) + Send + 'static,
189    ) -> TweenId {
190        self.start_inner(target, from, to, duration, easing, 0.0, None, Some(Box::new(on_complete)))
191    }
192
193    /// Start a tagged tween (for group cancellation).
194    pub fn start_tagged(
195        &mut self,
196        target: TweenTarget,
197        from: f32,
198        to: f32,
199        duration: f32,
200        easing: Easing,
201        tag: &str,
202    ) -> TweenId {
203        self.start_inner(target, from, to, duration, easing, 0.0, Some(tag.to_string()), None)
204    }
205
206    /// Full-featured tween start.
207    fn start_inner(
208        &mut self,
209        target: TweenTarget,
210        from: f32,
211        to: f32,
212        duration: f32,
213        easing: Easing,
214        delay: f32,
215        tag: Option<String>,
216        on_complete: Option<Box<dyn FnOnce(&mut TweenManager) + Send>>,
217    ) -> TweenId {
218        let id = TweenId(self.next_id);
219        self.next_id += 1;
220
221        let tween = Tween::new(from, to, duration, easing);
222        let state = TweenState::new(tween);
223
224        self.tweens.push(ActiveTween {
225            id,
226            target,
227            state,
228            delay_remaining: delay,
229            tag,
230            on_complete,
231            cancelled: false,
232        });
233
234        id
235    }
236
237    // ── Cancellation ────────────────────────────────────────────────────
238
239    /// Cancel a specific tween by ID.
240    pub fn cancel(&mut self, id: TweenId) {
241        for t in &mut self.tweens {
242            if t.id == id {
243                t.cancelled = true;
244                break;
245            }
246        }
247    }
248
249    /// Cancel all tweens with a given tag.
250    pub fn cancel_tag(&mut self, tag: &str) {
251        for t in &mut self.tweens {
252            if t.tag.as_deref() == Some(tag) {
253                t.cancelled = true;
254            }
255        }
256    }
257
258    /// Cancel all tweens targeting a specific glyph.
259    pub fn cancel_glyph(&mut self, glyph_id: GlyphId) {
260        for t in &mut self.tweens {
261            let targets_glyph = matches!(
262                &t.target,
263                TweenTarget::GlyphPositionX(id)
264                | TweenTarget::GlyphPositionY(id)
265                | TweenTarget::GlyphPositionZ(id)
266                | TweenTarget::GlyphScale(id)
267                | TweenTarget::GlyphScaleX(id)
268                | TweenTarget::GlyphScaleY(id)
269                | TweenTarget::GlyphAlpha(id)
270                | TweenTarget::GlyphEmission(id)
271                | TweenTarget::GlyphRotation(id)
272                | TweenTarget::GlyphColorR(id)
273                | TweenTarget::GlyphColorG(id)
274                | TweenTarget::GlyphColorB(id)
275                | TweenTarget::GlyphGlowRadius(id)
276                | TweenTarget::GlyphTemperature(id)
277                | TweenTarget::GlyphEntropy(id)
278                    if *id == glyph_id
279            );
280            if targets_glyph {
281                t.cancelled = true;
282            }
283        }
284    }
285
286    /// Cancel all active tweens.
287    pub fn cancel_all(&mut self) {
288        for t in &mut self.tweens {
289            t.cancelled = true;
290        }
291    }
292
293    // ── Queries ─────────────────────────────────────────────────────────
294
295    /// Allocate a new ID and push a raw ActiveTween. Used by game_tweens presets.
296    pub fn push_raw(&mut self, target: TweenTarget, state: super::TweenState<f32>, delay: f32, tag: Option<String>, on_complete: Option<Box<dyn FnOnce(&mut TweenManager) + Send>>) -> TweenId {
297        let id = TweenId(self.next_id);
298        self.next_id += 1;
299        self.tweens.push(ActiveTween {
300            id, target, state,
301            delay_remaining: delay,
302            tag,
303            on_complete,
304            cancelled: false,
305        });
306        id
307    }
308
309    /// Check if a tween is still active.
310    pub fn is_active(&self, id: TweenId) -> bool {
311        self.tweens.iter().any(|t| t.id == id && !t.cancelled && !t.state.done)
312    }
313
314    /// Number of active tweens.
315    pub fn active_count(&self) -> usize {
316        self.tweens.iter().filter(|t| !t.cancelled && !t.state.done).count()
317    }
318
319    /// Get a named value (returns 0.0 if not set).
320    pub fn get_named(&self, name: &str) -> f32 {
321        self.named_values.get(name).copied().unwrap_or(0.0)
322    }
323
324    /// Get a bar fill value.
325    pub fn get_bar(&self, bar: BarId) -> f32 {
326        self.bar_values.get(&bar).copied().unwrap_or(0.0)
327    }
328
329    /// Get a bar ghost value.
330    pub fn get_bar_ghost(&self, bar: BarId) -> f32 {
331        self.bar_ghost_values.get(&bar).copied().unwrap_or(0.0)
332    }
333
334    // ── Tick ────────────────────────────────────────────────────────────
335
336    /// Advance all tweens by `dt` seconds.
337    ///
338    /// This method:
339    /// 1. Decrements delays on waiting tweens
340    /// 2. Ticks active tweens and reads their current value
341    /// 3. Applies each value to its target (stored in the manager's output fields)
342    /// 4. Collects completed tweens and runs their on_complete callbacks
343    /// 5. Removes completed/cancelled tweens
344    ///
345    /// **Important**: This does NOT directly modify the scene. The caller must
346    /// read the manager's output fields (`screen_fade`, `bar_values`, etc.)
347    /// and apply them to the engine/scene each frame.
348    pub fn tick(&mut self, dt: f32) {
349        // Reset per-frame accumulators.
350        self.camera_trauma_add = 0.0;
351
352        // Collect completed callback indices.
353        let mut completed_callbacks: Vec<Box<dyn FnOnce(&mut TweenManager) + Send>> = Vec::new();
354
355        for tween in &mut self.tweens {
356            if tween.cancelled {
357                continue;
358            }
359
360            // Handle delay.
361            if tween.delay_remaining > 0.0 {
362                tween.delay_remaining -= dt;
363                if tween.delay_remaining > 0.0 {
364                    continue;
365                }
366                // Overflow the delay into the tween.
367                let overflow = -tween.delay_remaining;
368                tween.delay_remaining = 0.0;
369                tween.state.tick(overflow);
370            } else {
371                tween.state.tick(dt);
372            }
373
374            let value = tween.state.value();
375
376            // Apply value to target.
377            match &mut tween.target {
378                TweenTarget::GlyphPositionX(_)
379                | TweenTarget::GlyphPositionY(_)
380                | TweenTarget::GlyphPositionZ(_)
381                | TweenTarget::GlyphScale(_)
382                | TweenTarget::GlyphScaleX(_)
383                | TweenTarget::GlyphScaleY(_)
384                | TweenTarget::GlyphAlpha(_)
385                | TweenTarget::GlyphEmission(_)
386                | TweenTarget::GlyphRotation(_)
387                | TweenTarget::GlyphColorR(_)
388                | TweenTarget::GlyphColorG(_)
389                | TweenTarget::GlyphColorB(_)
390                | TweenTarget::GlyphGlowRadius(_)
391                | TweenTarget::GlyphTemperature(_)
392                | TweenTarget::GlyphEntropy(_) => {
393                    // Glyph targets are applied externally by the caller via apply_to_scene().
394                }
395
396                TweenTarget::CameraFov => {
397                    self.camera_fov_override = Some(value);
398                }
399                TweenTarget::CameraPositionX => {
400                    self.camera_position_override[0] = Some(value);
401                }
402                TweenTarget::CameraPositionY => {
403                    self.camera_position_override[1] = Some(value);
404                }
405                TweenTarget::CameraPositionZ => {
406                    self.camera_position_override[2] = Some(value);
407                }
408                TweenTarget::CameraTargetX => {
409                    self.camera_target_override[0] = Some(value);
410                }
411                TweenTarget::CameraTargetY => {
412                    self.camera_target_override[1] = Some(value);
413                }
414                TweenTarget::CameraTargetZ => {
415                    self.camera_target_override[2] = Some(value);
416                }
417                TweenTarget::CameraTrauma => {
418                    self.camera_trauma_add = value;
419                }
420
421                TweenTarget::ScreenFade => {
422                    self.screen_fade = value;
423                }
424                TweenTarget::ScreenShake => {
425                    self.screen_shake = value;
426                }
427                TweenTarget::ScreenBloom => {
428                    self.screen_bloom_override = Some(value);
429                }
430                TweenTarget::ScreenChromaticAberration => {
431                    self.screen_chromatic_override = Some(value);
432                }
433                TweenTarget::ScreenVignette => {
434                    self.screen_vignette_override = Some(value);
435                }
436                TweenTarget::ScreenSaturation => {
437                    self.screen_saturation_override = Some(value);
438                }
439                TweenTarget::ScreenHueShift => {
440                    self.screen_hue_shift = value;
441                }
442
443                TweenTarget::BarFillPercent(bar_id) => {
444                    self.bar_values.insert(*bar_id, value);
445                }
446                TweenTarget::BarGhostPercent(bar_id) => {
447                    self.bar_ghost_values.insert(*bar_id, value);
448                }
449
450                TweenTarget::Custom(ref mut f) => {
451                    f(value);
452                }
453
454                TweenTarget::Named(ref name) => {
455                    self.named_values.insert(name.clone(), value);
456                }
457            }
458
459            // Check completion.
460            if tween.state.done {
461                if let Some(cb) = tween.on_complete.take() {
462                    completed_callbacks.push(cb);
463                }
464            }
465        }
466
467        // Remove completed/cancelled tweens.
468        self.tweens.retain(|t| !t.cancelled && !t.state.done);
469
470        // Run completion callbacks (these may start new tweens).
471        for cb in completed_callbacks {
472            cb(self);
473        }
474    }
475
476    /// Apply glyph-targeting tweens to the scene's glyph pool.
477    ///
478    /// Call this after `tick()` and before rendering. The caller provides
479    /// a mutable closure that can look up and modify glyphs by ID.
480    pub fn apply_to_glyphs<F>(&self, mut apply: F)
481    where
482        F: FnMut(GlyphId, &str, f32),
483    {
484        for tween in &self.tweens {
485            if tween.cancelled || tween.delay_remaining > 0.0 {
486                continue;
487            }
488            let value = tween.state.value();
489            match &tween.target {
490                TweenTarget::GlyphPositionX(id) => apply(*id, "position_x", value),
491                TweenTarget::GlyphPositionY(id) => apply(*id, "position_y", value),
492                TweenTarget::GlyphPositionZ(id) => apply(*id, "position_z", value),
493                TweenTarget::GlyphScale(id) => apply(*id, "scale", value),
494                TweenTarget::GlyphScaleX(id) => apply(*id, "scale_x", value),
495                TweenTarget::GlyphScaleY(id) => apply(*id, "scale_y", value),
496                TweenTarget::GlyphAlpha(id) => apply(*id, "alpha", value),
497                TweenTarget::GlyphEmission(id) => apply(*id, "emission", value),
498                TweenTarget::GlyphRotation(id) => apply(*id, "rotation", value),
499                TweenTarget::GlyphColorR(id) => apply(*id, "color_r", value),
500                TweenTarget::GlyphColorG(id) => apply(*id, "color_g", value),
501                TweenTarget::GlyphColorB(id) => apply(*id, "color_b", value),
502                TweenTarget::GlyphGlowRadius(id) => apply(*id, "glow_radius", value),
503                TweenTarget::GlyphTemperature(id) => apply(*id, "temperature", value),
504                TweenTarget::GlyphEntropy(id) => apply(*id, "entropy", value),
505                _ => {}
506            }
507        }
508    }
509
510    /// Reset all screen/camera overrides. Call at the start of each frame
511    /// before `tick()` if you want tweens to be the sole source of overrides.
512    pub fn reset_overrides(&mut self) {
513        self.screen_bloom_override = None;
514        self.screen_chromatic_override = None;
515        self.screen_vignette_override = None;
516        self.screen_saturation_override = None;
517        self.screen_hue_shift = 0.0;
518        self.camera_fov_override = None;
519        self.camera_position_override = [None; 3];
520        self.camera_target_override = [None; 3];
521    }
522}
523
524impl Default for TweenManager {
525    fn default() -> Self {
526        Self::new()
527    }
528}
529
530// ── Tests ───────────────────────────────────────────────────────────────────
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    #[test]
537    fn start_and_tick() {
538        let mut mgr = TweenManager::new();
539        let id = mgr.start(TweenTarget::ScreenFade, 0.0, 1.0, 1.0, Easing::Linear);
540        assert!(mgr.is_active(id));
541        mgr.tick(0.5);
542        assert!((mgr.screen_fade - 0.5).abs() < 0.05);
543        mgr.tick(0.6);
544        assert!(!mgr.is_active(id));
545        assert!((mgr.screen_fade - 1.0).abs() < 0.05);
546    }
547
548    #[test]
549    fn delayed_start() {
550        let mut mgr = TweenManager::new();
551        mgr.start_delayed(TweenTarget::ScreenFade, 0.0, 1.0, 1.0, 0.5, Easing::Linear);
552        mgr.tick(0.3);
553        assert!((mgr.screen_fade - 0.0).abs() < 0.01, "Should still be in delay");
554        mgr.tick(0.3); // 0.6 total, 0.1 past delay
555        assert!(mgr.screen_fade > 0.0, "Should have started");
556    }
557
558    #[test]
559    fn cancel_by_id() {
560        let mut mgr = TweenManager::new();
561        let id = mgr.start(TweenTarget::ScreenFade, 0.0, 1.0, 1.0, Easing::Linear);
562        mgr.cancel(id);
563        mgr.tick(0.5);
564        assert!(!mgr.is_active(id));
565    }
566
567    #[test]
568    fn cancel_by_tag() {
569        let mut mgr = TweenManager::new();
570        mgr.start_tagged(TweenTarget::ScreenFade, 0.0, 1.0, 1.0, Easing::Linear, "combat");
571        mgr.start_tagged(TweenTarget::ScreenShake, 0.0, 1.0, 1.0, Easing::Linear, "combat");
572        mgr.start_tagged(TweenTarget::ScreenBloom, 0.0, 1.0, 1.0, Easing::Linear, "menu");
573        assert_eq!(mgr.active_count(), 3);
574        mgr.cancel_tag("combat");
575        mgr.tick(0.0);
576        assert_eq!(mgr.active_count(), 1);
577    }
578
579    #[test]
580    fn bar_values() {
581        let mut mgr = TweenManager::new();
582        let bar = BarId(0);
583        mgr.start(TweenTarget::BarFillPercent(bar), 1.0, 0.5, 0.5, Easing::Linear);
584        mgr.tick(0.25);
585        let val = mgr.get_bar(bar);
586        assert!((val - 0.75).abs() < 0.05);
587    }
588
589    #[test]
590    fn named_values() {
591        let mut mgr = TweenManager::new();
592        mgr.start(TweenTarget::Named("test".to_string()), 0.0, 10.0, 1.0, Easing::Linear);
593        mgr.tick(0.5);
594        assert!((mgr.get_named("test") - 5.0).abs() < 0.5);
595    }
596
597    #[test]
598    fn callback_fires_on_complete() {
599        use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
600        let fired = Arc::new(AtomicBool::new(false));
601        let fired_clone = fired.clone();
602        let mut mgr = TweenManager::new();
603        mgr.start_with_callback(
604            TweenTarget::ScreenFade, 0.0, 1.0, 0.1, Easing::Linear,
605            move |_mgr| { fired_clone.store(true, Ordering::SeqCst); },
606        );
607        mgr.tick(0.2);
608        assert!(fired.load(Ordering::SeqCst));
609    }
610
611    #[test]
612    fn callback_can_chain_tweens() {
613        let mut mgr = TweenManager::new();
614        mgr.start_with_callback(
615            TweenTarget::ScreenFade, 0.0, 1.0, 0.1, Easing::Linear,
616            |mgr| {
617                mgr.start(TweenTarget::ScreenFade, 1.0, 0.0, 0.1, Easing::Linear);
618            },
619        );
620        mgr.tick(0.15); // Complete first, start second
621        assert!(mgr.active_count() >= 1);
622    }
623}