summaryrefslogtreecommitdiff
path: root/graphics/src/render.rs
blob: 8c88210b5182e69de93123331d436a9bac82223f (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
//! 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, Sub};

use dungeon::{Dungeon, Entity, Floor, MAP_SIZE, Player, Pos, Tile};
use raylib::{
	RaylibThread,
	color::Color,
	math::{Rectangle, 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);
		FrameRenderer {
			handle: draw_handle,
			thread,
			renderer: self,
		}
	}
}

/// 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,
}
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 the current render width
	fn render_width(&self) -> i32 {
		self.handle.get_render_width()
	}

	/// Returns the current render height
	fn render_height(&self) -> i32 {
		self.handle.get_render_height()
	}

	/// Returns the wanted map tile size
	fn tile_size(&self) -> i32 {
		// the pixl size we will base everything on
		let size = self.render_width().max(self.render_height());
		// min visible tiles
		let dist = VIEW_DISTANCE * 2 + 1;
		// get crude number of pixles
		// TODO: force scaling levels
		size.div(dist).max(16)
	}

	/// Clear the screen
	pub 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<()> {
		// Clear the background to black
		self.clear();

		// Draw the dungeon
		self.draw_tiles(dungeon)?;
		self.draw_player(&dungeon.player);

		// Draw the ui
		self.draw_ui(dungeon);

		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();
	}

	/// Draw the player sprite
	pub fn draw_player(&mut self, player: &Player) {
		self.draw_entity(&player.entity);
	}

	/// Draws an entity
	#[expect(clippy::unused_self)]
	pub fn draw_entity(&mut self, _entity: &Entity) {
		// TODO:
	}

	/// 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
		let size = self.tile_size() as f32;
		let dist = VIEW_DISTANCE as f32;
		let camera = Vector2::from(dungeon.camera().xy());
		let source_rec = Rectangle::new(
			camera.x.sub(dist).max(0.0) * size,
			camera.y.sub(dist).max(0.0) * size,
			size * (dist * 2.0 + 1.0),
			size * (dist * 2.0 + 1.0),
		);

		self.handle
			.draw_texture_rec(&tex, source_rec, Vector2::zero(), 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,
	}
}