Skip to main content

terrain_forge/
ops.rs

1//! Unified ops facade: algorithms, effects, and grid combine.
2//!
3//! Use this module for name-based execution with optional JSON params.
4//!
5//! ```rust
6//! use terrain_forge::{Grid, ops};
7//! use terrain_forge::ops::Params;
8//! use serde_json::json;
9//!
10//! let mut grid = Grid::new(80, 60);
11//! let mut params = Params::new();
12//! params.insert("min_room_size".to_string(), json!(6));
13//! params.insert("max_depth".to_string(), json!(5));
14//! params.insert("room_padding".to_string(), json!(1));
15//!
16//! ops::generate("bsp", &mut grid, Some(12345), Some(&params)).unwrap();
17//! ```
18
19use crate::algorithms::*;
20pub use crate::compose::BlendMode as CombineMode;
21use crate::effects;
22use crate::noise;
23use crate::semantic::{marker_positions, MarkerType, SemanticLayers};
24use crate::{Algorithm, Grid, Tile};
25use std::collections::HashMap;
26
27pub type Params = HashMap<String, serde_json::Value>;
28pub type OpResult<T> = Result<T, OpError>;
29
30#[derive(Debug, Clone)]
31/// Error type for ops operations.
32pub struct OpError {
33    message: String,
34}
35
36impl OpError {
37    pub fn new(message: impl Into<String>) -> Self {
38        Self {
39            message: message.into(),
40        }
41    }
42}
43
44impl std::fmt::Display for OpError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        self.message.fmt(f)
47    }
48}
49
50impl std::error::Error for OpError {}
51
52/// Generate using a named algorithm with optional seed and params.
53/// Generates terrain by algorithm name with optional seed and params.
54///
55/// # Examples
56///
57/// ```
58/// use terrain_forge::{Grid, ops};
59///
60/// let mut grid = Grid::new(40, 30);
61/// ops::generate("bsp", &mut grid, Some(42), None).unwrap();
62/// assert!(grid.count(|t| t.is_floor()) > 0);
63/// ```
64pub fn generate(
65    name: &str,
66    grid: &mut Grid<Tile>,
67    seed: Option<u64>,
68    params: Option<&Params>,
69) -> OpResult<()> {
70    let algo = build_algorithm(name, params)?;
71    algo.generate(grid, seed.unwrap_or(0));
72    Ok(())
73}
74
75/// Generate using a named algorithm with optional semantic output.
76/// Generates terrain and extracts semantic layers.
77pub fn generate_with_semantic(
78    name: &str,
79    grid: &mut Grid<Tile>,
80    seed: Option<u64>,
81    params: Option<&Params>,
82    semantic: Option<&mut SemanticLayers>,
83) -> OpResult<()> {
84    let name = name.trim();
85    if name == "prefab" {
86        let (config, library) = build_prefab_config(params)?;
87        let placer = PrefabPlacer::new(config, library);
88        if let Some(semantic) = semantic {
89            placer.generate_with_semantic(grid, seed.unwrap_or(0), semantic);
90            return Ok(());
91        }
92        placer.generate(grid, seed.unwrap_or(0));
93        return Ok(());
94    }
95
96    let algo = build_algorithm(name, params)?;
97    algo.generate(grid, seed.unwrap_or(0));
98    Ok(())
99}
100
101/// Build an algorithm instance from a name + optional params.
102/// Builds an algorithm instance from a name and optional params.
103pub fn build_algorithm(
104    name: &str,
105    params: Option<&Params>,
106) -> OpResult<Box<dyn Algorithm<Tile> + Send + Sync>> {
107    let name = name.trim();
108    match name {
109        "bsp" => {
110            let mut config = BspConfig::default();
111            if let Some(params) = params {
112                if let Some(v) = get_usize(params, "min_room_size") {
113                    config.min_room_size = v;
114                }
115                if let Some(v) = get_usize(params, "max_depth") {
116                    config.max_depth = v;
117                }
118                if let Some(v) = get_usize(params, "room_padding") {
119                    config.room_padding = v;
120                }
121            }
122            Ok(Box::new(Bsp::new(config)))
123        }
124        "cellular" | "cellular_automata" => {
125            let mut config = CellularConfig::default();
126            if let Some(params) = params {
127                if let Some(v) = get_f64(params, "initial_floor_chance") {
128                    config.initial_floor_chance = v;
129                }
130                if let Some(v) = get_usize(params, "iterations") {
131                    config.iterations = v;
132                }
133                if let Some(v) = get_usize(params, "birth_limit") {
134                    config.birth_limit = v;
135                }
136                if let Some(v) = get_usize(params, "death_limit") {
137                    config.death_limit = v;
138                }
139            }
140            Ok(Box::new(CellularAutomata::new(config)))
141        }
142        "drunkard" => {
143            let mut config = DrunkardConfig::default();
144            if let Some(params) = params {
145                if let Some(v) = get_f64(params, "floor_percent") {
146                    config.floor_percent = v;
147                }
148                if let Some(v) = get_usize(params, "max_iterations") {
149                    config.max_iterations = v;
150                }
151            }
152            Ok(Box::new(DrunkardWalk::new(config)))
153        }
154        "maze" => {
155            let mut config = MazeConfig::default();
156            if let Some(params) = params {
157                if let Some(v) = get_usize(params, "corridor_width") {
158                    config.corridor_width = v;
159                }
160            }
161            Ok(Box::new(Maze::new(config)))
162        }
163        "rooms" | "simple_rooms" => {
164            let mut config = SimpleRoomsConfig::default();
165            if let Some(params) = params {
166                if let Some(v) = get_usize(params, "max_rooms") {
167                    config.max_rooms = v;
168                }
169                if let Some(v) = get_usize(params, "min_room_size") {
170                    config.min_room_size = v;
171                }
172                if let Some(v) = get_usize(params, "max_room_size") {
173                    config.max_room_size = v;
174                }
175                if let Some(v) = get_usize(params, "min_spacing") {
176                    config.min_spacing = v;
177                }
178            }
179            Ok(Box::new(SimpleRooms::new(config)))
180        }
181        "voronoi" => {
182            let mut config = VoronoiConfig::default();
183            if let Some(params) = params {
184                if let Some(v) = get_usize(params, "num_points") {
185                    config.num_points = v;
186                }
187                if let Some(v) = get_f64(params, "floor_chance") {
188                    config.floor_chance = v;
189                }
190            }
191            Ok(Box::new(Voronoi::new(config)))
192        }
193        "dla" => {
194            let mut config = DlaConfig::default();
195            if let Some(params) = params {
196                if let Some(v) = get_usize(params, "num_particles") {
197                    config.num_particles = v;
198                }
199                if let Some(v) = get_usize(params, "max_walk_steps") {
200                    config.max_walk_steps = v;
201                }
202            }
203            Ok(Box::new(Dla::new(config)))
204        }
205        "wfc" | "wave_function_collapse" => {
206            let mut config = WfcConfig::default();
207            if let Some(params) = params {
208                if let Some(v) = get_f64(params, "floor_weight") {
209                    config.floor_weight = v;
210                }
211                if let Some(v) = get_usize(params, "pattern_size") {
212                    config.pattern_size = v;
213                }
214                if let Some(v) = get_bool(params, "enable_backtracking") {
215                    config.enable_backtracking = v;
216                }
217            }
218            Ok(Box::new(Wfc::new(config)))
219        }
220        "percolation" => {
221            let mut config = PercolationConfig::default();
222            if let Some(params) = params {
223                if let Some(v) = get_f64(params, "fill_probability") {
224                    config.fill_probability = v;
225                }
226                if let Some(v) = get_bool(params, "keep_largest") {
227                    config.keep_largest = v;
228                }
229            }
230            Ok(Box::new(Percolation::new(config)))
231        }
232        "diamond_square" => {
233            let mut config = DiamondSquareConfig::default();
234            if let Some(params) = params {
235                if let Some(v) = get_f64(params, "roughness") {
236                    config.roughness = v;
237                }
238                if let Some(v) = get_f64(params, "threshold") {
239                    config.threshold = v;
240                }
241            }
242            Ok(Box::new(DiamondSquare::new(config)))
243        }
244        "agent" => {
245            let mut config = AgentConfig::default();
246            if let Some(params) = params {
247                if let Some(v) = get_usize(params, "num_agents") {
248                    config.num_agents = v;
249                }
250                if let Some(v) = get_usize(params, "steps_per_agent") {
251                    config.steps_per_agent = v;
252                }
253                if let Some(v) = get_f64(params, "turn_chance") {
254                    config.turn_chance = v;
255                }
256            }
257            Ok(Box::new(AgentBased::new(config)))
258        }
259        "fractal" => {
260            let mut config = FractalConfig::default();
261            if let Some(params) = params {
262                if let Some(v) = get_str(params, "fractal_type") {
263                    config.fractal_type = match v {
264                        "julia" => FractalType::Julia,
265                        _ => FractalType::Mandelbrot,
266                    };
267                }
268                if let Some(v) = get_usize(params, "max_iterations") {
269                    config.max_iterations = v;
270                }
271            }
272            Ok(Box::new(Fractal::new(config)))
273        }
274        "noise_fill" | "noise" => {
275            let mut config = NoiseFillConfig::default();
276            if let Some(params) = params {
277                config.noise = parse_noise_type(params.get("noise"));
278                if let Some(v) = get_f64(params, "frequency") {
279                    config.frequency = v;
280                }
281                if let Some(v) = get_f64(params, "scale").or_else(|| get_f64(params, "size")) {
282                    config.scale = v;
283                }
284                if let Some(range) = get_range(params, "range")
285                    .or_else(|| get_range(params, "value_range"))
286                    .or_else(|| get_range(params, "output_range"))
287                {
288                    config.output_range = range;
289                }
290                if let Some(range) = get_range(params, "fill_range") {
291                    config.fill_range = Some(range);
292                }
293                if let Some(v) = get_f64(params, "threshold") {
294                    config.threshold = v;
295                }
296                if let Some(v) = get_u32(params, "octaves") {
297                    config.octaves = v.max(1);
298                }
299                if let Some(v) = get_f64(params, "lacunarity") {
300                    config.lacunarity = v;
301                }
302                if let Some(v) = get_f64(params, "persistence") {
303                    config.persistence = v;
304                }
305            }
306            Ok(Box::new(NoiseFill::new(config)))
307        }
308        "glass_seam" | "gsb" => {
309            let mut config = GlassSeamConfig::default();
310            if let Some(params) = params {
311                if let Some(v) = get_f64(params, "coverage_threshold") {
312                    config.coverage_threshold = v;
313                }
314                if let Some(v) = get_points(params, "required_points") {
315                    config.required_points = v;
316                }
317                if let Some(v) = get_usize(params, "carve_radius") {
318                    config.carve_radius = v;
319                }
320                if let Some(v) = get_bool(params, "use_mst_terminals") {
321                    config.use_mst_terminals = v;
322                }
323            }
324            Ok(Box::new(GlassSeam::new(config)))
325        }
326        "room_accretion" | "accretion" => {
327            let mut config = RoomAccretionConfig::default();
328            if let Some(params) = params {
329                if let Some(templates_val) = params.get("templates") {
330                    let templates = parse_room_templates(templates_val);
331                    if !templates.is_empty() {
332                        config.templates = templates;
333                    }
334                }
335                if let Some(v) = get_usize(params, "max_rooms") {
336                    config.max_rooms = v;
337                }
338                if let Some(v) = get_f64(params, "loop_chance") {
339                    config.loop_chance = v;
340                }
341            }
342            Ok(Box::new(RoomAccretion::new(config)))
343        }
344        "prefab" => {
345            let (config, library) = build_prefab_config(params)?;
346            Ok(Box::new(PrefabPlacer::new(config, library)))
347        }
348        _ => crate::algorithms::get(name)
349            .ok_or_else(|| OpError::new(format!("Unknown algorithm: {}", name))),
350    }
351}
352
353/// Apply a named effect with optional params.
354/// Applies a named effect to the grid.
355pub fn effect(
356    name: &str,
357    grid: &mut Grid<Tile>,
358    params: Option<&Params>,
359    semantic: Option<&SemanticLayers>,
360) -> OpResult<()> {
361    let name = name.trim();
362    match name {
363        "erode" => {
364            let iterations = params.and_then(|p| get_usize(p, "iterations")).unwrap_or(1);
365            effects::erode(grid, iterations);
366            Ok(())
367        }
368        "dilate" => {
369            let iterations = params.and_then(|p| get_usize(p, "iterations")).unwrap_or(1);
370            effects::dilate(grid, iterations);
371            Ok(())
372        }
373        "open" => {
374            let iterations = params.and_then(|p| get_usize(p, "iterations")).unwrap_or(1);
375            effects::open(grid, iterations);
376            Ok(())
377        }
378        "close" => {
379            let iterations = params.and_then(|p| get_usize(p, "iterations")).unwrap_or(1);
380            effects::close(grid, iterations);
381            Ok(())
382        }
383        "bridge_gaps" => {
384            let max_distance = params
385                .and_then(|p| get_usize(p, "max_distance"))
386                .unwrap_or(5);
387            effects::bridge_gaps(grid, max_distance);
388            Ok(())
389        }
390        "remove_dead_ends" => {
391            let iterations = params.and_then(|p| get_usize(p, "iterations")).unwrap_or(3);
392            effects::remove_dead_ends(grid, iterations);
393            Ok(())
394        }
395        "connect_regions_spanning" => {
396            let chance = params
397                .and_then(|p| get_f64(p, "extra_connection_chance"))
398                .unwrap_or(0.2);
399            let seed = params.and_then(|p| get_u64(p, "seed")).unwrap_or(42);
400            let mut rng = crate::Rng::new(seed);
401            effects::connect_regions_spanning(grid, chance, &mut rng);
402            Ok(())
403        }
404        "mirror" => {
405            let (horizontal, vertical) = params
406                .map(|p| {
407                    (
408                        get_bool(p, "horizontal").unwrap_or(true),
409                        get_bool(p, "vertical").unwrap_or(false),
410                    )
411                })
412                .unwrap_or((true, false));
413            effects::mirror(grid, horizontal, vertical);
414            Ok(())
415        }
416        "rotate" => {
417            let degrees = params.and_then(|p| get_u64(p, "degrees")).unwrap_or(90) as u32;
418            effects::rotate(grid, degrees);
419            Ok(())
420        }
421        "scatter" => {
422            let density = params.and_then(|p| get_f64(p, "density")).unwrap_or(0.12);
423            let seed = params.and_then(|p| get_u64(p, "seed")).unwrap_or(42);
424            effects::scatter(grid, density, seed);
425            Ok(())
426        }
427        "gaussian_blur" => {
428            let radius = params.and_then(|p| get_usize(p, "radius")).unwrap_or(1);
429            effects::gaussian_blur(grid, radius);
430            Ok(())
431        }
432        "median_filter" => {
433            let radius = params.and_then(|p| get_usize(p, "radius")).unwrap_or(1);
434            effects::median_filter(grid, radius);
435            Ok(())
436        }
437        "domain_warp" => {
438            let amplitude = params.and_then(|p| get_f64(p, "amplitude")).unwrap_or(2.0);
439            let frequency = params.and_then(|p| get_f64(p, "frequency")).unwrap_or(0.08);
440            let seed = params.and_then(|p| get_u64(p, "seed")).unwrap_or(42);
441            let noise = noise::Perlin::new(seed);
442            effects::domain_warp(grid, &noise, amplitude, frequency);
443            Ok(())
444        }
445        "clear_rect" => {
446            let Some(params) = params else {
447                return Err(OpError::new("clear_rect requires params"));
448            };
449            let center = parse_point(params.get("center"))
450                .ok_or_else(|| OpError::new("clear_rect requires center: [x, y]"))?;
451            let width = get_usize(params, "width").unwrap_or(3);
452            let height = get_usize(params, "height").unwrap_or(3);
453            effects::clear_rect(grid, center, width, height);
454            Ok(())
455        }
456        "clear_marker_area" => {
457            let Some(semantic) = semantic else {
458                return Err(OpError::new("clear_marker_area requires semantic layers"));
459            };
460            let Some(params) = params else {
461                return Err(OpError::new("clear_marker_area requires params"));
462            };
463            let marker_name = get_str(params, "marker").unwrap_or("spawn");
464            let marker_type = parse_marker_type(marker_name);
465            let width = get_usize(params, "width").unwrap_or(5);
466            let height = get_usize(params, "height").unwrap_or(5);
467            let positions = marker_positions(semantic, &marker_type);
468            if positions.is_empty() {
469                return Err(OpError::new(format!(
470                    "No markers found for {}",
471                    marker_name
472                )));
473            }
474            for pos in positions {
475                effects::clear_rect(grid, pos, width, height);
476            }
477            Ok(())
478        }
479        "connect_markers" => {
480            let Some(semantic) = semantic else {
481                return Err(OpError::new("connect_markers requires semantic layers"));
482            };
483            let Some(params) = params else {
484                return Err(OpError::new("connect_markers requires params"));
485            };
486            let from = get_str(params, "from").unwrap_or("spawn");
487            let to = get_str(params, "to").unwrap_or("exit");
488            let method = get_str(params, "method").unwrap_or("line");
489            let radius = get_usize(params, "radius").unwrap_or(0);
490            let from_type = parse_marker_type(from);
491            let to_type = parse_marker_type(to);
492            let connect_method = match method {
493                "path" => effects::MarkerConnectMethod::Path,
494                _ => effects::MarkerConnectMethod::Line,
495            };
496            if !effects::connect_markers(
497                grid,
498                semantic,
499                &from_type,
500                &to_type,
501                connect_method,
502                radius,
503            ) {
504                return Err(OpError::new(format!(
505                    "Failed to connect markers {} -> {}",
506                    from, to
507                )));
508            }
509            Ok(())
510        }
511        "invert" => {
512            effects::invert(grid);
513            Ok(())
514        }
515        "resize" => {
516            let Some(params) = params else {
517                return Err(OpError::new("resize requires params"));
518            };
519            let width =
520                get_usize(params, "width").ok_or_else(|| OpError::new("resize requires width"))?;
521            let height = get_usize(params, "height")
522                .ok_or_else(|| OpError::new("resize requires height"))?;
523            let pad = parse_tile(params.get("pad").or_else(|| params.get("pad_value")))
524                .unwrap_or(Tile::Wall);
525            effects::resize(grid, width, height, pad);
526            Ok(())
527        }
528        _ => Err(OpError::new(format!("Unknown effect: {}", name))),
529    }
530}
531
532/// Combine another grid into the current grid using a mode.
533/// Combines two grids using the specified blend mode.
534pub fn combine(mode: CombineMode, grid: &mut Grid<Tile>, other: &Grid<Tile>) -> OpResult<()> {
535    let w = grid.width().min(other.width());
536    let h = grid.height().min(other.height());
537    for y in 0..h {
538        for x in 0..w {
539            let other_cell = other[(x, y)];
540            match mode {
541                CombineMode::Replace => {
542                    grid.set(x as i32, y as i32, other_cell);
543                }
544                CombineMode::Union => {
545                    if other_cell.is_floor() {
546                        grid.set(x as i32, y as i32, Tile::Floor);
547                    }
548                }
549                CombineMode::Intersect | CombineMode::Mask => {
550                    if !other_cell.is_floor() {
551                        grid.set(x as i32, y as i32, Tile::Wall);
552                    }
553                }
554                CombineMode::Difference => {
555                    if other_cell.is_floor() {
556                        grid.set(x as i32, y as i32, Tile::Wall);
557                    }
558                }
559            }
560        }
561    }
562    Ok(())
563}
564
565fn get_usize(params: &Params, key: &str) -> Option<usize> {
566    params.get(key).and_then(value_to_u64).map(|v| v as usize)
567}
568
569fn get_u64(params: &Params, key: &str) -> Option<u64> {
570    params.get(key).and_then(value_to_u64)
571}
572
573fn get_u32(params: &Params, key: &str) -> Option<u32> {
574    get_u64(params, key).and_then(|v| u32::try_from(v).ok())
575}
576
577fn get_f64(params: &Params, key: &str) -> Option<f64> {
578    params.get(key).and_then(value_to_f64)
579}
580
581fn get_bool(params: &Params, key: &str) -> Option<bool> {
582    params.get(key).and_then(value_to_bool)
583}
584
585fn get_str<'a>(params: &'a Params, key: &str) -> Option<&'a str> {
586    params.get(key).and_then(|v| v.as_str())
587}
588
589fn get_range(params: &Params, key: &str) -> Option<(f64, f64)> {
590    parse_range(params.get(key))
591}
592
593fn value_to_u64(value: &serde_json::Value) -> Option<u64> {
594    value
595        .as_u64()
596        .or_else(|| value.as_i64().and_then(|v| u64::try_from(v).ok()))
597        .or_else(|| value.as_str().and_then(|v| v.parse::<u64>().ok()))
598}
599
600fn value_to_f64(value: &serde_json::Value) -> Option<f64> {
601    value
602        .as_f64()
603        .or_else(|| value.as_u64().map(|v| v as f64))
604        .or_else(|| value.as_i64().map(|v| v as f64))
605        .or_else(|| value.as_str().and_then(|v| v.parse::<f64>().ok()))
606}
607
608fn value_to_bool(value: &serde_json::Value) -> Option<bool> {
609    value
610        .as_bool()
611        .or_else(|| value.as_str().and_then(|v| v.parse::<bool>().ok()))
612}
613
614fn parse_point(value: Option<&serde_json::Value>) -> Option<(usize, usize)> {
615    let value = value?;
616    let array = value.as_array()?;
617    if array.len() != 2 {
618        return None;
619    }
620    let x = value_to_u64(&array[0])? as usize;
621    let y = value_to_u64(&array[1])? as usize;
622    Some((x, y))
623}
624
625fn parse_range(value: Option<&serde_json::Value>) -> Option<(f64, f64)> {
626    let value = value?;
627    if let Some(arr) = value.as_array() {
628        if arr.len() == 2 {
629            let min = value_to_f64(&arr[0])?;
630            let max = value_to_f64(&arr[1])?;
631            return Some((min, max));
632        }
633    }
634    if let Some(obj) = value.as_object() {
635        let min = obj.get("min").and_then(value_to_f64)?;
636        let max = obj.get("max").and_then(value_to_f64)?;
637        return Some((min, max));
638    }
639    None
640}
641
642fn get_points(params: &Params, key: &str) -> Option<Vec<(usize, usize)>> {
643    let value = params.get(key)?;
644    let array = value.as_array()?;
645    let mut points = Vec::new();
646    for item in array {
647        if let Some(point) = parse_point(Some(item)) {
648            points.push(point);
649        }
650    }
651    Some(points)
652}
653
654fn parse_noise_type(value: Option<&serde_json::Value>) -> NoiseType {
655    let Some(value) = value else {
656        return NoiseType::Perlin;
657    };
658    let Some(name) = value.as_str() else {
659        return NoiseType::Perlin;
660    };
661    match name.trim().to_ascii_lowercase().as_str() {
662        "simplex" => NoiseType::Simplex,
663        "value" => NoiseType::Value,
664        "worley" | "cellular" => NoiseType::Worley,
665        _ => NoiseType::Perlin,
666    }
667}
668
669fn parse_room_templates(val: &serde_json::Value) -> Vec<RoomTemplate> {
670    let mut templates = Vec::new();
671    if let Some(array) = val.as_array() {
672        for item in array {
673            if let Some(obj) = item.as_object() {
674                if let Some(rect) = obj.get("Rectangle") {
675                    if let Some(rect_obj) = rect.as_object() {
676                        let min = rect_obj.get("min").and_then(value_to_u64).unwrap_or(5) as usize;
677                        let max = rect_obj.get("max").and_then(value_to_u64).unwrap_or(10) as usize;
678                        templates.push(RoomTemplate::Rectangle { min, max });
679                    }
680                } else if let Some(circle) = obj.get("Circle") {
681                    if let Some(circle_obj) = circle.as_object() {
682                        let min_radius = circle_obj
683                            .get("min_radius")
684                            .and_then(value_to_u64)
685                            .unwrap_or(3) as usize;
686                        let max_radius = circle_obj
687                            .get("max_radius")
688                            .and_then(value_to_u64)
689                            .unwrap_or(6) as usize;
690                        templates.push(RoomTemplate::Circle {
691                            min_radius,
692                            max_radius,
693                        });
694                    }
695                } else if let Some(blob) = obj.get("Blob") {
696                    if let Some(blob_obj) = blob.as_object() {
697                        let size =
698                            blob_obj.get("size").and_then(value_to_u64).unwrap_or(8) as usize;
699                        let smoothing = blob_obj
700                            .get("smoothing")
701                            .and_then(value_to_u64)
702                            .unwrap_or(2) as usize;
703                        templates.push(RoomTemplate::Blob { size, smoothing });
704                    }
705                }
706            }
707        }
708    }
709    if templates.is_empty() {
710        templates.push(RoomTemplate::Rectangle { min: 5, max: 10 });
711    }
712    templates
713}
714
715fn parse_prefabs(val: &serde_json::Value) -> Vec<Prefab> {
716    let mut prefabs = Vec::new();
717    if let Some(array) = val.as_array() {
718        for item in array {
719            if let Some(obj) = item.as_object() {
720                if let Some(pattern) = obj.get("pattern") {
721                    if let Some(pattern_array) = pattern.as_array() {
722                        let pattern_strs: Vec<&str> =
723                            pattern_array.iter().filter_map(|v| v.as_str()).collect();
724                        if !pattern_strs.is_empty() {
725                            let legend = obj
726                                .get("legend")
727                                .and_then(|v| serde_json::from_value(v.clone()).ok());
728                            let mut prefab = Prefab::from_data(PrefabData {
729                                name: obj
730                                    .get("name")
731                                    .and_then(|v| v.as_str())
732                                    .unwrap_or("unnamed")
733                                    .to_string(),
734                                width: pattern_strs.first().map(|s| s.len()).unwrap_or(0),
735                                height: pattern_strs.len(),
736                                pattern: pattern_strs.iter().map(|s| (*s).to_string()).collect(),
737                                weight: obj.get("weight").and_then(value_to_f64).unwrap_or(1.0)
738                                    as f32,
739                                tags: obj.get("tags").and_then(parse_tags).unwrap_or_default(),
740                                legend,
741                            });
742                            if prefab.name.is_empty() {
743                                prefab.name = "unnamed".to_string();
744                            }
745                            prefabs.push(prefab);
746                        }
747                    }
748                }
749            }
750        }
751    }
752    prefabs
753}
754
755fn build_prefab_config(params: Option<&Params>) -> OpResult<(PrefabConfig, PrefabLibrary)> {
756    let mut config = PrefabConfig::default();
757    let mut library = PrefabLibrary::new();
758    if let Some(params) = params {
759        if let Some(paths_val) = params.get("library_paths") {
760            let paths = parse_string_list(paths_val);
761            if !paths.is_empty() {
762                match PrefabLibrary::load_from_paths(paths) {
763                    Ok(loaded) => library.extend_from(loaded),
764                    Err(err) => {
765                        return Err(OpError::new(format!(
766                            "Failed to load prefab library paths: {}",
767                            err
768                        )))
769                    }
770                }
771            }
772        }
773        if let Some(dir) = get_str(params, "library_dir") {
774            match PrefabLibrary::load_from_dir(dir) {
775                Ok(loaded) => library.extend_from(loaded),
776                Err(err) => {
777                    return Err(OpError::new(format!(
778                        "Failed to load prefab library dir '{}': {}",
779                        dir, err
780                    )))
781                }
782            }
783        }
784        if let Some(path) = get_str(params, "library_path") {
785            match PrefabLibrary::load_from_json(path) {
786                Ok(loaded) => library.extend_from(loaded),
787                Err(err) => {
788                    return Err(OpError::new(format!(
789                        "Failed to load prefab library '{}': {}",
790                        path, err
791                    )))
792                }
793            }
794        }
795        if let Some(prefabs_val) = params.get("prefabs") {
796            for prefab in parse_prefabs(prefabs_val) {
797                library.add_prefab(prefab);
798            }
799        }
800        if let Some(tags_val) = params.get("tags") {
801            if let Some(tags) = parse_tags(tags_val) {
802                config.tags = Some(tags);
803            }
804        }
805        if let Some(mode) = get_str(params, "placement_mode") {
806            if let Some(parsed) = parse_prefab_placement_mode(mode) {
807                config.placement_mode = parsed;
808            }
809        }
810        if let Some(v) = get_usize(params, "max_prefabs") {
811            config.max_prefabs = v;
812        }
813        if let Some(v) = get_usize(params, "min_spacing") {
814            config.min_spacing = v;
815        }
816        if let Some(v) = get_bool(params, "allow_rotation") {
817            config.allow_rotation = v;
818        }
819        if let Some(v) = get_bool(params, "allow_mirroring") {
820            config.allow_mirroring = v;
821        }
822        if let Some(v) = get_bool(params, "weighted_selection") {
823            config.weighted_selection = v;
824        }
825    }
826    if library.get_prefabs().is_empty() {
827        library.add_prefab(Prefab::rect(5, 5));
828    }
829    Ok((config, library))
830}
831
832fn parse_tags(value: &serde_json::Value) -> Option<Vec<String>> {
833    if let Some(arr) = value.as_array() {
834        let tags: Vec<String> = arr
835            .iter()
836            .filter_map(|v| v.as_str().map(|s| s.to_string()))
837            .collect();
838        if tags.is_empty() {
839            None
840        } else {
841            Some(tags)
842        }
843    } else if let Some(s) = value.as_str() {
844        let trimmed = s.trim();
845        if trimmed.is_empty() {
846            None
847        } else {
848            Some(vec![trimmed.to_string()])
849        }
850    } else {
851        None
852    }
853}
854
855fn parse_string_list(value: &serde_json::Value) -> Vec<String> {
856    if let Some(arr) = value.as_array() {
857        arr.iter()
858            .filter_map(|v| v.as_str().map(|s| s.to_string()))
859            .collect()
860    } else if let Some(s) = value.as_str() {
861        if s.trim().is_empty() {
862            Vec::new()
863        } else {
864            vec![s.trim().to_string()]
865        }
866    } else {
867        Vec::new()
868    }
869}
870
871fn parse_prefab_placement_mode(value: &str) -> Option<PrefabPlacementMode> {
872    match value.trim().to_ascii_lowercase().as_str() {
873        "overwrite" => Some(PrefabPlacementMode::Overwrite),
874        "merge" => Some(PrefabPlacementMode::Merge),
875        "paint_floor" | "paintfloor" | "floor" => Some(PrefabPlacementMode::PaintFloor),
876        "paint_wall" | "paintwall" | "wall" => Some(PrefabPlacementMode::PaintWall),
877        _ => None,
878    }
879}
880
881fn parse_tile(value: Option<&serde_json::Value>) -> Option<Tile> {
882    let value = value?;
883    if let Some(b) = value.as_bool() {
884        return Some(if b { Tile::Floor } else { Tile::Wall });
885    }
886    if let Some(n) = value_to_u64(value) {
887        return Some(if n == 0 { Tile::Wall } else { Tile::Floor });
888    }
889    let s = value.as_str()?;
890    match s.trim().to_ascii_lowercase().as_str() {
891        "floor" | "f" | "1" | "true" => Some(Tile::Floor),
892        "wall" | "w" | "0" | "false" => Some(Tile::Wall),
893        _ => None,
894    }
895}
896
897fn parse_marker_type(name: &str) -> MarkerType {
898    let trimmed = name.trim();
899    let lower = trimmed.to_ascii_lowercase();
900    match lower.as_str() {
901        "spawn" => MarkerType::Spawn,
902        "playerstart" | "player_start" => MarkerType::Custom("PlayerStart".to_string()),
903        "exit" => MarkerType::Custom("Exit".to_string()),
904        "treasure" | "loot" => MarkerType::Custom("Treasure".to_string()),
905        "enemy" => MarkerType::Custom("Enemy".to_string()),
906        "furniture" => MarkerType::Custom("Furniture".to_string()),
907        "boss" | "boss_room" => MarkerType::BossRoom,
908        "safe_zone" | "safezone" => MarkerType::SafeZone,
909        _ if lower.starts_with("quest_objective") => {
910            let lvl = lower
911                .split('_')
912                .next_back()
913                .and_then(|v| v.parse::<u8>().ok())
914                .unwrap_or(1);
915            MarkerType::QuestObjective { priority: lvl }
916        }
917        _ if lower.starts_with("loot_tier") => {
918            let tier = lower
919                .split('_')
920                .next_back()
921                .and_then(|v| v.parse::<u8>().ok())
922                .unwrap_or(1);
923            MarkerType::LootTier { tier }
924        }
925        _ if lower.starts_with("encounter") => {
926            let difficulty = lower
927                .split('_')
928                .next_back()
929                .and_then(|v| v.parse::<u8>().ok())
930                .unwrap_or(1);
931            MarkerType::EncounterZone { difficulty }
932        }
933        _ => MarkerType::Custom(trimmed.to_string()),
934    }
935}