summaryrefslogtreecommitdiff
path: root/graphics/src/render.rs
blob: a5cdf0957ec87a6e9c425f4b92dae8a4e9263f82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! The `render` module contains the structures for displaying
//! the game, with each frame represented by a `Renderer` and
//! frame specific information in `FrameInfo`.

/// The (prefered) view distance of the game
const VIEW_DISTANCE: i32 = 10;

use std::ops::Div;

use dungeon::{Dungeon, Entity, Floor, MAP_SIZE, Pos, Tile};
use raylib::{
	RaylibThread,
	camera::Camera2D,
	color::Color,
	ffi,
	math::Vector2,
	prelude::{RaylibDraw, RaylibDrawHandle, RaylibHandle, RaylibTextureModeExt},
	texture::RenderTexture2D,
};

use crate::assets::ImageData;

/// The `Renderer` struct is the persistant renderer
/// for the duration for the application.
#[derive(Debug)]
pub struct Renderer {
	/// Set of sprites to be drawn
	image: ImageData,
	/// Pre-rendered map texture that updates if the map changes
	/// Stores the hash of the map tiles to check this
	tiles: Option<(u64, RenderTexture2D)>,
}
impl Renderer {
	pub(crate) fn new(image: ImageData) -> Self {
		Self { image, tiles: None }
	}

	/// Invokes the renderer for the current frame
	pub(crate) fn invoke<'a>(
		&'a mut self,
		handle: &'a mut RaylibHandle,
		thread: &'a RaylibThread,
	) -> FrameRenderer<'a> {
		let draw_handle = handle.begin_drawing(thread);

		// calculate the scaling factor
		let width = draw_handle.get_render_width();
		let height = draw_handle.get_render_height();
		let tile_size = {
			let size = width.max(height);
			let dist = VIEW_DISTANCE * 2 + 1;
			// TODO: force by 16 scaling levels
			size.div(dist).max(16)
		};

		FrameRenderer {
			handle: draw_handle,
			thread,
			renderer: self,
			tile_size,
		}
	}
}

/// A `FrameRenderer` is a renderer for a single
/// frame of the game. It is created per frame.
pub struct FrameRenderer<'a> {
	/// The current draw handle for raylib
	handle: RaylibDrawHandle<'a>,
	/// The raylib thread
	thread: &'a RaylibThread,
	/// Mutable reference to the main renderer (stores persistant data)
	renderer: &'a mut Renderer,
	/// The tile size for this frame
	tile_size: i32,
}
impl<'a> FrameRenderer<'a> {
	/// Returns last computed fps
	fn fps(&self) -> u32 {
		self.handle.get_fps()
	}

	/// Returns image data
	#[expect(dead_code)]
	fn image(&self) -> &ImageData {
		&self.renderer.image
	}

	/// Returns a raylib camera for the given position
	#[must_use]
	fn camera(&self, dungeon: &Dungeon) -> Camera2D {
		let cpos = dungeon.camera();
		let width = self.handle.get_render_width();
		let height = self.handle.get_render_height();
		Camera2D {
			target: Vector2::from(cpos.xy()).scale_by(self.tile_size as f32),
			offset: Vector2::new(width as f32 / 2.0, height as f32 / 2.0),
			rotation: 0.0,
			zoom: 1.0,
		}
	}

	/// Clear the screen
	fn clear(&mut self) {
		self.handle.clear_background(Color::BLACK);
	}

	/// Draws an entire frame
	///
	/// # Examples
	/// ```no_run
	/// use dungeon::Dungeon;
	/// use graphics::Window;
	/// let mut window = Window::new(800, 600, "Dungeon Crawl").unwrap();
	/// let mut renderer = window.renderer();
	/// let dungeon = Dungeon::new();
	/// renderer.draw_frame(&dungeon);
	/// ```
	pub fn draw_frame(&mut self, dungeon: &Dungeon) -> crate::Result<()> {
		self.clear();
		self.draw_dungeon(dungeon)?;
		self.draw_ui(dungeon);
		Ok(())
	}

	/// Draws the dungeon, (tiles and entities)
	pub fn draw_dungeon(&mut self, dungeon: &Dungeon) -> crate::Result<()> {
		let camera = self.camera(dungeon);
		unsafe {
			ffi::BeginMode2D(camera.into());
		}
		self.draw_tiles(dungeon)?;
		self.draw_entities(dungeon);
		unsafe {
			ffi::EndMode2D();
		}
		Ok(())
	}

	/// Draws player HP, inventory, and floor number
	pub fn draw_ui(&mut self, _dungeon: &Dungeon) {
		#[cfg(feature = "debug")]
		// Draw fps (debug only)
		self.draw_fps();
	}

	/// Draws the entities on the map
	pub fn draw_entities(&mut self, dungeon: &Dungeon) {
		self.draw_entity(&dungeon.player.entity);
	}

	/// Draws an entity
	#[expect(clippy::cast_possible_truncation)]
	pub fn draw_entity(&mut self, entity: &Entity) {
		let size = self.tile_size;
		let x = (entity.fpos.x() * size as f32) as i32;
		let y = (entity.fpos.y() * size as f32) as i32;
		// TODO: per entity color
		self.handle.draw_rectangle(x, y, size, size, Color::GREEN);
	}

	/// Draw dungeon tiles
	pub fn draw_tiles(&mut self, dungeon: &Dungeon) -> crate::Result<()> {
		let hash = dungeon.floor.hash();
		let tex = match self.renderer.tiles.take() {
			Some((h, tex)) if hash == h => tex,
			_ => self.draw_tiles_to_tex(&dungeon.floor)?,
		};

		// caculate the starting postion on the texture to draw from
		self.handle.draw_texture(&tex, 0, 0, Color::WHITE);

		// save the texture
		self.renderer.tiles.replace((hash, tex));

		Ok(())
	}

	/// Draw tiles on a provided texture
	fn draw_tiles_to_tex(&mut self, floor: &Floor) -> crate::Result<RenderTexture2D> {
		let size = self.tile_size;
		let pixels = (MAP_SIZE as i32) * size;
		let mut tex =
			self.handle
				.load_render_texture(self.thread, pixels as u32, pixels as u32)?;

		// draw the tiles to the texture
		{
			let mut handle = self.handle.begin_texture_mode(self.thread, &mut tex);
			handle.clear_background(Color::BLACK);
			for pos in Pos::values() {
				let (x, y) = pos.xy();
				let color = tile_color(floor.get(pos));
				handle.draw_rectangle(x as i32 * size, y as i32 * size, size, size, color);
			}
		}

		Ok(tex)
	}

	/// Draw FPS counter
	pub fn draw_fps(&mut self) {
		let fps_str = format!("{}", self.fps());
		self.handle.draw_text(&fps_str, 10, 10, 30, Color::YELLOW);
	}
}

fn tile_color(tile: Tile) -> Color {
	// TODO: use textures instead of colors :)
	match tile {
		Tile::Wall => Color::BLUE,
		Tile::Air => Color::RED,
		Tile::Stairs => Color::GRAY,
	}
}