1use 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 {
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
52pub 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
75pub 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
101pub 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
353pub 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
532pub 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}