1use super::Rng;
9use std::collections::{HashMap, HashSet, VecDeque};
10use glam::IVec2;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DungeonTheme {
17 Cave,
18 Cathedral,
19 Laboratory,
20 Temple,
21 Ruins,
22 Void,
23}
24
25impl DungeonTheme {
26 pub fn floor_glyphs(self) -> &'static [char] {
27 match self {
28 DungeonTheme::Cave => &['.', ',', '\''],
29 DungeonTheme::Cathedral => &['+', '.', '\u{00B7}'],
30 DungeonTheme::Laboratory => &['.', ':', '\u{00B7}'],
31 DungeonTheme::Temple => &['\u{256C}', '\u{256A}', '\u{256B}', '.'],
32 DungeonTheme::Ruins => &['.', ',', '~', '"'],
33 DungeonTheme::Void => &['.', '\u{00B7}', '\u{2218}', '\u{00B0}'],
34 }
35 }
36
37 pub fn wall_glyphs(self) -> &'static [char] {
38 match self {
39 DungeonTheme::Cave => &['#', '\u{2588}', '\u{2593}'],
40 DungeonTheme::Cathedral => &['\u{2588}', '\u{2593}', '\u{2502}', '\u{2500}'],
41 DungeonTheme::Laboratory => &['\u{2588}', '\u{2593}', '\u{2554}', '\u{2557}'],
42 DungeonTheme::Temple => &['\u{2588}', '\u{2593}', '\u{2560}', '\u{2563}'],
43 DungeonTheme::Ruins => &['#', '\u{2593}', '%'],
44 DungeonTheme::Void => &['\u{2593}', '\u{2591}', '\u{2592}'],
45 }
46 }
47
48 pub fn ambient_color(self) -> glam::Vec4 {
49 match self {
50 DungeonTheme::Cave => glam::Vec4::new(0.4, 0.3, 0.2, 1.0),
51 DungeonTheme::Cathedral => glam::Vec4::new(0.6, 0.5, 0.8, 1.0),
52 DungeonTheme::Laboratory => glam::Vec4::new(0.3, 0.8, 0.4, 1.0),
53 DungeonTheme::Temple => glam::Vec4::new(0.8, 0.6, 0.2, 1.0),
54 DungeonTheme::Ruins => glam::Vec4::new(0.5, 0.5, 0.4, 1.0),
55 DungeonTheme::Void => glam::Vec4::new(0.1, 0.0, 0.2, 1.0),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct IRect {
65 pub x: i32,
66 pub y: i32,
67 pub w: i32,
68 pub h: i32,
69}
70
71impl IRect {
72 pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { Self { x, y, w, h } }
73
74 pub fn center(&self) -> IVec2 {
75 IVec2::new(self.x + self.w / 2, self.y + self.h / 2)
76 }
77
78 pub fn center_tuple(&self) -> (i32, i32) {
79 (self.x + self.w / 2, self.y + self.h / 2)
80 }
81
82 pub fn contains(&self, tx: i32, ty: i32) -> bool {
83 tx >= self.x && tx < self.x + self.w && ty >= self.y && ty < self.y + self.h
84 }
85
86 pub fn overlaps(&self, other: &IRect) -> bool {
87 self.x < other.x + other.w && self.x + self.w > other.x
88 && self.y < other.y + other.h && self.y + self.h > other.y
89 }
90
91 pub fn area(&self) -> i32 { self.w * self.h }
92
93 pub fn shrink(&self, margin: i32) -> Option<IRect> {
94 let nw = self.w - margin * 2;
95 let nh = self.h - margin * 2;
96 if nw < 1 || nh < 1 { return None; }
97 Some(IRect::new(self.x + margin, self.y + margin, nw, nh))
98 }
99
100 pub fn expand(&self, amount: i32) -> IRect {
101 IRect::new(self.x - amount, self.y - amount, self.w + amount * 2, self.h + amount * 2)
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Tile {
110 Wall,
111 Floor,
112 Door,
113 Corridor,
114 Stairs,
115 Void,
116}
117
118impl Tile {
119 pub fn is_walkable(self) -> bool {
120 matches!(self, Tile::Floor | Tile::Door | Tile::Corridor | Tile::Stairs)
121 }
122
123 pub fn is_opaque(self) -> bool {
124 matches!(self, Tile::Wall | Tile::Void)
125 }
126}
127
128#[derive(Debug, Clone, PartialEq)]
132pub enum RoomType {
133 Normal,
134 Start,
135 Entrance,
136 Exit,
137 Combat(f32),
138 Treasure,
139 Boss,
140 Shop,
141 Puzzle,
142 Rest,
143 Secret,
144 Shrine,
145 Trap,
146}
147
148#[derive(Debug, Clone)]
152pub struct Room {
153 pub id: usize,
154 pub rect: IRect,
155 pub room_type: RoomType,
156 pub connections: Vec<usize>,
157 pub tags: Vec<String>,
158 pub spawns: Vec<IVec2>,
159 pub visited: bool,
160}
161
162impl Room {
163 pub fn new(id: usize, rect: IRect) -> Self {
164 Self {
165 id, rect, room_type: RoomType::Normal,
166 connections: Vec::new(), tags: Vec::new(), spawns: Vec::new(), visited: false,
167 }
168 }
169
170 pub fn center(&self) -> IVec2 { self.rect.center() }
171 pub fn bounds(&self) -> IRect { self.rect }
172
173 pub fn generate_spawns(&mut self, rng: &mut Rng, count: usize) {
174 let IRect { x, y, w, h } = self.rect;
175 self.spawns.clear();
176 for _ in 0..count {
177 let sx = rng.range_i32(x + 1, (x + w - 2).max(x + 1));
178 let sy = rng.range_i32(y + 1, (y + h - 2).max(y + 1));
179 self.spawns.push(IVec2::new(sx, sy));
180 }
181 }
182
183 pub fn add_tag(&mut self, tag: impl Into<String>) {
184 let t = tag.into();
185 if !self.tags.contains(&t) { self.tags.push(t); }
186 }
187
188 pub fn has_tag(&self, tag: &str) -> bool {
189 self.tags.iter().any(|t| t == tag)
190 }
191}
192
193#[derive(Debug, Clone)]
197pub struct Corridor {
198 pub from: usize,
199 pub to: usize,
200 pub path: Vec<IVec2>,
201 pub width: u8,
202 pub has_door: bool,
203 pub bend: IVec2,
204}
205
206impl Corridor {
207 pub fn new(from: usize, to: usize, from_pos: IVec2, to_pos: IVec2, rng: &mut Rng) -> Self {
208 let bend = if rng.chance(0.5) {
209 IVec2::new(to_pos.x, from_pos.y)
210 } else {
211 IVec2::new(from_pos.x, to_pos.y)
212 };
213 let path = Self::l_path(from_pos, to_pos, bend);
214 Self { from, to, path, width: 1, has_door: rng.chance(0.3), bend }
215 }
216
217 fn l_path(from: IVec2, to: IVec2, bend: IVec2) -> Vec<IVec2> {
218 let mut tiles = Vec::new();
219 let dx1 = (bend.x - from.x).signum();
220 let dy1 = (bend.y - from.y).signum();
221 let mut cur = from;
222 while cur != bend {
223 tiles.push(cur);
224 cur.x += dx1;
225 cur.y += dy1;
226 }
227 tiles.push(bend);
228 let dx2 = (to.x - bend.x).signum();
229 let dy2 = (to.y - bend.y).signum();
230 while cur != to {
231 cur.x += dx2;
232 cur.y += dy2;
233 tiles.push(cur);
234 }
235 tiles
236 }
237
238 pub fn tiles(&self) -> &[IVec2] { &self.path }
239}
240
241#[derive(Debug, Clone, Default)]
245pub struct DungeonGraph {
246 pub rooms: Vec<Room>,
247 pub corridors: Vec<Corridor>,
248 adj: Vec<Vec<(usize, usize)>>,
249}
250
251impl DungeonGraph {
252 pub fn new() -> Self { Self::default() }
253
254 pub fn add_room(&mut self, room: Room) -> usize {
255 let id = self.rooms.len();
256 self.rooms.push(room);
257 self.adj.push(Vec::new());
258 id
259 }
260
261 pub fn add_corridor(&mut self, corridor: Corridor) {
262 let ci = self.corridors.len();
263 let from = corridor.from;
264 let to = corridor.to;
265 self.corridors.push(corridor);
266 if from < self.adj.len() { self.adj[from].push((to, ci)); }
267 if to < self.adj.len() { self.adj[to].push((from, ci)); }
268 if from < self.rooms.len() { self.rooms[from].connections.push(to); }
269 if to < self.rooms.len() { self.rooms[to].connections.push(from); }
270 }
271
272 pub fn connected_components(&self) -> usize {
273 if self.rooms.is_empty() { return 0; }
274 let n = self.rooms.len();
275 let mut parent: Vec<usize> = (0..n).collect();
276 fn find(parent: &mut Vec<usize>, x: usize) -> usize {
277 if parent[x] != x { parent[x] = find(parent, parent[x]); }
278 parent[x]
279 }
280 for c in &self.corridors {
281 let rx = find(&mut parent, c.from);
282 let ry = find(&mut parent, c.to);
283 if rx != ry { parent[rx] = ry; }
284 }
285 let mut roots = HashSet::new();
286 for i in 0..n { roots.insert(find(&mut parent, i)); }
287 roots.len()
288 }
289
290 pub fn is_connected(&self) -> bool {
291 self.connected_components() <= 1
292 }
293
294 pub fn shortest_path(&self, a: usize, b: usize) -> Option<Vec<usize>> {
295 if a >= self.rooms.len() || b >= self.rooms.len() { return None; }
296 if a == b { return Some(vec![a]); }
297 let mut visited = vec![false; self.rooms.len()];
298 let mut prev = vec![usize::MAX; self.rooms.len()];
299 let mut queue = VecDeque::new();
300 visited[a] = true;
301 queue.push_back(a);
302 while let Some(cur) = queue.pop_front() {
303 if cur == b {
304 let mut path = Vec::new();
305 let mut node = b;
306 while node != usize::MAX {
307 path.push(node);
308 node = prev[node];
309 }
310 path.reverse();
311 return Some(path);
312 }
313 if cur < self.adj.len() {
314 for &(nb, _) in &self.adj[cur] {
315 if !visited[nb] {
316 visited[nb] = true;
317 prev[nb] = cur;
318 queue.push_back(nb);
319 }
320 }
321 }
322 }
323 None
324 }
325
326 pub fn minimum_spanning_tree(&self) -> Vec<(usize, usize)> {
327 let n = self.rooms.len();
328 if n < 2 { return Vec::new(); }
329 let mut edges: Vec<(f32, usize, usize)> = Vec::new();
330 for i in 0..n {
331 for j in (i + 1)..n {
332 let ci = self.rooms[i].center();
333 let cj = self.rooms[j].center();
334 let dx = (ci.x - cj.x) as f32;
335 let dy = (ci.y - cj.y) as f32;
336 edges.push(((dx * dx + dy * dy).sqrt(), i, j));
337 }
338 }
339 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
340 let mut parent: Vec<usize> = (0..n).collect();
341 fn find(parent: &mut Vec<usize>, x: usize) -> usize {
342 if parent[x] != x { parent[x] = find(parent, parent[x]); }
343 parent[x]
344 }
345 let mut mst = Vec::new();
346 for (_, u, v) in edges {
347 let ru = find(&mut parent, u);
348 let rv = find(&mut parent, v);
349 if ru != rv {
350 parent[ru] = rv;
351 mst.push((u, v));
352 if mst.len() == n - 1 { break; }
353 }
354 }
355 mst
356 }
357}
358
359pub struct BspSplitter {
362 pub min_room_size: i32,
363 pub split_jitter: f32,
364 pub max_depth: u32,
365}
366
367impl BspSplitter {
368 pub fn new(min_room_size: i32, split_jitter: f32, max_depth: u32) -> Self {
369 Self { min_room_size, split_jitter, max_depth }
370 }
371
372 pub fn generate(&self, width: i32, height: i32, rng: &mut Rng) -> DungeonGraph {
373 let bounds = IRect::new(0, 0, width, height);
374 let mut leaves = Vec::new();
375 self.split_node(bounds, rng, 0, &mut leaves);
376
377 let mut graph = DungeonGraph::new();
378 let n = leaves.len();
379 for (i, rect) in leaves.iter().enumerate() {
380 let mut room = Room::new(i, *rect);
381 let spawn_count = rng.range_usize(3) + 1;
382 room.generate_spawns(rng, spawn_count);
383 graph.add_room(room);
384 }
385
386 let mst_edges = graph.minimum_spanning_tree();
387 for (a, b) in mst_edges {
388 let fp = graph.rooms[a].center();
389 let tp = graph.rooms[b].center();
390 let c = Corridor::new(a, b, fp, tp, rng);
391 graph.add_corridor(c);
392 }
393 let extra = (n / 5).max(1);
394 let total = graph.rooms.len();
395 for _ in 0..extra {
396 if total < 2 { break; }
397 let a = rng.range_usize(total);
398 let b = rng.range_usize(total);
399 if a != b {
400 let fp = graph.rooms[a].center();
401 let tp = graph.rooms[b].center();
402 let c = Corridor::new(a, b, fp, tp, rng);
403 graph.add_corridor(c);
404 }
405 }
406
407 if !graph.rooms.is_empty() { graph.rooms[0].room_type = RoomType::Entrance; }
408 let last = graph.rooms.len().saturating_sub(1);
409 if graph.rooms.len() > 1 { graph.rooms[last].room_type = RoomType::Exit; }
410 let specials = graph.rooms.len() / 6;
411 let mut indices: Vec<usize> = (1..graph.rooms.len().saturating_sub(1)).collect();
412 rng.shuffle(&mut indices);
413 for &i in indices.iter().take(specials) {
414 graph.rooms[i].room_type = RoomType::Treasure;
415 }
416 for &i in indices.iter().skip(specials).take(specials) {
417 graph.rooms[i].room_type = RoomType::Combat(rng.range_f32(0.3, 0.9));
418 }
419 if let Some(&bi) = indices.last() {
420 graph.rooms[bi].room_type = RoomType::Boss;
421 }
422
423 graph
424 }
425
426 fn split_node(&self, bounds: IRect, rng: &mut Rng, depth: u32, leaves: &mut Vec<IRect>) {
427 if depth >= self.max_depth
428 || (bounds.w < self.min_room_size * 2 && bounds.h < self.min_room_size * 2)
429 {
430 if let Some(inner) = bounds.shrink(2) {
431 if inner.w >= self.min_room_size && inner.h >= self.min_room_size {
432 let rw = rng.range_i32(self.min_room_size, inner.w);
433 let rh = rng.range_i32(self.min_room_size, inner.h);
434 let rx = rng.range_i32(inner.x, inner.x + inner.w - rw);
435 let ry = rng.range_i32(inner.y, inner.y + inner.h - rh);
436 leaves.push(IRect::new(rx, ry, rw, rh));
437 }
438 }
439 return;
440 }
441 let split_h = if bounds.w > bounds.h { false }
442 else if bounds.h > bounds.w { true }
443 else { rng.chance(0.5) };
444 let jitter = rng.range_f32(-self.split_jitter, self.split_jitter);
445 let ratio = (0.5 + jitter).clamp(0.25, 0.75);
446 if split_h {
447 if bounds.h < self.min_room_size * 2 { leaves.push(bounds); return; }
448 let sy = bounds.y + (bounds.h as f32 * ratio) as i32;
449 self.split_node(IRect::new(bounds.x, bounds.y, bounds.w, sy - bounds.y), rng, depth + 1, leaves);
450 self.split_node(IRect::new(bounds.x, sy, bounds.w, bounds.h - (sy - bounds.y)), rng, depth + 1, leaves);
451 } else {
452 if bounds.w < self.min_room_size * 2 { leaves.push(bounds); return; }
453 let sx = bounds.x + (bounds.w as f32 * ratio) as i32;
454 self.split_node(IRect::new(bounds.x, bounds.y, sx - bounds.x, bounds.h), rng, depth + 1, leaves);
455 self.split_node(IRect::new(sx, bounds.y, bounds.w - (sx - bounds.x), bounds.h), rng, depth + 1, leaves);
456 }
457 }
458}
459
460pub struct RoomPlacer {
463 pub min_room_w: i32,
464 pub max_room_w: i32,
465 pub min_room_h: i32,
466 pub max_room_h: i32,
467 pub separation: i32,
468 pub max_attempts: usize,
469}
470
471impl Default for RoomPlacer {
472 fn default() -> Self {
473 Self { min_room_w: 5, max_room_w: 14, min_room_h: 4, max_room_h: 10, separation: 2, max_attempts: 500 }
474 }
475}
476
477impl RoomPlacer {
478 pub fn new(min_w: i32, max_w: i32, min_h: i32, max_h: i32, sep: i32) -> Self {
479 Self { min_room_w: min_w, max_room_w: max_w, min_room_h: min_h, max_room_h: max_h, separation: sep, max_attempts: 500 }
480 }
481
482 pub fn generate(&self, width: i32, height: i32, num_rooms: usize, rng: &mut Rng) -> DungeonGraph {
483 let mut placed: Vec<IRect> = Vec::new();
484 let mut attempts = 0usize;
485 while placed.len() < num_rooms && attempts < self.max_attempts {
486 attempts += 1;
487 let rw = rng.range_i32(self.min_room_w, self.max_room_w);
488 let rh = rng.range_i32(self.min_room_h, self.max_room_h);
489 let rx = rng.range_i32(1, (width - rw - 1).max(2));
490 let ry = rng.range_i32(1, (height - rh - 1).max(2));
491 let candidate = IRect::new(rx, ry, rw, rh);
492 let expanded = candidate.expand(self.separation);
493 if !placed.iter().any(|r| r.overlaps(&expanded)) { placed.push(candidate); }
494 }
495 let mut graph = DungeonGraph::new();
496 for (i, rect) in placed.iter().enumerate() {
497 let mut room = Room::new(i, *rect);
498 let spawn_count = rng.range_usize(3) + 1;
499 room.generate_spawns(rng, spawn_count);
500 graph.add_room(room);
501 }
502 let mst = graph.minimum_spanning_tree();
503 for (a, b) in mst {
504 let fp = graph.rooms[a].center();
505 let tp = graph.rooms[b].center();
506 let c = Corridor::new(a, b, fp, tp, rng);
507 graph.add_corridor(c);
508 }
509 let extra = (placed.len() / 5).max(1);
510 let n = graph.rooms.len();
511 for _ in 0..extra {
512 if n < 2 { break; }
513 let a = rng.range_usize(n);
514 let b = (a + 1 + rng.range_usize(n - 1)) % n;
515 let fp = graph.rooms[a].center();
516 let tp = graph.rooms[b].center();
517 let c = Corridor::new(a, b, fp, tp, rng);
518 graph.add_corridor(c);
519 }
520 if !graph.rooms.is_empty() { graph.rooms[0].room_type = RoomType::Entrance; }
521 let last = graph.rooms.len().saturating_sub(1);
522 if last > 0 { graph.rooms[last].room_type = RoomType::Exit; }
523 graph
524 }
525}
526
527pub struct CellularDungeon {
530 pub width: usize,
531 pub height: usize,
532 pub fill_prob: f32,
533 pub birth: usize,
534 pub survive: usize,
535 pub iterations: usize,
536}
537
538impl CellularDungeon {
539 pub fn new(width: usize, height: usize) -> Self {
540 Self { width, height, fill_prob: 0.45, birth: 5, survive: 4, iterations: 5 }
541 }
542
543 pub fn generate(&self, rng: &mut Rng) -> Vec<bool> {
544 let n = self.width * self.height;
545 let mut grid: Vec<bool> = (0..n).map(|_| !rng.chance(self.fill_prob)).collect();
546 self.fill_border(&mut grid, false);
547 let mut next = vec![false; n];
548 for _ in 0..self.iterations {
549 for y in 0..self.height {
550 for x in 0..self.width {
551 let nb = self.count_neighbours(&grid, x, y);
552 let alive = grid[y * self.width + x];
553 next[y * self.width + x] = if alive { nb >= self.survive } else { nb >= self.birth };
554 }
555 }
556 self.fill_border(&mut next, false);
557 std::mem::swap(&mut grid, &mut next);
558 }
559 let largest = self.largest_component(&grid);
560 for i in 0..n { if grid[i] && !largest.contains(&i) { grid[i] = false; } }
561 grid
562 }
563
564 fn fill_border(&self, grid: &mut Vec<bool>, val: bool) {
565 let (w, h) = (self.width, self.height);
566 for x in 0..w { grid[x] = val; grid[(h-1)*w+x] = val; }
567 for y in 0..h { grid[y*w] = val; grid[y*w+w-1] = val; }
568 }
569
570 fn count_neighbours(&self, grid: &[bool], x: usize, y: usize) -> usize {
571 let mut count = 0;
572 for dy in -1i32..=1 { for dx in -1i32..=1 {
573 if dx == 0 && dy == 0 { continue; }
574 let nx = x as i32 + dx; let ny = y as i32 + dy;
575 if nx < 0 || ny < 0 || nx as usize >= self.width || ny as usize >= self.height { count += 1; }
576 else if grid[ny as usize * self.width + nx as usize] { count += 1; }
577 }}
578 count
579 }
580
581 fn largest_component(&self, grid: &[bool]) -> HashSet<usize> {
582 let n = self.width * self.height;
583 let mut visited = vec![false; n];
584 let mut best: HashSet<usize> = HashSet::new();
585 for start in 0..n {
586 if !grid[start] || visited[start] { continue; }
587 let mut comp = HashSet::new();
588 let mut q = VecDeque::new();
589 q.push_back(start); visited[start] = true;
590 while let Some(idx) = q.pop_front() {
591 comp.insert(idx);
592 let (x, y) = ((idx % self.width) as i32, (idx / self.width) as i32);
593 for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
594 let (nx, ny) = (x + dx, y + dy);
595 if nx < 0 || ny < 0 || nx as usize >= self.width || ny as usize >= self.height { continue; }
596 let ni = ny as usize * self.width + nx as usize;
597 if grid[ni] && !visited[ni] { visited[ni] = true; q.push_back(ni); }
598 }
599 }
600 if comp.len() > best.len() { best = comp; }
601 }
602 best
603 }
604}
605
606#[derive(Debug, Clone)]
609pub struct WfcTile {
610 pub id: usize,
611 pub name: String,
612 pub weight: f32,
613 pub allowed_neighbours: [Vec<usize>; 4],
614}
615
616impl WfcTile {
617 pub fn new(id: usize, name: impl Into<String>, weight: f32) -> Self {
618 Self { id, name: name.into(), weight, allowed_neighbours: [Vec::new(), Vec::new(), Vec::new(), Vec::new()] }
619 }
620
621 pub fn allow_neighbour(&mut self, direction: usize, neighbour_id: usize) {
622 if direction < 4 { self.allowed_neighbours[direction].push(neighbour_id); }
623 }
624}
625
626pub struct WfcDungeon {
627 pub width: usize,
628 pub height: usize,
629 tiles: Vec<WfcTile>,
630}
631
632impl WfcDungeon {
633 pub fn new(width: usize, height: usize, tiles: Vec<WfcTile>) -> Self {
634 Self { width, height, tiles }
635 }
636
637 pub fn generate(&self, rng: &mut Rng) -> Option<Vec<usize>> {
638 let n = self.width * self.height;
639 let tc = self.tiles.len();
640 if tc == 0 { return None; }
641 let all: Vec<usize> = (0..tc).collect();
642 let mut cells: Vec<Vec<usize>> = vec![all.clone(); n];
643 let max_iter = n * tc + 100;
644 let mut iter = 0;
645 loop {
646 iter += 1;
647 if iter > max_iter { return None; }
648 if cells.iter().all(|c| c.len() == 1) { break; }
649 let ci = cells.iter().enumerate().filter(|(_, c)| c.len() > 1).min_by_key(|(_, c)| c.len()).map(|(i, _)| i)?;
650 let opts = cells[ci].clone();
651 let weighted: Vec<(usize, f32)> = opts.iter().map(|&tid| (tid, self.tiles[tid].weight)).collect();
652 let chosen = rng.pick_weighted(&weighted).copied()?;
653 cells[ci] = vec![chosen];
654 if !self.propagate(&mut cells) { return None; }
655 }
656 Some(cells.iter().map(|c| *c.first().unwrap_or(&0)).collect())
657 }
658
659 fn propagate(&self, cells: &mut Vec<Vec<usize>>) -> bool {
660 let (w, h) = (self.width, self.height);
661 let n = w * h;
662 let mut changed = true;
663 while changed {
664 changed = false;
665 for idx in 0..n {
666 let (x, y) = ((idx % w) as i32, (idx / w) as i32);
667 let nbrs: [(i32, i32, usize); 4] = [(x, y-1, 0),(x, y+1, 1),(x+1, y, 2),(x-1, y, 3)];
668 for (nx, ny, dir) in nbrs {
669 if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
670 let ni = ny as usize * w + nx as usize;
671 let opp = match dir { 0 => 1, 1 => 0, 2 => 3, _ => 2 };
672 let cur = cells[idx].clone();
673 let before = cells[ni].len();
674 cells[ni].retain(|&nt| {
675 cur.iter().any(|&ct| {
676 (ct < self.tiles.len() && self.tiles[ct].allowed_neighbours[dir].contains(&nt))
677 || (nt < self.tiles.len() && self.tiles[nt].allowed_neighbours[opp].contains(&ct))
678 })
679 });
680 if cells[ni].is_empty() { return false; }
681 if cells[ni].len() < before { changed = true; }
682 }
683 }
684 }
685 true
686 }
687
688 pub fn default_tileset() -> Vec<WfcTile> {
689 let mut wall = WfcTile::new(0, "wall", 1.0);
690 let mut floor = WfcTile::new(1, "floor", 3.0);
691 let mut door = WfcTile::new(2, "door", 0.3);
692 for d in 0..4 { wall.allow_neighbour(d, 0); wall.allow_neighbour(d, 1); }
693 for d in 0..4 { floor.allow_neighbour(d, 0); floor.allow_neighbour(d, 1); floor.allow_neighbour(d, 2); }
694 for d in 0..4 { door.allow_neighbour(d, 0); door.allow_neighbour(d, 1); door.allow_neighbour(d, 2); }
695 vec![wall, floor, door]
696 }
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Hash)]
702pub enum ObjectKind {
703 Enemy, TreasureChest, Trap, LightSource, SpawnPoint,
704 ShopKeeper, BossMonster, Shrine, Puzzle, RestArea,
705}
706
707#[derive(Debug, Clone)]
708pub struct PlacedObject {
709 pub kind: ObjectKind,
710 pub position: IVec2,
711 pub metadata: HashMap<String, String>,
712}
713
714impl PlacedObject {
715 pub fn new(kind: ObjectKind, position: IVec2) -> Self {
716 Self { kind, position, metadata: HashMap::new() }
717 }
718 pub fn with_meta(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
719 self.metadata.insert(k.into(), v.into()); self
720 }
721}
722
723pub struct DungeonDecorator {
724 pub enemy_density: f32,
725 pub trap_density: f32,
726 pub light_density: f32,
727}
728
729impl Default for DungeonDecorator {
730 fn default() -> Self { Self { enemy_density: 0.05, trap_density: 0.02, light_density: 0.03 } }
731}
732
733impl DungeonDecorator {
734 pub fn new(ed: f32, td: f32, ld: f32) -> Self { Self { enemy_density: ed, trap_density: td, light_density: ld } }
735
736 pub fn decorate(&self, graph: &DungeonGraph, depth: u32, rng: &mut Rng) -> Vec<PlacedObject> {
737 let mut out = Vec::new();
738 for room in &graph.rooms {
739 let area = room.rect.area() as f32;
740 match &room.room_type {
741 RoomType::Entrance => {
742 out.push(PlacedObject::new(ObjectKind::SpawnPoint, room.center()).with_meta("room_id", room.id.to_string()));
743 self.lights(room, 2, rng, &mut out);
744 }
745 RoomType::Exit => {
746 out.push(PlacedObject::new(ObjectKind::SpawnPoint, room.center()).with_meta("type","exit"));
747 }
748 RoomType::Combat(diff) => {
749 let ne = ((area * self.enemy_density * diff) as usize + 1).min(8);
750 for _ in 0..ne {
751 let pos = self.rpos(room, rng);
752 let lv = (depth as f32 * diff) as u32 + 1;
753 out.push(PlacedObject::new(ObjectKind::Enemy, pos).with_meta("level", lv.to_string()));
754 }
755 let nt = ((area * self.trap_density) as usize).min(3);
756 for _ in 0..nt {
757 out.push(PlacedObject::new(ObjectKind::Trap, self.rpos(room, rng)).with_meta("hidden","true"));
758 }
759 }
760 RoomType::Treasure => {
761 let nc = rng.range_usize(2) + 1;
762 for _ in 0..nc {
763 out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("depth", depth.to_string()));
764 }
765 self.lights(room, 1, rng, &mut out);
766 }
767 RoomType::Boss => {
768 out.push(PlacedObject::new(ObjectKind::BossMonster, room.center()).with_meta("level",(depth*3).to_string()));
769 out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("rarity","legendary"));
770 self.lights(room, 3, rng, &mut out);
771 }
772 RoomType::Shop => {
773 out.push(PlacedObject::new(ObjectKind::ShopKeeper, room.center()).with_meta("stock_seed", rng.next_u64().to_string()));
774 self.lights(room, 2, rng, &mut out);
775 }
776 RoomType::Rest | RoomType::Shrine => {
777 out.push(PlacedObject::new(ObjectKind::RestArea, room.center()));
778 self.lights(room, 1, rng, &mut out);
779 }
780 RoomType::Puzzle => {
781 out.push(PlacedObject::new(ObjectKind::Puzzle, room.center()).with_meta("seed", rng.next_u64().to_string()));
782 }
783 RoomType::Secret => {
784 out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("hidden","true").with_meta("rarity","rare"));
785 }
786 _ => {
787 let ne = ((area * self.enemy_density) as usize).min(5);
788 for _ in 0..ne {
789 out.push(PlacedObject::new(ObjectKind::Enemy, self.rpos(room, rng)).with_meta("level", depth.to_string()));
790 }
791 }
792 }
793 let nl = ((area * self.light_density) as usize).min(4);
794 for _ in 0..nl { out.push(PlacedObject::new(ObjectKind::LightSource, self.rpos(room, rng))); }
795 }
796 out
797 }
798
799 fn lights(&self, room: &Room, n: usize, rng: &mut Rng, out: &mut Vec<PlacedObject>) {
800 for _ in 0..n { out.push(PlacedObject::new(ObjectKind::LightSource, self.rpos(room, rng))); }
801 }
802
803 fn rpos(&self, room: &Room, rng: &mut Rng) -> IVec2 {
804 let r = room.rect;
805 IVec2::new(
806 rng.range_i32(r.x + 1, (r.x + r.w - 2).max(r.x + 1)),
807 rng.range_i32(r.y + 1, (r.y + r.h - 2).max(r.y + 1)),
808 )
809 }
810}
811
812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815pub struct MazeCell {
816 pub walls: [bool; 4],
817 pub visited: bool,
818}
819
820impl Default for MazeCell {
821 fn default() -> Self { Self { walls: [true; 4], visited: false } }
822}
823
824pub struct MazeGenerator {
825 pub width: usize,
826 pub height: usize,
827}
828
829impl MazeGenerator {
830 pub fn new(width: usize, height: usize) -> Self { Self { width, height } }
831
832 fn idx(&self, x: usize, y: usize) -> usize { y * self.width + x }
833
834 fn remove_wall(cells: &mut Vec<MazeCell>, a: usize, b: usize, dir: usize) {
835 let opp = [1usize, 0, 3, 2];
836 cells[a].walls[dir] = false;
837 cells[b].walls[opp[dir]] = false;
838 cells[a].visited = true;
839 cells[b].visited = true;
840 }
841
842 fn nbrs(&self, x: usize, y: usize) -> Vec<(usize, usize, usize)> {
843 let mut v = Vec::new();
844 if y > 0 { v.push((x, y-1, 0)); }
845 if y+1 < self.height { v.push((x, y+1, 1)); }
846 if x+1 < self.width { v.push((x+1, y, 2)); }
847 if x > 0 { v.push((x-1, y, 3)); }
848 v
849 }
850
851 pub fn recursive_backtracker(&self, rng: &mut Rng) -> Vec<MazeCell> {
852 let n = self.width * self.height;
853 let mut cells = vec![MazeCell::default(); n];
854 let mut stack = Vec::new();
855 let s = self.idx(0, 0);
856 cells[s].visited = true;
857 stack.push((0usize, 0usize));
858 while let Some(&(cx, cy)) = stack.last() {
859 let mut unvisited: Vec<_> = self.nbrs(cx, cy).into_iter().filter(|&(nx, ny, _)| !cells[self.idx(nx,ny)].visited).collect();
860 if unvisited.is_empty() { stack.pop(); }
861 else {
862 rng.shuffle(&mut unvisited);
863 let (nx, ny, dir) = unvisited[0];
864 let (ai, bi) = (self.idx(cx,cy), self.idx(nx,ny));
865 Self::remove_wall(&mut cells, ai, bi, dir);
866 cells[bi].visited = true;
867 stack.push((nx, ny));
868 }
869 }
870 cells
871 }
872
873 pub fn ellers_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
874 let n = self.width * self.height;
875 let mut cells = vec![MazeCell::default(); n];
876 let (w, h) = (self.width, self.height);
877 let mut set_id = (1..=w).collect::<Vec<usize>>();
878 let mut next_set = w + 1;
879 for y in 0..h {
880 let last = y + 1 == h;
881 for x in 0..(w-1) {
882 let merge = if last { set_id[x] != set_id[x+1] } else { rng.chance(0.5) && set_id[x] != set_id[x+1] };
883 if merge {
884 let old = set_id[x+1]; let new = set_id[x];
885 for s in &mut set_id { if *s == old { *s = new; } }
886 let (ai, bi) = (self.idx(x, y), self.idx(x+1, y));
887 Self::remove_wall(&mut cells, ai, bi, 2);
888 }
889 }
890 if !last {
891 let mut sets: HashMap<usize, Vec<usize>> = HashMap::new();
892 for x in 0..w { sets.entry(set_id[x]).or_default().push(x); }
893 let mut nid: Vec<usize> = (next_set..next_set+w).collect();
894 next_set += w;
895 for (sid, xs) in &sets {
896 let nv = rng.range_usize(xs.len()) + 1;
897 let mut chosen = xs.clone(); rng.shuffle(&mut chosen);
898 for &cx in chosen.iter().take(nv) {
899 let (ai, bi) = (self.idx(cx,y), self.idx(cx,y+1));
900 Self::remove_wall(&mut cells, ai, bi, 1);
901 nid[cx] = *sid;
902 }
903 }
904 set_id = nid;
905 }
906 }
907 cells
908 }
909
910 pub fn prims_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
911 let n = self.width * self.height;
912 let mut cells = vec![MazeCell::default(); n];
913 let mut in_maze = vec![false; n];
914 let s = self.idx(0,0); in_maze[s] = true; cells[s].visited = true;
915 let mut frontier: Vec<(usize,usize,usize,usize,usize)> = self.nbrs(0,0).into_iter().map(|(nx,ny,d)| (0,0,nx,ny,d)).collect();
916 while !frontier.is_empty() {
917 let fi = rng.range_usize(frontier.len());
918 let (ax,ay,nx,ny,dir) = frontier.swap_remove(fi);
919 let bi = self.idx(nx,ny);
920 if in_maze[bi] { continue; }
921 in_maze[bi] = true; cells[bi].visited = true;
922 Self::remove_wall(&mut cells, self.idx(ax,ay), bi, dir);
923 for (nnx,nny,nd) in self.nbrs(nx,ny) { if !in_maze[self.idx(nnx,nny)] { frontier.push((nx,ny,nnx,nny,nd)); } }
924 }
925 cells
926 }
927
928 pub fn kruskals_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
929 let n = self.width * self.height;
930 let mut cells = vec![MazeCell::default(); n];
931 let mut edges: Vec<(usize,usize,usize,usize,usize)> = Vec::new();
932 for y in 0..self.height { for x in 0..self.width {
933 if x+1 < self.width { edges.push((x, y, x+1, y, 2)); }
934 if y+1 < self.height { edges.push((x, y, x, y+1, 1)); }
935 }}
936 rng.shuffle(&mut edges);
937 let mut parent: Vec<usize> = (0..n).collect();
938 fn find(p: &mut Vec<usize>, x: usize) -> usize {
939 if p[x] != x { p[x] = find(p, p[x]); } p[x]
940 }
941 for (ax,ay,bx,by,dir) in edges {
942 let (ai, bi) = (self.idx(ax,ay), self.idx(bx,by));
943 let (ra, rb) = (find(&mut parent, ai), find(&mut parent, bi));
944 if ra != rb { parent[ra] = rb; Self::remove_wall(&mut cells, ai, bi, dir); }
945 }
946 cells
947 }
948
949 pub fn to_tiles(&self, cells: &[MazeCell]) -> Vec<Tile> {
950 let (tw, th) = (self.width*2+1, self.height*2+1);
951 let mut tiles = vec![Tile::Wall; tw*th];
952 for y in 0..self.height { for x in 0..self.width {
953 let cell = cells[self.idx(x,y)];
954 let (tx,ty) = (x*2+1, y*2+1);
955 tiles[ty*tw+tx] = Tile::Floor;
956 if !cell.walls[2] && x+1 < self.width { tiles[ty*tw+tx+1] = Tile::Corridor; }
957 if !cell.walls[1] && y+1 < self.height { tiles[(ty+1)*tw+tx] = Tile::Corridor; }
958 }}
959 tiles
960 }
961}
962
963#[derive(Debug, Clone)]
966pub struct DungeonFloor {
967 pub width: usize,
968 pub height: usize,
969 pub theme: DungeonTheme,
970 pub rooms: Vec<Room>,
971 pub corridors: Vec<Corridor>,
972 pub tiles: Vec<Tile>,
973 pub depth: u32,
974 pub start: (i32, i32),
975 pub exit: (i32, i32),
976 pub boss_room: Option<usize>,
977}
978
979impl DungeonFloor {
980 pub fn generate(seed: u64, depth: u32, theme: DungeonTheme) -> Self {
981 let mut rng = Rng::new(seed ^ (depth as u64).wrapping_mul(0xdeadbeef));
982 let w = (60 + depth as usize * 5).min(200);
983 let h = (40 + depth as usize * 3).min(120);
984 let graph = BspSplitter::new(5, 0.2, 5 + depth/2).generate(w as i32, h as i32, &mut rng);
985 let rooms = graph.rooms.clone();
986 let corridors = graph.corridors.clone();
987 let start = rooms.first().map(|r| { let c=r.center(); (c.x,c.y) }).unwrap_or((1,1));
988 let exit = rooms.last() .map(|r| { let c=r.center(); (c.x,c.y) }).unwrap_or((w as i32-2, h as i32-2));
989 let boss_room = rooms.iter().position(|r| r.room_type == RoomType::Boss);
990 let mut tiles = vec![Tile::Wall; w*h];
991 for room in &rooms {
992 let IRect{x,y,w:rw,h:rh} = room.rect;
993 for ty in y..(y+rh) { for tx in x..(x+rw) {
994 if tx>=0 && ty>=0 && (tx as usize)<w && (ty as usize)<h {
995 tiles[ty as usize*w+tx as usize] = Tile::Floor;
996 }
997 }}
998 }
999 for corr in &corridors {
1000 for pos in corr.tiles() {
1001 let (tx,ty) = (pos.x, pos.y);
1002 if tx>=0 && ty>=0 && (tx as usize)<w && (ty as usize)<h {
1003 let i = ty as usize*w+tx as usize;
1004 if tiles[i] == Tile::Wall { tiles[i] = Tile::Corridor; }
1005 }
1006 }
1007 if corr.has_door {
1008 let (bx,by) = (corr.bend.x, corr.bend.y);
1009 if bx>=0 && by>=0 && (bx as usize)<w && (by as usize)<h {
1010 tiles[by as usize*w+bx as usize] = Tile::Door;
1011 }
1012 }
1013 }
1014 let (ex,ey) = exit;
1015 if ex>=0 && ey>=0 && (ex as usize)<w && (ey as usize)<h {
1016 tiles[ey as usize*w+ex as usize] = Tile::Stairs;
1017 }
1018 Self { width:w, height:h, theme, rooms, corridors, tiles, depth, start, exit, boss_room }
1019 }
1020
1021 pub fn get(&self, x: i32, y: i32) -> Tile {
1022 if x<0||y<0||(x as usize)>=self.width||(y as usize)>=self.height { return Tile::Void; }
1023 self.tiles[y as usize*self.width+x as usize]
1024 }
1025
1026 pub fn reachable_tiles(&self, sx: i32, sy: i32) -> Vec<(i32,i32)> {
1027 let mut visited = vec![false; self.width*self.height];
1028 let mut queue = VecDeque::new();
1029 let mut result = Vec::new();
1030 if self.get(sx,sy).is_walkable() { queue.push_back((sx,sy)); }
1031 while let Some((x,y)) = queue.pop_front() {
1032 if x<0||y<0||(x as usize)>=self.width||(y as usize)>=self.height { continue; }
1033 let idx = y as usize*self.width+x as usize;
1034 if visited[idx] { continue; }
1035 visited[idx] = true;
1036 if self.tiles[idx].is_walkable() {
1037 result.push((x,y));
1038 for (dx,dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] { queue.push_back((x+dx, y+dy)); }
1039 }
1040 }
1041 result
1042 }
1043
1044 pub fn walkable_count(&self) -> usize { self.tiles.iter().filter(|t| t.is_walkable()).count() }
1045
1046 pub fn room_at(&self, x: i32, y: i32) -> Option<usize> {
1047 self.rooms.iter().position(|r| r.rect.contains(x,y))
1048 }
1049
1050 pub fn doors(&self) -> impl Iterator<Item=(i32,i32)> + '_ {
1051 (0..self.height).flat_map(move |y| (0..self.width).filter_map(move |x| {
1052 if self.tiles[y*self.width+x] == Tile::Door { Some((x as i32, y as i32)) } else { None }
1053 }))
1054 }
1055
1056 pub fn dimensions(&self) -> (usize,usize) { (self.width, self.height) }
1057}
1058
1059#[derive(Debug, Clone)]
1060pub struct FloorMetrics {
1061 pub room_count: usize, pub corridor_count: usize, pub walkable_tiles: usize,
1062 pub total_tiles: usize, pub fill_ratio: f32, pub has_boss: bool,
1063 pub treasure_rooms: usize, pub avg_room_area: f32,
1064}
1065
1066impl FloorMetrics {
1067 pub fn compute(floor: &DungeonFloor) -> Self {
1068 let walkable = floor.walkable_count();
1069 let total = floor.width * floor.height;
1070 let treasure = floor.rooms.iter().filter(|r| r.room_type == RoomType::Treasure).count();
1071 let avg_area = if floor.rooms.is_empty() { 0.0 } else {
1072 floor.rooms.iter().map(|r| r.rect.area()).sum::<i32>() as f32 / floor.rooms.len() as f32
1073 };
1074 Self { room_count: floor.rooms.len(), corridor_count: floor.corridors.len(), walkable_tiles: walkable,
1075 total_tiles: total, fill_ratio: walkable as f32/total as f32, has_boss: floor.boss_room.is_some(),
1076 treasure_rooms: treasure, avg_room_area: avg_area }
1077 }
1078}
1079
1080#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 fn rng() -> Rng { Rng::new(42) }
1087
1088 #[test] fn dungeon_floor_generates_rooms() { assert!(!DungeonFloor::generate(42,1,DungeonTheme::Cave).rooms.is_empty()); }
1089 #[test] fn dungeon_floor_start_walkable() { let f=DungeonFloor::generate(99,1,DungeonTheme::Cave); let (sx,sy)=f.start; assert!(f.get(sx,sy).is_walkable()); }
1090 #[test] fn floor_fill_ratio_reasonable() { let m=FloorMetrics::compute(&DungeonFloor::generate(7,1,DungeonTheme::Cave)); assert!(m.fill_ratio>0.01&&m.fill_ratio<0.95,"fill: {}",m.fill_ratio); }
1091
1092 #[test]
1093 fn bsp_splitter_connected() {
1094 let mut r = rng();
1095 let g = BspSplitter::new(5,0.15,4).generate(80,60,&mut r);
1096 assert!(!g.rooms.is_empty());
1097 assert!(g.is_connected());
1098 }
1099
1100 #[test]
1101 fn room_placer_generates_rooms() {
1102 let mut r = rng();
1103 let g = RoomPlacer::default().generate(100,80,10,&mut r);
1104 assert!(g.rooms.len()>=3,"got {}",g.rooms.len());
1105 }
1106
1107 #[test]
1108 fn cellular_has_floor_tiles() {
1109 let mut r = rng();
1110 let grid = CellularDungeon::new(60,40).generate(&mut r);
1111 assert!(grid.iter().filter(|&&b| b).count()>50);
1112 }
1113
1114 #[test]
1115 fn maze_rb_all_visited() {
1116 let mut r = rng();
1117 assert!(MazeGenerator::new(10,10).recursive_backtracker(&mut r).iter().all(|c| c.visited));
1118 }
1119
1120 #[test]
1121 fn maze_prims_all_visited() {
1122 let mut r = rng();
1123 assert!(MazeGenerator::new(8,8).prims_algorithm(&mut r).iter().all(|c| c.visited));
1124 }
1125
1126 #[test]
1127 fn maze_kruskals_all_visited() {
1128 let mut r = rng();
1129 assert!(MazeGenerator::new(8,8).kruskals_algorithm(&mut r).iter().all(|c| c.visited));
1130 }
1131
1132 #[test]
1133 fn graph_shortest_path() {
1134 let mut r = rng();
1135 let g = RoomPlacer::default().generate(80,60,6,&mut r);
1136 if g.rooms.len()>=2 { assert!(g.shortest_path(0,g.rooms.len()-1).is_some()); }
1137 }
1138
1139 #[test]
1140 fn graph_mst_edges() {
1141 let mut r = rng();
1142 let g = RoomPlacer::default().generate(80,60,8,&mut r);
1143 let n = g.rooms.len();
1144 if n>=2 { assert_eq!(g.minimum_spanning_tree().len(), n-1); }
1145 }
1146
1147 #[test]
1148 fn decorator_places_objects() {
1149 let mut r = rng();
1150 let g = BspSplitter::new(6,0.2,4).generate(80,60,&mut r);
1151 assert!(!DungeonDecorator::default().decorate(&g,3,&mut r).is_empty());
1152 }
1153
1154 #[test]
1155 fn wfc_does_not_panic() {
1156 let mut r = rng();
1157 let _ = WfcDungeon::new(8,8,WfcDungeon::default_tileset()).generate(&mut r);
1158 }
1159
1160 #[test]
1161 fn maze_tiles_correct_size() {
1162 let mut r = rng();
1163 let g = MazeGenerator::new(5,5);
1164 let cells = g.recursive_backtracker(&mut r);
1165 assert_eq!(g.to_tiles(&cells).len(), 11*11);
1166 }
1167
1168 #[test]
1169 fn ellers_all_visited() {
1170 let mut r = rng();
1171 assert!(MazeGenerator::new(8,8).ellers_algorithm(&mut r).iter().all(|c| c.visited));
1172 }
1173}