1use crate::{Grid, Tile};
19use std::collections::HashMap;
20
21#[derive(Debug, Clone)]
23pub struct SemanticConfig {
24 pub size_thresholds: Vec<(usize, String)>,
26 pub marker_types: Vec<(String, f32)>,
28 pub max_markers_per_region: usize,
30 pub marker_scaling_factor: f32,
32 pub connectivity_type: ConnectivityType,
34 pub region_analysis: RegionAnalysisConfig,
36 pub marker_placement: MarkerPlacementConfig,
38}
39
40#[derive(Debug, Clone)]
42pub enum ConnectivityType {
43 FourConnected,
45 EightConnected,
47}
48
49#[derive(Debug, Clone)]
51pub struct RegionAnalysisConfig {
52 pub analyze_shape: bool,
54 pub analyze_connectivity_patterns: bool,
56 pub min_analysis_size: usize,
58}
59
60#[derive(Debug, Clone)]
62pub struct MarkerPlacementConfig {
63 pub strategy: PlacementStrategy,
65 pub min_marker_distance: usize,
67 pub avoid_walls: bool,
69}
70
71#[derive(Debug, Clone)]
73pub enum PlacementStrategy {
74 Random,
76 Center,
78 Edges,
80 Corners,
82}
83
84impl SemanticConfig {
85 pub fn cave_system() -> Self {
87 Self {
88 size_thresholds: vec![
89 (80, "Chamber".to_string()),
90 (25, "Tunnel".to_string()),
91 (5, "Alcove".to_string()),
92 (0, "Crevice".to_string()),
93 ],
94 marker_types: vec![
95 ("PlayerStart".to_string(), 1.0),
96 ("Exit".to_string(), 0.8),
97 ("Treasure".to_string(), 0.4),
98 ("Enemy".to_string(), 0.6),
99 ("Crystal".to_string(), 0.2),
100 ],
101 max_markers_per_region: 2,
102 marker_scaling_factor: 80.0, connectivity_type: ConnectivityType::EightConnected, region_analysis: RegionAnalysisConfig {
105 analyze_shape: true, analyze_connectivity_patterns: true,
107 min_analysis_size: 15,
108 },
109 marker_placement: MarkerPlacementConfig {
110 strategy: PlacementStrategy::Random,
111 min_marker_distance: 5,
112 avoid_walls: true,
113 },
114 }
115 }
116
117 pub fn room_system() -> Self {
119 Self {
120 size_thresholds: vec![
121 (150, "Hall".to_string()),
122 (50, "Room".to_string()),
123 (15, "Chamber".to_string()),
124 (0, "Closet".to_string()),
125 ],
126 marker_types: vec![
127 ("PlayerStart".to_string(), 1.0),
128 ("Exit".to_string(), 1.0),
129 ("Treasure".to_string(), 0.3),
130 ("Enemy".to_string(), 0.4),
131 ("Furniture".to_string(), 0.7),
132 ],
133 max_markers_per_region: 4,
134 marker_scaling_factor: 60.0, connectivity_type: ConnectivityType::FourConnected, region_analysis: RegionAnalysisConfig {
137 analyze_shape: true, analyze_connectivity_patterns: false,
139 min_analysis_size: 8,
140 },
141 marker_placement: MarkerPlacementConfig {
142 strategy: PlacementStrategy::Center, min_marker_distance: 4,
144 avoid_walls: true,
145 },
146 }
147 }
148
149 pub fn maze_system() -> Self {
151 Self {
152 size_thresholds: vec![
153 (50, "Junction".to_string()),
154 (10, "Corridor".to_string()),
155 (0, "DeadEnd".to_string()),
156 ],
157 marker_types: vec![
158 ("PlayerStart".to_string(), 1.0),
159 ("Exit".to_string(), 1.0),
160 ("Treasure".to_string(), 0.1),
161 ("Trap".to_string(), 0.3),
162 ],
163 max_markers_per_region: 1,
164 marker_scaling_factor: 30.0, connectivity_type: ConnectivityType::FourConnected, region_analysis: RegionAnalysisConfig {
167 analyze_shape: false,
168 analyze_connectivity_patterns: true, min_analysis_size: 5,
170 },
171 marker_placement: MarkerPlacementConfig {
172 strategy: PlacementStrategy::Corners, min_marker_distance: 8,
174 avoid_walls: false, },
176 }
177 }
178}
179
180impl Default for SemanticConfig {
181 fn default() -> Self {
182 Self {
183 size_thresholds: vec![
184 (100, "Large".to_string()),
185 (25, "Medium".to_string()),
186 (5, "Small".to_string()),
187 (0, "Tiny".to_string()),
188 ],
189 marker_types: vec![
190 ("PlayerStart".to_string(), 1.0),
191 ("Exit".to_string(), 1.0),
192 ("Treasure".to_string(), 0.3),
193 ("Enemy".to_string(), 0.5),
194 ],
195 max_markers_per_region: 3,
196 marker_scaling_factor: 100.0,
197 connectivity_type: ConnectivityType::FourConnected,
198 region_analysis: RegionAnalysisConfig {
199 analyze_shape: false,
200 analyze_connectivity_patterns: false,
201 min_analysis_size: 10,
202 },
203 marker_placement: MarkerPlacementConfig {
204 strategy: PlacementStrategy::Random,
205 min_marker_distance: 3,
206 avoid_walls: true,
207 },
208 }
209 }
210}
211
212#[derive(Debug, Clone)]
214pub struct Region {
215 pub id: u32,
216 pub kind: String,
217 pub cells: Vec<(u32, u32)>,
218 pub tags: Vec<String>,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Hash)]
223pub enum MarkerType {
224 Spawn,
226 Exit,
227
228 QuestObjective {
230 priority: u8,
231 },
232 QuestStart,
233 QuestEnd,
234
235 LootTier {
237 tier: u8,
238 },
239 Treasure,
240
241 EncounterZone {
243 difficulty: u8,
244 },
245 BossRoom,
246 SafeZone,
247
248 Custom(String),
250}
251
252impl MarkerType {
253 pub fn category(&self) -> &'static str {
255 match self {
256 MarkerType::Spawn | MarkerType::Exit => "spawn",
257 MarkerType::QuestObjective { .. } | MarkerType::QuestStart | MarkerType::QuestEnd => {
258 "quest"
259 }
260 MarkerType::LootTier { .. } | MarkerType::Treasure => "loot",
261 MarkerType::EncounterZone { .. } | MarkerType::BossRoom | MarkerType::SafeZone => {
262 "encounter"
263 }
264 MarkerType::Custom(_) => "custom",
265 }
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct Marker {
272 pub x: u32,
273 pub y: u32,
274 pub marker_type: MarkerType,
275 pub weight: f32,
276 pub region_id: Option<u32>,
277 pub metadata: HashMap<String, String>,
278}
279
280impl Marker {
281 pub fn new(x: u32, y: u32, marker_type: MarkerType) -> Self {
283 Self {
284 x,
285 y,
286 marker_type,
287 weight: 1.0,
288 region_id: None,
289 metadata: HashMap::new(),
290 }
291 }
292
293 pub fn with_tag(x: u32, y: u32, tag: String) -> Self {
295 Self::new(x, y, MarkerType::Custom(tag))
296 }
297
298 pub fn tag(&self) -> String {
300 match &self.marker_type {
301 MarkerType::Spawn => "spawn".to_string(),
302 MarkerType::Exit => "exit".to_string(),
303 MarkerType::QuestObjective { priority } => format!("quest_objective_{}", priority),
304 MarkerType::QuestStart => "quest_start".to_string(),
305 MarkerType::QuestEnd => "quest_end".to_string(),
306 MarkerType::LootTier { tier } => format!("loot_tier_{}", tier),
307 MarkerType::Treasure => "treasure".to_string(),
308 MarkerType::EncounterZone { difficulty } => format!("encounter_{}", difficulty),
309 MarkerType::BossRoom => "boss_room".to_string(),
310 MarkerType::SafeZone => "safe_zone".to_string(),
311 MarkerType::Custom(tag) => tag.clone(),
312 }
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct MarkerConstraints {
319 pub min_distance_same: Option<f32>,
321 pub min_distance_any: Option<f32>,
323 pub max_distance_from: Vec<(MarkerType, f32)>,
325 pub exclude_types: Vec<MarkerType>,
327 pub require_nearby: Vec<(MarkerType, f32)>,
329}
330
331impl MarkerConstraints {
332 pub fn none() -> Self {
334 Self {
335 min_distance_same: None,
336 min_distance_any: None,
337 max_distance_from: Vec::new(),
338 exclude_types: Vec::new(),
339 require_nearby: Vec::new(),
340 }
341 }
342
343 pub fn quest_objective() -> Self {
345 Self {
346 min_distance_same: Some(10.0),
347 min_distance_any: Some(3.0),
348 max_distance_from: Vec::new(),
349 exclude_types: vec![MarkerType::SafeZone],
350 require_nearby: Vec::new(),
351 }
352 }
353
354 pub fn loot() -> Self {
356 Self {
357 min_distance_same: Some(5.0),
358 min_distance_any: Some(2.0),
359 max_distance_from: Vec::new(),
360 exclude_types: vec![MarkerType::SafeZone],
361 require_nearby: Vec::new(),
362 }
363 }
364}
365
366#[derive(Debug, Clone)]
368pub struct Masks {
369 pub walkable: Vec<Vec<bool>>,
370 pub no_spawn: Vec<Vec<bool>>,
371 pub width: usize,
372 pub height: usize,
373}
374
375#[derive(Debug, Clone)]
377pub struct ConnectivityGraph {
378 pub regions: Vec<u32>,
379 pub edges: Vec<(u32, u32)>,
380}
381
382#[derive(Debug, Clone)]
384pub struct SemanticLayers {
385 pub regions: Vec<Region>,
386 pub markers: Vec<Marker>,
387 pub masks: Masks,
388 pub connectivity: ConnectivityGraph,
389}
390
391pub fn marker_positions(layers: &SemanticLayers, marker_type: &MarkerType) -> Vec<(usize, usize)> {
393 layers
394 .markers
395 .iter()
396 .filter(|marker| &marker.marker_type == marker_type)
397 .map(|marker| (marker.x as usize, marker.y as usize))
398 .collect()
399}
400
401impl Region {
402 pub fn new(id: u32, kind: impl Into<String>) -> Self {
403 Self {
404 id,
405 kind: kind.into(),
406 cells: Vec::new(),
407 tags: Vec::new(),
408 }
409 }
410
411 pub fn add_cell(&mut self, x: u32, y: u32) {
412 self.cells.push((x, y));
413 }
414
415 pub fn add_tag(&mut self, tag: impl Into<String>) {
416 self.tags.push(tag.into());
417 }
418
419 pub fn area(&self) -> usize {
420 self.cells.len()
421 }
422}
423
424impl Marker {
425 pub fn with_weight(mut self, weight: f32) -> Self {
426 self.weight = weight;
427 self
428 }
429
430 pub fn with_region(mut self, region_id: u32) -> Self {
431 self.region_id = Some(region_id);
432 self
433 }
434
435 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
436 self.metadata.insert(key.into(), value.into());
437 self
438 }
439}
440
441#[derive(Debug, Clone)]
443pub struct SemanticRequirements {
444 pub min_regions: HashMap<String, usize>,
446 pub max_regions: HashMap<String, usize>,
448 pub required_connections: Vec<(String, String)>,
450 pub min_walkable_area: Option<usize>,
452 pub required_markers: HashMap<MarkerType, usize>,
454}
455
456impl SemanticRequirements {
457 pub fn none() -> Self {
459 Self {
460 min_regions: HashMap::new(),
461 max_regions: HashMap::new(),
462 required_connections: Vec::new(),
463 min_walkable_area: None,
464 required_markers: HashMap::new(),
465 }
466 }
467
468 pub fn basic_dungeon() -> Self {
470 let mut req = Self::none();
471 req.min_regions.insert("room".to_string(), 3);
472 req.required_connections
473 .push(("room".to_string(), "corridor".to_string()));
474 req.required_markers.insert(MarkerType::Spawn, 1);
475 req.required_markers.insert(MarkerType::Exit, 1);
476 req
477 }
478
479 pub fn validate(&self, layers: &SemanticLayers) -> bool {
481 let mut region_counts: HashMap<String, usize> = HashMap::new();
483 for region in &layers.regions {
484 *region_counts.entry(region.kind.clone()).or_insert(0) += 1;
485 }
486
487 for (kind, min_count) in &self.min_regions {
488 if region_counts.get(kind).unwrap_or(&0) < min_count {
489 return false;
490 }
491 }
492
493 let mut marker_counts: HashMap<MarkerType, usize> = HashMap::new();
495 for marker in &layers.markers {
496 *marker_counts.entry(marker.marker_type.clone()).or_insert(0) += 1;
497 }
498
499 for (marker_type, min_count) in &self.required_markers {
500 if marker_counts.get(marker_type).unwrap_or(&0) < min_count {
501 return false;
502 }
503 }
504
505 true
506 }
507}
508
509impl Masks {
510 pub fn new(width: usize, height: usize) -> Self {
511 Self {
512 walkable: vec![vec![false; width]; height],
513 no_spawn: vec![vec![false; width]; height],
514 width,
515 height,
516 }
517 }
518
519 pub fn from_tiles(tiles: &Grid<Tile>) -> Self {
520 let mut masks = Self::new(tiles.width(), tiles.height());
521
522 for y in 0..tiles.height() {
523 for x in 0..tiles.width() {
524 let walkable = tiles.get(x as i32, y as i32).is_some_and(|t| t.is_floor());
525 masks.walkable[y][x] = walkable;
526 }
527 }
528
529 masks
530 }
531}
532
533impl ConnectivityGraph {
534 pub fn new() -> Self {
535 Self {
536 regions: Vec::new(),
537 edges: Vec::new(),
538 }
539 }
540
541 pub fn add_region(&mut self, id: u32) {
542 if !self.regions.contains(&id) {
543 self.regions.push(id);
544 }
545 }
546
547 pub fn add_edge(&mut self, from: u32, to: u32) {
548 self.add_region(from);
549 self.add_region(to);
550
551 if !self.edges.contains(&(from, to)) && !self.edges.contains(&(to, from)) {
552 self.edges.push((from, to));
553 }
554 }
555}
556
557#[derive(Debug, Clone)]
559pub struct VerticalConnectivity {
560 pub stair_candidates: Vec<(u32, u32, u32, u32)>,
562 pub stairs: Vec<(u32, u32, u32, u32)>,
564 pub floor_accessibility: HashMap<u32, Vec<u32>>,
566}
567
568impl VerticalConnectivity {
569 pub fn new() -> Self {
571 Self {
572 stair_candidates: Vec::new(),
573 stairs: Vec::new(),
574 floor_accessibility: HashMap::new(),
575 }
576 }
577
578 pub fn analyze_stair_candidates(&mut self, floor_grids: &[Grid<Tile>], min_clearance: usize) {
582 self.stair_candidates.clear();
583
584 for floor_idx in 0..floor_grids.len().saturating_sub(1) {
585 let current_floor = &floor_grids[floor_idx];
586 let next_floor = &floor_grids[floor_idx + 1];
587
588 for y in min_clearance..current_floor.height().saturating_sub(min_clearance) {
590 for x in min_clearance..current_floor.width().saturating_sub(min_clearance) {
591 if self.is_valid_stair_location(
592 current_floor,
593 next_floor,
594 x as i32,
595 y as i32,
596 min_clearance as i32,
597 ) {
598 self.stair_candidates.push((
599 x as u32,
600 y as u32,
601 floor_idx as u32,
602 (floor_idx + 1) as u32,
603 ));
604 }
605 }
606 }
607 }
608 }
609
610 fn is_valid_stair_location(
612 &self,
613 floor1: &Grid<Tile>,
614 floor2: &Grid<Tile>,
615 x: i32,
616 y: i32,
617 clearance: i32,
618 ) -> bool {
619 let tile1 = floor1.get(x, y);
621 let tile2 = floor2.get(x, y);
622
623 if !tile1.is_some_and(|t| t.is_floor()) || !tile2.is_some_and(|t| t.is_floor()) {
624 return false;
625 }
626
627 for dy in -clearance..=clearance {
629 for dx in -clearance..=clearance {
630 let check_x = x + dx;
631 let check_y = y + dy;
632
633 let clear1 = floor1.get(check_x, check_y).is_some_and(|t| t.is_floor());
634 let clear2 = floor2.get(check_x, check_y).is_some_and(|t| t.is_floor());
635
636 if !clear1 || !clear2 {
637 return false;
638 }
639 }
640 }
641
642 true
643 }
644
645 pub fn place_stairs(&mut self, max_stairs_per_floor: usize) {
647 self.stairs.clear();
648
649 let mut floor_candidates: HashMap<(u32, u32), Vec<(u32, u32)>> = HashMap::new();
651 for &(x, y, from_floor, to_floor) in &self.stair_candidates {
652 floor_candidates
653 .entry((from_floor, to_floor))
654 .or_default()
655 .push((x, y));
656 }
657
658 for ((from_floor, to_floor), candidates) in floor_candidates {
660 let stairs_to_place = candidates.len().min(max_stairs_per_floor);
661 for &(x, y) in candidates.iter().take(stairs_to_place) {
662 self.stairs.push((x, y, from_floor, to_floor));
663 }
664 }
665 }
666}
667
668impl Default for VerticalConnectivity {
669 fn default() -> Self {
670 Self::new()
671 }
672}
673
674impl Default for ConnectivityGraph {
675 fn default() -> Self {
676 Self::new()
677 }
678}