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