Skip to main content

dotzuki_engine/render/
mod.rs

1//! Map layer definitions, rendering state, colour types, geometry types,
2//! and the pixel framebuffer.
3//!
4//! This module defines:
5//!
6//! * **Layer compositing** — [`MapLayer`], [`MapRenderState`], [`BlendMode`] for
7//!   multi-layer tilemap rendering.
8//! * **Colour** — [`Rgba`] for representing pixel colours.
9//! * **Framebuffer** — [`FrameBuffer`], [`DirtyRegion`], screen-dimension constants.
10//! * **Geometry** — [`TilePos`], [`TileRect`], [`BracketSides`] for tile-grid
11//!   positioning.
12//!
13//! The actual compositing logic lives in `pokered-renderer`; this crate
14//! only provides the **data model**.
15
16pub mod color;
17pub mod framebuffer;
18pub mod geometry;
19pub mod painter;
20
21pub use color::Rgba;
22pub use framebuffer::{DirtyRegion, FrameBuffer, BYTES_PER_PIXEL, TILE_SIZE};
23pub use geometry::{BracketSides, TilePos, TileRect};
24pub use painter::{Frame, LabelValue, Painter, Ui};
25
26use crate::tilemap::Tilemap;
27
28/// How a layer blends with layers below it.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum BlendMode {
31    /// Source over destination (standard alpha compositing).
32    Normal,
33    /// Add source and destination colours (for glow / fire / flash effects).
34    Additive,
35    /// Multiply source and destination colours (for shadows / dark overlays).
36    Multiply,
37}
38
39/// A single layer in the map rendering stack.
40///
41/// Each layer owns a [`Tilemap`] and carries display properties that
42/// control how it is positioned and blended relative to other layers.
43#[derive(Debug, Clone)]
44pub struct MapLayer {
45    /// The tilemap data for this layer.
46    pub tilemap: Tilemap,
47    /// Whether this layer should be drawn.  Invisible layers are skipped
48    /// entirely during compositing.
49    pub visible: bool,
50    /// Global opacity: `0.0` = fully transparent, `1.0` = fully opaque.
51    pub opacity: f32,
52    /// Parallax scroll factor.
53    ///
54    /// `(1.0, 1.0)` means the layer scrolls at the same rate as the camera.
55    /// `(0.5, 0.5)` means the layer scrolls at half the camera speed (distant
56    /// background), and `(2.0, 2.0)` means it scrolls faster (foreground).
57    pub scroll_factor: (f32, f32),
58    /// How this layer blends with layers rendered below it.
59    pub blend_mode: BlendMode,
60    /// Sort order.  Lower values are rendered first (behind).
61    pub z_index: i32,
62    /// When true, the layer's content does not animate frame-to-frame.
63    /// Renderers may cache pre-rendered tiles for performance.
64    pub no_animation: bool,
65    /// Elevation level this layer belongs to (0 = ground, the default).
66    /// Multi-level maps split rendering at the player's elevation: layers
67    /// with `level <= player_elevation` render below sprites, layers above
68    /// render over them.
69    pub level: i32,
70}
71
72impl MapLayer {
73    /// Create a new layer with sensible defaults.
74    ///
75    /// * `tilemap` – the tile data for this layer.
76    /// * `z_index` – render order (lower = behind).
77    pub fn new(tilemap: Tilemap, z_index: i32) -> Self {
78        Self {
79            tilemap,
80            visible: true,
81            opacity: 1.0,
82            scroll_factor: (1.0, 1.0),
83            blend_mode: BlendMode::Normal,
84            z_index,
85            no_animation: false,
86            level: 0,
87        }
88    }
89}
90
91/// The complete rendering state for a map (all layers).
92#[derive(Debug, Clone)]
93pub struct MapRenderState {
94    /// Ordered stack of map layers.  The compositor sorts them by
95    /// [`MapLayer::z_index`] before drawing.
96    pub layers: Vec<MapLayer>,
97    /// RGBA background colour used to fill the framebuffer before any layer
98    /// is drawn.
99    pub background_color: (u8, u8, u8, u8),
100}
101
102impl MapRenderState {
103    /// Create an empty render state with a transparent black background.
104    pub fn new() -> Self {
105        Self {
106            layers: Vec::new(),
107            background_color: (0, 0, 0, 255),
108        }
109    }
110
111    /// Add a layer to the stack.
112    pub fn add_layer(&mut self, layer: MapLayer) {
113        self.layers.push(layer);
114    }
115
116    /// Count how many layers are currently visible.
117    pub fn visible_layer_count(&self) -> usize {
118        self.layers.iter().filter(|l| l.visible).count()
119    }
120}
121
122impl Default for MapRenderState {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::tilemap::Tilemap;
132
133    fn make_tilemap(w: u16, h: u16) -> Tilemap {
134        Tilemap::new(w, h)
135    }
136
137    #[test]
138    fn blend_mode_equality() {
139        assert_eq!(BlendMode::Normal, BlendMode::Normal);
140        assert_ne!(BlendMode::Normal, BlendMode::Additive);
141        assert_ne!(BlendMode::Additive, BlendMode::Multiply);
142    }
143
144    #[test]
145    fn map_layer_defaults() {
146        let tm = make_tilemap(10, 10);
147        let layer = MapLayer::new(tm, 0);
148        assert!(layer.visible);
149        assert!((layer.opacity - 1.0).abs() < f32::EPSILON);
150        assert_eq!(layer.scroll_factor, (1.0, 1.0));
151        assert_eq!(layer.blend_mode, BlendMode::Normal);
152        assert_eq!(layer.z_index, 0);
153        assert_eq!(layer.level, 0);
154    }
155
156    #[test]
157    fn map_layer_custom() {
158        let tm = make_tilemap(8, 8);
159        let layer = MapLayer {
160            tilemap: tm,
161            visible: false,
162            opacity: 0.5,
163            scroll_factor: (0.5, 0.5),
164            blend_mode: BlendMode::Additive,
165            z_index: 5,
166            no_animation: false,
167            level: 1,
168        };
169        assert!(!layer.visible);
170        assert!((layer.opacity - 0.5).abs() < f32::EPSILON);
171        assert_eq!(layer.scroll_factor, (0.5, 0.5));
172        assert_eq!(layer.blend_mode, BlendMode::Additive);
173        assert_eq!(layer.z_index, 5);
174        assert_eq!(layer.level, 1);
175    }
176
177    #[test]
178    fn map_render_state_default() {
179        let state = MapRenderState::default();
180        assert!(state.layers.is_empty());
181        assert_eq!(state.background_color, (0, 0, 0, 255));
182    }
183
184    #[test]
185    fn map_render_state_add_layer() {
186        let mut state = MapRenderState::new();
187        let tm = make_tilemap(32, 32);
188        state.add_layer(MapLayer::new(tm, 0));
189        assert_eq!(state.layers.len(), 1);
190    }
191
192    #[test]
193    fn visible_layer_count() {
194        let mut state = MapRenderState::new();
195        assert_eq!(state.visible_layer_count(), 0);
196
197        let mut l1 = MapLayer::new(make_tilemap(4, 4), 0);
198        l1.visible = true;
199        state.add_layer(l1);
200
201        let mut l2 = MapLayer::new(make_tilemap(4, 4), 1);
202        l2.visible = false;
203        state.add_layer(l2);
204
205        let mut l3 = MapLayer::new(make_tilemap(4, 4), 2);
206        l3.visible = true;
207        state.add_layer(l3);
208
209        assert_eq!(state.visible_layer_count(), 2);
210    }
211
212    #[test]
213    fn map_render_state_background_color() {
214        let state = MapRenderState {
215            layers: vec![],
216            background_color: (0x12, 0x34, 0x56, 0xFF),
217        };
218        assert_eq!(state.background_color, (0x12, 0x34, 0x56, 0xFF));
219    }
220
221    #[test]
222    fn layer_clone_independent() {
223        let tm = make_tilemap(4, 4);
224        let mut l1 = MapLayer::new(tm, 0);
225        l1.opacity = 0.7;
226        let l2 = l1.clone();
227        assert!((l2.opacity - 0.7).abs() < f32::EPSILON);
228        // l2.tilemap should be a clone, not a reference
229        assert_eq!(l2.tilemap.width, 4);
230    }
231}