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 {
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
51pub 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
63pub 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
88pub 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
333pub 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
511pub 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}