jugar_core/
components.rs

1//! Common game components for universal design
2//!
3//! These components support both mobile (6") and ultrawide (49") displays
4//! through responsive anchoring and scaling systems.
5
6use core::fmt;
7
8use glam::Vec2;
9use serde::{Deserialize, Serialize};
10
11/// 2D position component
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Position {
14    /// X coordinate
15    pub x: f32,
16    /// Y coordinate
17    pub y: f32,
18}
19
20impl Position {
21    /// Creates a new position
22    #[must_use]
23    pub const fn new(x: f32, y: f32) -> Self {
24        Self { x, y }
25    }
26
27    /// Creates a position at the origin
28    #[must_use]
29    pub const fn zero() -> Self {
30        Self { x: 0.0, y: 0.0 }
31    }
32
33    /// Converts to a glam Vec2
34    #[must_use]
35    pub const fn as_vec2(self) -> Vec2 {
36        Vec2::new(self.x, self.y)
37    }
38
39    /// Creates from a glam Vec2
40    #[must_use]
41    pub const fn from_vec2(v: Vec2) -> Self {
42        Self { x: v.x, y: v.y }
43    }
44
45    /// Distance to another position
46    #[must_use]
47    pub fn distance_to(self, other: Self) -> f32 {
48        let dx = other.x - self.x;
49        let dy = other.y - self.y;
50        dx.hypot(dy)
51    }
52}
53
54impl Default for Position {
55    fn default() -> Self {
56        Self::zero()
57    }
58}
59
60impl fmt::Display for Position {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "({:.2}, {:.2})", self.x, self.y)
63    }
64}
65
66/// 2D velocity component
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
68pub struct Velocity {
69    /// X velocity
70    pub x: f32,
71    /// Y velocity
72    pub y: f32,
73}
74
75impl Velocity {
76    /// Creates a new velocity
77    #[must_use]
78    pub const fn new(x: f32, y: f32) -> Self {
79        Self { x, y }
80    }
81
82    /// Creates a zero velocity
83    #[must_use]
84    pub const fn zero() -> Self {
85        Self { x: 0.0, y: 0.0 }
86    }
87
88    /// Returns the speed (magnitude)
89    #[must_use]
90    pub fn speed(self) -> f32 {
91        self.x.hypot(self.y)
92    }
93
94    /// Normalizes the velocity to unit length, or returns zero if magnitude is zero
95    #[must_use]
96    pub fn normalized(self) -> Self {
97        let mag = self.speed();
98        if mag < f32::EPSILON {
99            Self::zero()
100        } else {
101            Self {
102                x: self.x / mag,
103                y: self.y / mag,
104            }
105        }
106    }
107
108    /// Scales the velocity by a factor
109    #[must_use]
110    pub const fn scaled(self, factor: f32) -> Self {
111        Self {
112            x: self.x * factor,
113            y: self.y * factor,
114        }
115    }
116}
117
118impl Default for Velocity {
119    fn default() -> Self {
120        Self::zero()
121    }
122}
123
124impl fmt::Display for Velocity {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "vel({:.2}, {:.2})", self.x, self.y)
127    }
128}
129
130/// UI Anchor for responsive layout
131///
132/// Anchors determine how UI elements position themselves relative to
133/// their parent container. This enables the same UI to work on both
134/// mobile (6") and ultrawide (49") displays.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
136pub enum Anchor {
137    /// Center of the container
138    #[default]
139    Center,
140    /// Top-left corner
141    TopLeft,
142    /// Top-center
143    TopCenter,
144    /// Top-right corner
145    TopRight,
146    /// Middle-left
147    MiddleLeft,
148    /// Middle-right
149    MiddleRight,
150    /// Bottom-left corner
151    BottomLeft,
152    /// Bottom-center
153    BottomCenter,
154    /// Bottom-right corner
155    BottomRight,
156    /// Stretch to fill available space
157    Stretch,
158}
159
160impl Anchor {
161    /// Returns the normalized anchor point (0.0 to 1.0)
162    #[must_use]
163    pub const fn normalized(self) -> (f32, f32) {
164        match self {
165            Self::TopLeft => (0.0, 0.0),
166            Self::TopCenter => (0.5, 0.0),
167            Self::TopRight => (1.0, 0.0),
168            Self::MiddleLeft => (0.0, 0.5),
169            Self::Center | Self::Stretch => (0.5, 0.5),
170            Self::MiddleRight => (1.0, 0.5),
171            Self::BottomLeft => (0.0, 1.0),
172            Self::BottomCenter => (0.5, 1.0),
173            Self::BottomRight => (1.0, 1.0),
174        }
175    }
176}
177
178impl fmt::Display for Anchor {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::Center => write!(f, "center"),
182            Self::TopLeft => write!(f, "top-left"),
183            Self::TopCenter => write!(f, "top-center"),
184            Self::TopRight => write!(f, "top-right"),
185            Self::MiddleLeft => write!(f, "middle-left"),
186            Self::MiddleRight => write!(f, "middle-right"),
187            Self::BottomLeft => write!(f, "bottom-left"),
188            Self::BottomCenter => write!(f, "bottom-center"),
189            Self::BottomRight => write!(f, "bottom-right"),
190            Self::Stretch => write!(f, "stretch"),
191        }
192    }
193}
194
195/// Scale mode for UI elements
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
197pub enum ScaleMode {
198    /// Scale based on shortest dimension (height on landscape)
199    #[default]
200    Adaptive,
201    /// Maintain pixel-perfect rendering (for pixel art)
202    PixelPerfect,
203    /// Fixed size in pixels, no scaling
204    Fixed,
205}
206
207/// UI Element component for responsive layout
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct UiElement {
210    /// Anchor point for positioning
211    pub anchor: Anchor,
212    /// Offset from anchor point
213    pub offset: Vec2,
214    /// Size of the element
215    pub size: Vec2,
216    /// How the element scales
217    pub scale_mode: ScaleMode,
218    /// Whether the element is visible
219    pub visible: bool,
220    /// Z-order for layering (higher = on top)
221    pub z_order: i32,
222}
223
224impl UiElement {
225    /// Creates a new UI element with default settings
226    #[must_use]
227    pub const fn new(size: Vec2) -> Self {
228        Self {
229            anchor: Anchor::Center,
230            offset: Vec2::ZERO,
231            size,
232            scale_mode: ScaleMode::Adaptive,
233            visible: true,
234            z_order: 0,
235        }
236    }
237
238    /// Sets the anchor
239    #[must_use]
240    pub const fn with_anchor(mut self, anchor: Anchor) -> Self {
241        self.anchor = anchor;
242        self
243    }
244
245    /// Sets the offset
246    #[must_use]
247    pub const fn with_offset(mut self, offset: Vec2) -> Self {
248        self.offset = offset;
249        self
250    }
251
252    /// Sets the scale mode
253    #[must_use]
254    pub const fn with_scale_mode(mut self, mode: ScaleMode) -> Self {
255        self.scale_mode = mode;
256        self
257    }
258
259    /// Sets the z-order
260    #[must_use]
261    pub const fn with_z_order(mut self, z: i32) -> Self {
262        self.z_order = z;
263        self
264    }
265
266    /// Calculates the actual position given a container size
267    #[must_use]
268    pub fn calculate_position(&self, container_size: Vec2) -> Vec2 {
269        let (ax, ay) = self.anchor.normalized();
270        Vec2::new(
271            container_size.x.mul_add(ax, self.offset.x),
272            container_size.y.mul_add(ay, self.offset.y),
273        )
274    }
275}
276
277impl Default for UiElement {
278    fn default() -> Self {
279        Self::new(Vec2::new(100.0, 100.0))
280    }
281}
282
283/// Camera component with aspect ratio handling
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct Camera {
286    /// Zoom level (1.0 = normal)
287    pub zoom: f32,
288    /// Target resolution for pixel-perfect rendering
289    pub target_resolution: Option<Vec2>,
290    /// Whether to maintain aspect ratio
291    pub keep_aspect: bool,
292    /// Field of view for perspective (degrees)
293    pub fov: f32,
294    /// Camera position in world space
295    pub position: Position,
296}
297
298impl Camera {
299    /// Creates a new camera with default settings
300    #[must_use]
301    pub const fn new() -> Self {
302        Self {
303            zoom: 1.0,
304            target_resolution: None,
305            keep_aspect: true,
306            fov: 60.0,
307            position: Position::zero(),
308        }
309    }
310
311    /// Creates a camera for pixel art with fixed resolution
312    #[must_use]
313    pub const fn pixel_art(width: f32, height: f32) -> Self {
314        Self {
315            zoom: 1.0,
316            target_resolution: Some(Vec2::new(width, height)),
317            keep_aspect: true,
318            fov: 60.0,
319            position: Position::zero(),
320        }
321    }
322
323    /// Sets the zoom level
324    #[must_use]
325    pub const fn with_zoom(mut self, zoom: f32) -> Self {
326        self.zoom = zoom;
327        self
328    }
329
330    /// Sets the position
331    #[must_use]
332    pub const fn with_position(mut self, pos: Position) -> Self {
333        self.position = pos;
334        self
335    }
336}
337
338impl Default for Camera {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344/// Sprite component for rendering
345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
346pub struct Sprite {
347    /// Texture identifier
348    pub texture_id: u32,
349    /// Source rectangle in texture (for sprite sheets)
350    pub source: Option<Rect>,
351    /// Tint color (RGBA)
352    pub color: [f32; 4],
353    /// Flip horizontally
354    pub flip_x: bool,
355    /// Flip vertically
356    pub flip_y: bool,
357}
358
359impl Sprite {
360    /// Creates a new sprite with the given texture
361    #[must_use]
362    pub const fn new(texture_id: u32) -> Self {
363        Self {
364            texture_id,
365            source: None,
366            color: [1.0, 1.0, 1.0, 1.0],
367            flip_x: false,
368            flip_y: false,
369        }
370    }
371
372    /// Sets the source rectangle for sprite sheets
373    #[must_use]
374    pub const fn with_source(mut self, rect: Rect) -> Self {
375        self.source = Some(rect);
376        self
377    }
378
379    /// Sets the tint color
380    #[must_use]
381    pub const fn with_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
382        self.color = [r, g, b, a];
383        self
384    }
385}
386
387impl Default for Sprite {
388    fn default() -> Self {
389        Self::new(0)
390    }
391}
392
393/// Rectangle for collision and rendering
394#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
395pub struct Rect {
396    /// X position
397    pub x: f32,
398    /// Y position
399    pub y: f32,
400    /// Width
401    pub width: f32,
402    /// Height
403    pub height: f32,
404}
405
406impl Rect {
407    /// Creates a new rectangle
408    #[must_use]
409    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
410        Self {
411            x,
412            y,
413            width,
414            height,
415        }
416    }
417
418    /// Creates a rectangle at origin with given size
419    #[must_use]
420    pub const fn from_size(width: f32, height: f32) -> Self {
421        Self::new(0.0, 0.0, width, height)
422    }
423
424    /// Checks if a point is inside the rectangle
425    #[must_use]
426    pub fn contains_point(&self, x: f32, y: f32) -> bool {
427        x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
428    }
429
430    /// Checks if two rectangles overlap
431    #[must_use]
432    pub fn overlaps(&self, other: &Self) -> bool {
433        self.x < other.x + other.width
434            && self.x + self.width > other.x
435            && self.y < other.y + other.height
436            && self.y + self.height > other.y
437    }
438
439    /// Returns the center point
440    #[must_use]
441    pub const fn center(&self) -> (f32, f32) {
442        (self.x + self.width / 2.0, self.y + self.height / 2.0)
443    }
444}
445
446impl Default for Rect {
447    fn default() -> Self {
448        Self::new(0.0, 0.0, 1.0, 1.0)
449    }
450}
451
452#[cfg(test)]
453#[allow(clippy::unwrap_used, clippy::expect_used)]
454mod tests {
455    use super::*;
456
457    // ==================== POSITION TESTS ====================
458
459    #[test]
460    fn test_position_new() {
461        let pos = Position::new(10.0, 20.0);
462        assert!((pos.x - 10.0).abs() < f32::EPSILON);
463        assert!((pos.y - 20.0).abs() < f32::EPSILON);
464    }
465
466    #[test]
467    fn test_position_zero() {
468        let pos = Position::zero();
469        assert!((pos.x).abs() < f32::EPSILON);
470        assert!((pos.y).abs() < f32::EPSILON);
471    }
472
473    #[test]
474    fn test_position_default() {
475        let pos = Position::default();
476        assert!((pos.x).abs() < f32::EPSILON);
477        assert!((pos.y).abs() < f32::EPSILON);
478    }
479
480    #[test]
481    fn test_position_as_vec2() {
482        let pos = Position::new(10.0, 20.0);
483        let v = pos.as_vec2();
484        assert!((v.x - 10.0).abs() < f32::EPSILON);
485        assert!((v.y - 20.0).abs() < f32::EPSILON);
486    }
487
488    #[test]
489    fn test_position_from_vec2() {
490        let v = Vec2::new(10.0, 20.0);
491        let pos = Position::from_vec2(v);
492        assert!((pos.x - 10.0).abs() < f32::EPSILON);
493        assert!((pos.y - 20.0).abs() < f32::EPSILON);
494    }
495
496    #[test]
497    fn test_position_distance() {
498        let p1 = Position::new(0.0, 0.0);
499        let p2 = Position::new(3.0, 4.0);
500        assert!((p1.distance_to(p2) - 5.0).abs() < f32::EPSILON);
501    }
502
503    #[test]
504    fn test_position_display() {
505        let pos = Position::new(1.5, 2.5);
506        assert_eq!(format!("{pos}"), "(1.50, 2.50)");
507    }
508
509    // ==================== VELOCITY TESTS ====================
510
511    #[test]
512    fn test_velocity_new() {
513        let vel = Velocity::new(1.0, 2.0);
514        assert!((vel.x - 1.0).abs() < f32::EPSILON);
515        assert!((vel.y - 2.0).abs() < f32::EPSILON);
516    }
517
518    #[test]
519    fn test_velocity_zero() {
520        let vel = Velocity::zero();
521        assert!((vel.x).abs() < f32::EPSILON);
522        assert!((vel.y).abs() < f32::EPSILON);
523    }
524
525    #[test]
526    fn test_velocity_default() {
527        let vel = Velocity::default();
528        assert!((vel.x).abs() < f32::EPSILON);
529        assert!((vel.y).abs() < f32::EPSILON);
530    }
531
532    #[test]
533    fn test_velocity_speed() {
534        let vel = Velocity::new(3.0, 4.0);
535        assert!((vel.speed() - 5.0).abs() < f32::EPSILON);
536    }
537
538    #[test]
539    fn test_velocity_normalized() {
540        let vel = Velocity::new(3.0, 4.0);
541        let norm = vel.normalized();
542        assert!((norm.speed() - 1.0).abs() < 0.001);
543    }
544
545    #[test]
546    fn test_velocity_normalized_zero() {
547        let vel = Velocity::zero();
548        let norm = vel.normalized();
549        assert!((norm.x).abs() < f32::EPSILON);
550        assert!((norm.y).abs() < f32::EPSILON);
551    }
552
553    #[test]
554    fn test_velocity_scaled() {
555        let vel = Velocity::new(1.0, 2.0);
556        let scaled = vel.scaled(2.0);
557        assert!((scaled.x - 2.0).abs() < f32::EPSILON);
558        assert!((scaled.y - 4.0).abs() < f32::EPSILON);
559    }
560
561    #[test]
562    fn test_velocity_display() {
563        let vel = Velocity::new(1.5, 2.5);
564        assert_eq!(format!("{vel}"), "vel(1.50, 2.50)");
565    }
566
567    // ==================== ANCHOR TESTS ====================
568
569    #[test]
570    fn test_anchor_normalized() {
571        assert_eq!(Anchor::TopLeft.normalized(), (0.0, 0.0));
572        assert_eq!(Anchor::TopCenter.normalized(), (0.5, 0.0));
573        assert_eq!(Anchor::TopRight.normalized(), (1.0, 0.0));
574        assert_eq!(Anchor::MiddleLeft.normalized(), (0.0, 0.5));
575        assert_eq!(Anchor::Center.normalized(), (0.5, 0.5));
576        assert_eq!(Anchor::MiddleRight.normalized(), (1.0, 0.5));
577        assert_eq!(Anchor::BottomLeft.normalized(), (0.0, 1.0));
578        assert_eq!(Anchor::BottomCenter.normalized(), (0.5, 1.0));
579        assert_eq!(Anchor::BottomRight.normalized(), (1.0, 1.0));
580        assert_eq!(Anchor::Stretch.normalized(), (0.5, 0.5));
581    }
582
583    #[test]
584    fn test_anchor_default() {
585        let anchor = Anchor::default();
586        assert_eq!(anchor, Anchor::Center);
587    }
588
589    #[test]
590    fn test_anchor_display() {
591        assert_eq!(format!("{}", Anchor::TopLeft), "top-left");
592        assert_eq!(format!("{}", Anchor::TopCenter), "top-center");
593        assert_eq!(format!("{}", Anchor::TopRight), "top-right");
594        assert_eq!(format!("{}", Anchor::MiddleLeft), "middle-left");
595        assert_eq!(format!("{}", Anchor::Center), "center");
596        assert_eq!(format!("{}", Anchor::MiddleRight), "middle-right");
597        assert_eq!(format!("{}", Anchor::BottomLeft), "bottom-left");
598        assert_eq!(format!("{}", Anchor::BottomCenter), "bottom-center");
599        assert_eq!(format!("{}", Anchor::BottomRight), "bottom-right");
600        assert_eq!(format!("{}", Anchor::Stretch), "stretch");
601    }
602
603    // ==================== SCALE MODE TESTS ====================
604
605    #[test]
606    fn test_scale_mode_default() {
607        let mode = ScaleMode::default();
608        assert_eq!(mode, ScaleMode::Adaptive);
609    }
610
611    // ==================== UI ELEMENT TESTS ====================
612
613    #[test]
614    fn test_ui_element_new() {
615        let elem = UiElement::new(Vec2::new(100.0, 50.0));
616        assert_eq!(elem.anchor, Anchor::Center);
617        assert_eq!(elem.offset, Vec2::ZERO);
618        assert!(elem.visible);
619        assert_eq!(elem.z_order, 0);
620        assert_eq!(elem.scale_mode, ScaleMode::Adaptive);
621    }
622
623    #[test]
624    fn test_ui_element_default() {
625        let elem = UiElement::default();
626        assert!((elem.size.x - 100.0).abs() < f32::EPSILON);
627        assert!((elem.size.y - 100.0).abs() < f32::EPSILON);
628    }
629
630    #[test]
631    fn test_ui_element_builders() {
632        let elem = UiElement::new(Vec2::new(100.0, 50.0))
633            .with_anchor(Anchor::TopLeft)
634            .with_offset(Vec2::new(10.0, 20.0))
635            .with_scale_mode(ScaleMode::PixelPerfect)
636            .with_z_order(5);
637
638        assert_eq!(elem.anchor, Anchor::TopLeft);
639        assert_eq!(elem.offset, Vec2::new(10.0, 20.0));
640        assert_eq!(elem.scale_mode, ScaleMode::PixelPerfect);
641        assert_eq!(elem.z_order, 5);
642    }
643
644    #[test]
645    fn test_ui_element_position_calculation() {
646        let elem = UiElement::new(Vec2::new(100.0, 50.0))
647            .with_anchor(Anchor::TopLeft)
648            .with_offset(Vec2::new(10.0, 20.0));
649
650        let pos = elem.calculate_position(Vec2::new(800.0, 600.0));
651        assert!((pos.x - 10.0).abs() < f32::EPSILON);
652        assert!((pos.y - 20.0).abs() < f32::EPSILON);
653    }
654
655    #[test]
656    fn test_ui_element_center_position() {
657        let elem = UiElement::new(Vec2::new(100.0, 50.0)).with_anchor(Anchor::Center);
658
659        let pos = elem.calculate_position(Vec2::new(800.0, 600.0));
660        assert!((pos.x - 400.0).abs() < f32::EPSILON);
661        assert!((pos.y - 300.0).abs() < f32::EPSILON);
662    }
663
664    #[test]
665    fn test_ui_element_bottom_right_position() {
666        let elem = UiElement::new(Vec2::new(100.0, 50.0)).with_anchor(Anchor::BottomRight);
667
668        let pos = elem.calculate_position(Vec2::new(800.0, 600.0));
669        assert!((pos.x - 800.0).abs() < f32::EPSILON);
670        assert!((pos.y - 600.0).abs() < f32::EPSILON);
671    }
672
673    // ==================== RECT TESTS ====================
674
675    #[test]
676    fn test_rect_new() {
677        let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
678        assert!((rect.x - 10.0).abs() < f32::EPSILON);
679        assert!((rect.y - 20.0).abs() < f32::EPSILON);
680        assert!((rect.width - 100.0).abs() < f32::EPSILON);
681        assert!((rect.height - 50.0).abs() < f32::EPSILON);
682    }
683
684    #[test]
685    fn test_rect_from_size() {
686        let rect = Rect::from_size(100.0, 50.0);
687        assert!((rect.x).abs() < f32::EPSILON);
688        assert!((rect.y).abs() < f32::EPSILON);
689        assert!((rect.width - 100.0).abs() < f32::EPSILON);
690        assert!((rect.height - 50.0).abs() < f32::EPSILON);
691    }
692
693    #[test]
694    fn test_rect_default() {
695        let rect = Rect::default();
696        assert!((rect.x).abs() < f32::EPSILON);
697        assert!((rect.y).abs() < f32::EPSILON);
698        assert!((rect.width - 1.0).abs() < f32::EPSILON);
699        assert!((rect.height - 1.0).abs() < f32::EPSILON);
700    }
701
702    #[test]
703    fn test_rect_contains_point() {
704        let rect = Rect::new(10.0, 10.0, 100.0, 50.0);
705        assert!(rect.contains_point(50.0, 30.0));
706        assert!(!rect.contains_point(0.0, 0.0));
707        assert!(rect.contains_point(10.0, 10.0)); // Edge
708        assert!(rect.contains_point(110.0, 60.0)); // Other edge
709    }
710
711    #[test]
712    fn test_rect_overlaps() {
713        let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
714        let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
715        let r3 = Rect::new(200.0, 200.0, 50.0, 50.0);
716
717        assert!(r1.overlaps(&r2));
718        assert!(!r1.overlaps(&r3));
719    }
720
721    #[test]
722    fn test_rect_center() {
723        let rect = Rect::new(0.0, 0.0, 100.0, 50.0);
724        let (cx, cy) = rect.center();
725        assert!((cx - 50.0).abs() < f32::EPSILON);
726        assert!((cy - 25.0).abs() < f32::EPSILON);
727    }
728
729    // ==================== SPRITE TESTS ====================
730
731    #[test]
732    fn test_sprite_new() {
733        let sprite = Sprite::new(42);
734        assert_eq!(sprite.texture_id, 42);
735        assert!(sprite.source.is_none());
736        assert!(!sprite.flip_x);
737        assert!(!sprite.flip_y);
738    }
739
740    #[test]
741    fn test_sprite_default() {
742        let sprite = Sprite::default();
743        assert_eq!(sprite.texture_id, 0);
744    }
745
746    #[test]
747    fn test_sprite_with_source() {
748        let sprite = Sprite::new(1).with_source(Rect::new(0.0, 0.0, 32.0, 32.0));
749        assert!(sprite.source.is_some());
750        let src = sprite.source.unwrap();
751        assert!((src.width - 32.0).abs() < f32::EPSILON);
752    }
753
754    #[test]
755    fn test_sprite_with_color() {
756        let sprite = Sprite::new(1).with_color(1.0, 0.5, 0.0, 0.8);
757        assert!((sprite.color[0] - 1.0).abs() < f32::EPSILON);
758        assert!((sprite.color[1] - 0.5).abs() < f32::EPSILON);
759        assert!((sprite.color[2] - 0.0).abs() < f32::EPSILON);
760        assert!((sprite.color[3] - 0.8).abs() < f32::EPSILON);
761    }
762
763    // ==================== CAMERA TESTS ====================
764
765    #[test]
766    fn test_camera_new() {
767        let cam = Camera::new();
768        assert!((cam.zoom - 1.0).abs() < f32::EPSILON);
769        assert!(cam.keep_aspect);
770        assert!(cam.target_resolution.is_none());
771        assert!((cam.fov - 60.0).abs() < f32::EPSILON);
772    }
773
774    #[test]
775    fn test_camera_default() {
776        let cam = Camera::default();
777        assert!((cam.zoom - 1.0).abs() < f32::EPSILON);
778        assert!(cam.keep_aspect);
779    }
780
781    #[test]
782    fn test_camera_pixel_art() {
783        let cam = Camera::pixel_art(320.0, 240.0);
784        assert!(cam.target_resolution.is_some());
785        let res = cam.target_resolution.unwrap();
786        assert!((res.x - 320.0).abs() < f32::EPSILON);
787        assert!((res.y - 240.0).abs() < f32::EPSILON);
788    }
789
790    #[test]
791    fn test_camera_with_zoom() {
792        let cam = Camera::new().with_zoom(2.0);
793        assert!((cam.zoom - 2.0).abs() < f32::EPSILON);
794    }
795
796    #[test]
797    fn test_camera_with_position() {
798        let cam = Camera::new().with_position(Position::new(100.0, 200.0));
799        assert!((cam.position.x - 100.0).abs() < f32::EPSILON);
800        assert!((cam.position.y - 200.0).abs() < f32::EPSILON);
801    }
802}