1use jengine::engine::{Color, Game, jEngine, KeyCode, AnimationType};
12use jengine::pathfinding::prelude::{astar, astar_8dir, DijkstraMap};
13use jengine::renderer::text::Font;
14use jengine::ecs::{World, Entity};
15use jengine::{DEFAULT_FONT_METADATA, DEFAULT_TILE_H, DEFAULT_TILE_W, DEFAULT_TILESET};
16
17const MAP_W: i32 = 40;
20const MAP_H: i32 = 22;
21
22const C_WALL: Color = Color([0.15, 0.15, 0.18, 1.0]);
23const C_FLOOR: Color = Color([0.05, 0.05, 0.06, 1.0]);
24const C_START: Color = Color([0.20, 0.90, 0.40, 1.0]);
25const C_GOAL: Color = Color([1.00, 0.30, 0.20, 1.0]);
26const C_PATH: Color = Color([0.30, 0.60, 1.00, 1.0]);
27const C_ACCENT: Color = Color([1.00, 0.80, 0.20, 1.0]);
28
29#[derive(Copy, Clone)]
32struct Position { x: i32, y: i32 }
33struct Marker { color: Color, glyph: char }
34
35struct PathfindingShowcase {
38 world: World,
39 walls: Vec<bool>,
40 start_ent: Entity,
41 goal_ent: Entity,
42 path: Option<Vec<(i32, i32)>>,
43 dijkstra: DijkstraMap,
44 mode_8dir: bool,
45 show_dijkstra: bool,
46 dirty: bool,
47}
48
49impl PathfindingShowcase {
50 fn new() -> Self {
51 let mut world = World::new();
52 let walls = Self::generate_maze();
53
54 let start_ent = world.spawn();
56 world.insert(start_ent, Position { x: 5, y: 5 });
57 world.insert(start_ent, Marker { color: C_START, glyph: 'S' });
58
59 let goal_ent = world.spawn();
60 world.insert(goal_ent, Position { x: 35, y: 15 });
61 world.insert(goal_ent, Marker { color: C_GOAL, glyph: 'G' });
62
63 let mut s = Self {
64 world,
65 walls,
66 start_ent,
67 goal_ent,
68 path: None,
69 dijkstra: DijkstraMap::new(MAP_W, MAP_H, &[(0,0)], |_, _| true),
70 mode_8dir: false,
71 show_dijkstra: false,
72 dirty: true,
73 };
74 s.recompute();
75 s
76 }
77
78 fn generate_maze() -> Vec<bool> {
79 let mut walls = vec![false; (MAP_W * MAP_H) as usize];
80 let mut block = |x, y| {
81 if x >= 0 && x < MAP_W && y >= 0 && y < MAP_H {
82 walls[(y * MAP_W + x) as usize] = true;
83 }
84 };
85
86 for x in 0..MAP_W { block(x, 0); block(x, MAP_H - 1); }
88 for y in 0..MAP_H { block(0, y); block(MAP_W - 1, y); }
89
90 for x in 10..30 { block(x, 7); block(x, 14); }
92 for y in 3..10 { block(10, y); }
93 for y in 14..19 { block(30, y); }
94
95 walls
96 }
97
98 fn is_walkable(&self, x: i32, y: i32) -> bool {
99 if x < 0 || x >= MAP_W || y < 0 || y >= MAP_H { return false; }
100 !self.walls[(y * MAP_W + x) as usize]
101 }
102
103 fn recompute(&mut self) {
104 let s = self.world.get::<Position>(self.start_ent).unwrap();
105 let g = self.world.get::<Position>(self.goal_ent).unwrap();
106 let (sx, sy) = (s.x, s.y);
107 let (gx, gy) = (g.x, g.y);
108
109 if self.mode_8dir {
110 self.path = astar_8dir((sx, sy), (gx, gy), MAP_W, MAP_H, |x, y| self.is_walkable(x, y), 2000);
111 } else {
112 self.path = astar((sx, sy), (gx, gy), MAP_W, MAP_H, |x, y| self.is_walkable(x, y), 2000);
113 }
114
115 self.dijkstra = DijkstraMap::new(MAP_W, MAP_H, &[(gx, gy)], |x, y| self.is_walkable(x, y));
116 self.dirty = false;
117 }
118}
119
120impl Game for PathfindingShowcase {
121 fn on_enter(&mut self, engine: &mut jEngine) {
122 if let Ok(font) = Font::from_mtsdf_json(DEFAULT_FONT_METADATA) {
123 engine.renderer.set_mtsdf_distance_range(font.distance_range);
124 engine.ui.text.set_font(font);
125 }
126
127 engine.audio.load_sound("move", "resources/audio/UI_selection.wav");
129 engine.audio.load_sound("toggle", "resources/audio/UI_click.wav");
130 }
131
132 fn update(&mut self, engine: &mut jEngine) {
133 let mut moved = false;
134 let mut pos = *self.world.get::<Position>(self.start_ent).unwrap();
135
136 if engine.is_key_pressed(KeyCode::ArrowLeft) { pos.x -= 1; moved = true; }
137 if engine.is_key_pressed(KeyCode::ArrowRight) { pos.x += 1; moved = true; }
138 if engine.is_key_pressed(KeyCode::ArrowUp) { pos.y -= 1; moved = true; }
139 if engine.is_key_pressed(KeyCode::ArrowDown) { pos.y += 1; moved = true; }
140
141 if moved && self.is_walkable(pos.x, pos.y) {
142 self.world.insert(self.start_ent, pos);
143 self.dirty = true;
144 engine.play_sound("move");
145 engine.play_animation(self.start_ent, AnimationType::Bash {
147 direction: [0.0, -0.5],
148 magnitude: 4.0
149 });
150 }
151
152 if engine.is_key_pressed(KeyCode::Tab) {
153 self.mode_8dir = !self.mode_8dir;
154 self.dirty = true;
155 engine.play_sound("toggle");
156 engine.play_animation(self.goal_ent, AnimationType::Shiver { magnitude: 3.0 });
158 }
159
160 if engine.is_key_pressed(KeyCode::KeyD) {
161 self.show_dijkstra = !self.show_dijkstra;
162 engine.play_sound("toggle");
163 }
164
165 if self.dirty {
166 self.recompute();
167 }
168 }
169
170 fn render(&mut self, engine: &mut jEngine) {
171 engine.clear();
172 let tw = engine.tile_width();
173 let th = engine.tile_height();
174
175 let path_set: std::collections::HashSet<(i32, i32)> = self.path.as_ref()
177 .map(|p| p.iter().copied().collect())
178 .unwrap_or_default();
179
180 for y in 0..MAP_H {
181 for x in 0..MAP_W {
182 let ux = x as u32;
183 let uy = y as u32;
184
185 if self.walls[(y * MAP_W + x) as usize] {
186 engine.set_background(ux, uy, C_WALL);
187 engine.set_foreground(ux, uy, '#', Color([0.3, 0.3, 0.35, 1.0]));
188 } else if self.show_dijkstra {
189 let d = self.dijkstra.get(x, y);
190 if d < f32::MAX {
191 let t = (1.0 - (d / 30.0).min(1.0)) * 0.6;
192 engine.set_background(ux, uy, Color([0.1, 0.2 * t, 0.8 * t, 1.0]));
193 } else {
194 engine.set_background(ux, uy, C_FLOOR);
195 }
196 } else if path_set.contains(&(x, y)) {
197 engine.set_background(ux, uy, C_PATH);
198 engine.set_foreground(ux, uy, '.', Color::WHITE);
199 } else {
200 engine.set_background(ux, uy, C_FLOOR);
201 }
202 }
203 }
204
205 let s_pos = self.world.get::<Position>(self.start_ent).unwrap();
208 let s_mkr = self.world.get::<Marker>(self.start_ent).unwrap();
209 engine.set_foreground_entity(s_pos.x as u32, s_pos.y as u32, self.start_ent, s_mkr.glyph, s_mkr.color);
210
211 let g_pos = self.world.get::<Position>(self.goal_ent).unwrap();
212 let g_mkr = self.world.get::<Marker>(self.goal_ent).unwrap();
213 engine.set_foreground_entity(g_pos.x as u32, g_pos.y as u32, self.goal_ent, g_mkr.glyph, g_mkr.color);
214
215 let sw = engine.grid_width() as f32 * tw as f32;
217 let sh = engine.grid_height() as f32 * th as f32;
218 let th_f = th as f32;
219
220 engine.ui.ui_rect(0.0, 0.0, sw, th_f * 2.0, Color([0.0, 0.0, 0.0, 0.8]));
221 engine.ui.ui_text(10.0, 5.0, "PATHFINDING SHOWCASE", C_ACCENT, Color::TRANSPARENT, Some(18.0));
222
223 let mode_str = if self.mode_8dir { "8-Directional (Octile)" } else { "4-Directional (Manhattan)" };
224 engine.ui.ui_text(10.0, 25.0, &format!("Mode: {}", mode_str), Color::WHITE, Color::TRANSPARENT, None);
225
226 engine.ui.ui_rect(0.0, sh - th_f * 2.0, sw, th_f * 2.0, Color([0.0, 0.0, 0.0, 0.8]));
228 engine.ui.ui_text(10.0, sh - 40.0, "[Arrows] Move Start [Tab] Toggle 4/8-dir [D] Toggle Dijkstra Heatmap", Color::GRAY, Color::TRANSPARENT, None);
229
230 let path_info = self.path.as_ref().map(|p| format!("Path Length: {}", p.len())).unwrap_or("No Path Found".to_string());
231 engine.ui.ui_text(sw - 200.0, 5.0, &path_info, C_PATH, Color::TRANSPARENT, None);
232 }
233}
234
235fn main() {
236 jEngine::builder()
237 .with_title("jengine — Pathfinding Showcase")
238 .with_size(800, 576)
239 .with_tileset(DEFAULT_TILESET, DEFAULT_TILE_W, DEFAULT_TILE_H)
240 .run(PathfindingShowcase::new());
241}