terrain_forge/
semantic.rs

1//! Semantic layers for procedural generation
2//!
3//! Provides region metadata, spawn markers, and connectivity information
4//! alongside tile generation for game integration.
5
6use crate::{Grid, Tile};
7use std::collections::HashMap;
8
9/// Configuration for semantic layer generation
10#[derive(Debug, Clone)]
11pub struct SemanticConfig {
12    /// Size thresholds for region classification
13    pub size_thresholds: Vec<(usize, String)>,
14    /// Marker types to generate with their weights
15    pub marker_types: Vec<(String, f32)>,
16    /// Maximum number of markers per region type
17    pub max_markers_per_region: usize,
18    /// Region size scaling factor for marker density (default: 100.0)
19    pub marker_scaling_factor: f32,
20    /// Connectivity analysis type
21    pub connectivity_type: ConnectivityType,
22    /// Advanced region analysis options
23    pub region_analysis: RegionAnalysisConfig,
24    /// Marker placement strategy
25    pub marker_placement: MarkerPlacementConfig,
26}
27
28/// Type of connectivity analysis to perform
29#[derive(Debug, Clone)]
30pub enum ConnectivityType {
31    /// 4-connected (orthogonal neighbors only)
32    FourConnected,
33    /// 8-connected (includes diagonal neighbors)
34    EightConnected,
35}
36
37/// Configuration for advanced region analysis
38#[derive(Debug, Clone)]
39pub struct RegionAnalysisConfig {
40    /// Enable shape analysis (aspect ratio, compactness)
41    pub analyze_shape: bool,
42    /// Enable connectivity pattern analysis
43    pub analyze_connectivity_patterns: bool,
44    /// Minimum region size for detailed analysis
45    pub min_analysis_size: usize,
46}
47
48/// Configuration for marker placement strategies
49#[derive(Debug, Clone)]
50pub struct MarkerPlacementConfig {
51    /// Placement strategy for markers
52    pub strategy: PlacementStrategy,
53    /// Minimum distance between markers of same type
54    pub min_marker_distance: usize,
55    /// Avoid placing markers near walls
56    pub avoid_walls: bool,
57}
58
59/// Marker placement strategies
60#[derive(Debug, Clone)]
61pub enum PlacementStrategy {
62    /// Random placement within region
63    Random,
64    /// Place at region center
65    Center,
66    /// Place near region edges
67    Edges,
68    /// Place in corners/extremes
69    Corners,
70}
71
72impl SemanticConfig {
73    /// Configuration optimized for cave systems (Cellular Automata)
74    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, // Caves tend to be larger
91            connectivity_type: ConnectivityType::EightConnected, // Natural cave connections
92            region_analysis: RegionAnalysisConfig {
93                analyze_shape: true, // Cave shape matters
94                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    /// Configuration optimized for structured rooms
106    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, // Rooms are more compact
123            connectivity_type: ConnectivityType::FourConnected, // Structured connections
124            region_analysis: RegionAnalysisConfig {
125                analyze_shape: true, // Room rectangularity matters
126                analyze_connectivity_patterns: false,
127                min_analysis_size: 8,
128            },
129            marker_placement: MarkerPlacementConfig {
130                strategy: PlacementStrategy::Center, // Furniture in room centers
131                min_marker_distance: 4,
132                avoid_walls: true,
133            },
134        }
135    }
136
137    /// Configuration optimized for maze systems
138    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, // Mazes have smaller regions
153            connectivity_type: ConnectivityType::FourConnected, // Maze structure
154            region_analysis: RegionAnalysisConfig {
155                analyze_shape: false,
156                analyze_connectivity_patterns: true, // Junction analysis important
157                min_analysis_size: 5,
158            },
159            marker_placement: MarkerPlacementConfig {
160                strategy: PlacementStrategy::Corners, // Traps in corners
161                min_marker_distance: 8,
162                avoid_walls: false, // Maze walls are part of structure
163            },
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/// A distinct region within the generated map
201#[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/// Hierarchical marker types for different gameplay elements
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
211pub enum MarkerType {
212    /// Basic spawn points
213    Spawn,
214    Exit,
215
216    /// Quest-related markers
217    QuestObjective {
218        priority: u8,
219    },
220    QuestStart,
221    QuestEnd,
222
223    /// Loot and rewards
224    LootTier {
225        tier: u8,
226    },
227    Treasure,
228
229    /// Encounter zones
230    EncounterZone {
231        difficulty: u8,
232    },
233    BossRoom,
234    SafeZone,
235
236    /// Custom marker with string tag (backward compatibility)
237    Custom(String),
238}
239
240impl MarkerType {
241    /// Get the base category of this marker type
242    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/// A spawn marker for entity placement
258#[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    /// Create a new marker with the given type
270    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    /// Create a marker with custom tag (backward compatibility)
282    pub fn with_tag(x: u32, y: u32, tag: String) -> Self {
283        Self::new(x, y, MarkerType::Custom(tag))
284    }
285
286    /// Get the tag string for this marker (backward compatibility)
287    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/// Constraints for marker placement
305#[derive(Debug, Clone)]
306pub struct MarkerConstraints {
307    /// Minimum distance from other markers of the same type
308    pub min_distance_same: Option<f32>,
309    /// Minimum distance from any other marker
310    pub min_distance_any: Option<f32>,
311    /// Maximum distance from specific marker types
312    pub max_distance_from: Vec<(MarkerType, f32)>,
313    /// Marker types that cannot coexist in the same region
314    pub exclude_types: Vec<MarkerType>,
315    /// Required marker types that must exist nearby
316    pub require_nearby: Vec<(MarkerType, f32)>,
317}
318
319impl MarkerConstraints {
320    /// Create constraints with no restrictions
321    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    /// Create constraints for quest objectives (avoid clustering)
332    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    /// Create constraints for loot (can cluster but not too close)
343    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/// Spatial masks for gameplay logic
355#[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/// Region connectivity information
364#[derive(Debug, Clone)]
365pub struct ConnectivityGraph {
366    pub regions: Vec<u32>,
367    pub edges: Vec<(u32, u32)>,
368}
369
370/// Complete semantic information for a generated map
371#[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/// Requirements for semantic-driven generation
420#[derive(Debug, Clone)]
421pub struct SemanticRequirements {
422    /// Minimum number of regions of each type
423    pub min_regions: HashMap<String, usize>,
424    /// Maximum number of regions of each type
425    pub max_regions: HashMap<String, usize>,
426    /// Required connectivity between region types
427    pub required_connections: Vec<(String, String)>,
428    /// Minimum total walkable area
429    pub min_walkable_area: Option<usize>,
430    /// Required marker types and their minimum counts
431    pub required_markers: HashMap<MarkerType, usize>,
432}
433
434impl SemanticRequirements {
435    /// Create requirements with no constraints
436    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    /// Create basic dungeon requirements
447    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    /// Validate if semantic layers meet these requirements
458    pub fn validate(&self, layers: &SemanticLayers) -> bool {
459        // Check region counts
460        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        // Check marker counts
472        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/// Vertical connectivity analysis for multi-floor support
536#[derive(Debug, Clone)]
537pub struct VerticalConnectivity {
538    /// Potential stair locations (x, y, floor_from, floor_to)
539    pub stair_candidates: Vec<(u32, u32, u32, u32)>,
540    /// Confirmed stair placements
541    pub stairs: Vec<(u32, u32, u32, u32)>,
542    /// Regions accessible from each floor
543    pub floor_accessibility: HashMap<u32, Vec<u32>>,
544}
545
546impl VerticalConnectivity {
547    /// Create empty vertical connectivity analysis
548    pub fn new() -> Self {
549        Self {
550            stair_candidates: Vec::new(),
551            stairs: Vec::new(),
552            floor_accessibility: HashMap::new(),
553        }
554    }
555
556    /// Analyze potential stair placements between floors
557    /// This is a basic implementation that looks for suitable floor tiles
558    /// adjacent to walls that could support stairs
559    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            // Find locations that are floor on both levels with wall support
567            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    /// Check if a location is suitable for stair placement
589    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        // Both floors must have floor tiles at this location
598        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        // Check for adequate clearance around the stair location
606        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    /// Place stairs at the best candidate locations
624    pub fn place_stairs(&mut self, max_stairs_per_floor: usize) {
625        self.stairs.clear();
626
627        // Group candidates by floor pair
628        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        // Place limited number of stairs per floor pair
637        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}