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