Skip to main content

pathfinding/
pathfinding.rs

1//! # Pathfinding Showcase
2//! 
3//! A modern demonstration of jengine's pathfinding, audio, and animation systems.
4//! 
5//! Highlights:
6//! - Optimized A* (4-dir and 8-dir) using flat-vector storage.
7//! - Shader-based "juice" (animations) on path markers.
8//! - Reactive UI using immediate-mode primitives.
9//! - Positional and global audio via the restored Kira backend.
10
11use 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
17// ── Constants ────────────────────────────────────────────────────────────────
18
19const 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// ── Components ───────────────────────────────────────────────────────────────
30
31#[derive(Copy, Clone)]
32struct Position { x: i32, y: i32 }
33struct Marker { color: Color, glyph: char }
34
35// ── Pathfinding Example ──────────────────────────────────────────────────────
36
37struct 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        // Spawn interactive markers as entities to enable GPU animations
55        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        // Perimeter
87        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        // Corridors
91        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        // Load sound effects
128        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            // Play a small "juice" animation on the start marker when it moves
146            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            // Shiver the goal to indicate path mode change
157            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        // ── 1. Map Rendering ──
176        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        // ── 2. Entity Rendering (Start/Goal) ──
206        // Using set_foreground_entity enables the shader-based animation offsets
207        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        // ── 3. UI Overlay ──
216        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        // Legend/Controls at bottom
227        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}