1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_MAP_WIDTH: usize = 4096;
11const MAX_MAP_HEIGHT: usize = 4096;
12const MAX_LAYERS: usize = 32;
13const BLOB_TILE_COUNT: usize = 47;
14const A_STAR_MAX_ITERATIONS: usize = 1_000_000;
15const FOG_CHUNK_SIZE: usize = 8;
16const MINIMAP_MAX_SIZE: usize = 512;
17const MAX_UNDO_HISTORY: usize = 256;
18const PATROL_MAX_WAYPOINTS: usize = 64;
19const ENCOUNTER_MAX_SPAWNS: usize = 32;
20const BSP_MIN_ROOM_SIZE: usize = 4;
21const BSP_MAX_DEPTH: usize = 8;
22const AO_RADIUS: usize = 2;
23const LIGHT_FALLOFF_TABLE_SIZE: usize = 256;
24const JUMP_POINT_SEARCH_MAX: usize = 512;
25const CORRIDOR_WIDTH: usize = 2;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum TileSurface {
33 Solid,
34 Floor,
35 Water,
36 Lava,
37 Ice,
38 Mud,
39 Sand,
40 Void,
41 Bridge,
42 Stairs,
43}
44
45#[derive(Clone, Debug)]
46pub struct TileDefinition {
47 pub id: u32,
48 pub name: String,
49 pub surface: TileSurface,
50 pub walkable: bool,
51 pub swimmable: bool,
52 pub flyable: bool,
53 pub solid_collision: bool,
54 pub damage_per_second: f32,
55 pub friction: f32,
56 pub movement_cost: f32,
57 pub texture_id: u32,
58 pub overlay_texture_id: Option<u32>,
59 pub light_blocking: bool,
60 pub light_emission: f32,
61 pub auto_tile_group: Option<u32>,
62 pub height_offset: f32,
63 pub destructible: bool,
64 pub health: f32,
65 pub sound_id: u32,
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq)]
69pub struct Tile {
70 pub tile_id: u32,
71 pub variant: u8,
72 pub auto_tile_mask: u8,
73 pub flags: u16,
74 pub ambient_occlusion: u8,
75 pub light_level: u8,
76 pub entity_id: u32,
77 pub height: i16,
78}
79
80impl Tile {
81 pub fn new(tile_id: u32) -> Self { Self { tile_id, ..Default::default() } }
82 pub fn with_id(tile_id: u32) -> Self { Self { tile_id, ..Default::default() } }
83 pub fn empty() -> Self { Self::default() }
84}
85
86pub struct TileDatabase {
87 pub definitions: HashMap<u32, TileDefinition>,
88}
89
90#[derive(Clone, Copy, Debug, PartialEq)]
91pub enum LayerType {
92 Background,
93 Collision,
94 Decoration,
95 Entity,
96 FogOfWar,
97 Lighting,
98 Navigation,
99 Overlay,
100 Ground,
101 Tile,
102}
103
104#[derive(Clone, Debug)]
105pub struct MapLayer {
106 pub name: String,
107 pub layer_type: LayerType,
108 pub tiles: Vec<Tile>,
109 pub width: usize,
110 pub height: usize,
111 pub visible: bool,
112 pub locked: bool,
113 pub opacity: f32,
114 pub z_offset: f32,
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub enum MapProjection {
119 TopDown,
120 Isometric,
121 IsometricStaggered,
122 Hexagonal,
123}
124
125#[derive(Clone, Debug)]
126pub struct TileMap {
127 pub width: usize,
128 pub height: usize,
129 pub tile_width: u32,
130 pub tile_height: u32,
131 pub layers: Vec<MapLayer>,
132 pub projection: MapProjection,
133 pub background_color: Vec4,
134 pub ambient_light: f32,
135}
136
137pub const BLOB_MAPPING: [u8; 16] = [
138 0, 1, 2, 3, 4, 5, 6, 7,
139 8, 9, 10, 11, 12, 13, 14, 15,
140];
141
142pub fn blob_8bit_to_47(mask: u8) -> u8 {
144 const BLOB47: [u8; 256] = compute_blob47_table();
147 BLOB47[mask as usize]
148}
149
150const fn compute_blob47_table() -> [u8; 256] {
151 let mut table = [0u8; 256];
152 let mut i = 0usize;
153 while i < 256 {
154 let n = (i >> 1) & 1;
155 let e = (i >> 4) & 1;
156 let s = (i >> 6) & 1;
157 let w = (i >> 3) & 1;
158 let nw = (i >> 0) & 1;
159 let ne = (i >> 2) & 1;
160 let sw = (i >> 5) & 1;
161 let se = (i >> 7) & 1;
162 let real_nw = if n == 1 && w == 1 { nw } else { 0 };
164 let real_ne = if n == 1 && e == 1 { ne } else { 0 };
165 let real_sw = if s == 1 && w == 1 { sw } else { 0 };
166 let real_se = if s == 1 && e == 1 { se } else { 0 };
167 let canonical = n | (e << 1) | (s << 2) | (w << 3)
168 | (real_nw << 4) | (real_ne << 5) | (real_sw << 6) | (real_se << 7);
169 table[i] = canonical as u8;
170 i += 1;
171 }
172 table
173}
174
175pub struct AutoTiler;
176
177#[derive(Clone, Debug, PartialEq)]
178pub enum RoomShape {
179 Rectangular,
180 LShaped,
181 Circular,
182 IrregularPolygon,
183 Cross,
184}
185
186#[derive(Clone, Copy, Debug, PartialEq)]
187pub enum RoomType {
188 Normal,
189 Boss,
190 Treasure,
191 Spawn,
192 Exit,
193 Shop,
194 Secret,
195 Corridor,
196 Safe,
197}
198
199#[derive(Clone, Debug)]
200pub struct Doorway {
201 pub x: usize,
202 pub y: usize,
203 pub direction: CardinalDir,
204 pub width: usize,
205 pub is_locked: bool,
206 pub key_id: Option<u32>,
207 pub connected_room_id: Option<u32>,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub enum CardinalDir {
212 North,
213 South,
214 East,
215 West,
216}
217
218#[derive(Clone, Debug)]
219pub struct Room {
220 pub id: u32,
221 pub x: usize,
222 pub y: usize,
223 pub width: usize,
224 pub height: usize,
225 pub shape: RoomShape,
226 pub room_type: RoomType,
227 pub doorways: Vec<Doorway>,
228 pub connected_rooms: Vec<u32>,
229 pub polygon_points: Vec<(i32, i32)>,
230 pub metadata: HashMap<String, String>,
231}
232
233pub struct RoomGraph {
234 pub nodes: HashMap<u32, Room>,
235 pub edges: HashMap<u32, Vec<u32>>,
236}
237
238pub struct BspNode {
239 pub rect: (usize, usize, usize, usize), pub left: Option<Box<BspNode>>,
241 pub right: Option<Box<BspNode>>,
242 pub room: Option<Room>,
243}
244
245pub struct BspDungeonGenerator {
246 pub rng: MapRng,
247 pub room_padding: usize,
248 pub min_room_size: usize,
249 pub winding_factor: f32,
250}
251
252pub struct MapRng {
253 pub state: u64,
254}
255impl MapRng {
256 pub fn new(seed: u64) -> Self { Self { state: seed ^ 0x853c49e6748fea9b } }
257 pub fn next_u64(&mut self) -> u64 { self.state ^= self.state << 13; self.state ^= self.state >> 7; self.state ^= self.state << 17; self.state }
258 pub fn next_f32(&mut self) -> f32 { (self.next_u64() >> 33) as f32 / 2147483648.0 }
259 pub fn next_range(&mut self, min: u32, max: u32) -> u32 { if min >= max { return min; } min + (self.next_u64() % (max - min) as u64) as u32 }
260}
261
262#[derive(Clone, Debug)]
263pub struct SpawnTableEntry {
264 pub entity_type_id: u32,
265 pub weight: f32,
266 pub min_level: u32,
267 pub max_level: u32,
268 pub group_size_min: u32,
269 pub group_size_max: u32,
270}
271
272#[derive(Clone, Debug)]
273pub struct SpawnTable {
274 pub entries: Vec<SpawnTableEntry>,
275 pub total_weight: f32,
276}
277
278pub struct EncounterZone {
279 pub id: u32,
280 pub polygon: Vec<(i32, i32)>,
281 pub spawn_table: SpawnTable,
282 pub min_level: u32,
283 pub max_level: u32,
284 pub respawn_time_secs: f32,
285 pub trigger_conditions: Vec<TriggerCondition>,
286 pub is_active: bool,
287 pub encounter_name: String,
288}
289
290pub enum TriggerCondition {
291 OnEnter,
292 OnPlayerNearby { radius: f32 },
293 OnQuestFlag { flag_id: u32, value: bool },
294 OnTime { time_of_day_min: f32, time_of_day_max: f32 },
295 OnEnemyCount { count: u32, comparison: Comparison },
296}
297
298#[derive(Clone, Copy, Debug, PartialEq)]
299pub enum Comparison {
300 LessThan,
301 LessEqual,
302 Equal,
303 GreaterEqual,
304 GreaterThan,
305}
306
307#[derive(Clone, Copy, Debug, PartialEq)]
312pub enum FogState {
313 Hidden,
314 Explored,
315 Visible,
316}
317
318#[derive(Clone, Debug)]
319pub struct MinimapData {
320 pub width: usize,
321 pub height: usize,
322 pub pixels: Vec<[u8; 4]>,
323 pub fog_states: Vec<FogState>,
324 pub scale_x: f32,
325 pub scale_y: f32,
326}
327
328pub struct MinimapGenerator;
329
330#[derive(Clone, Debug, PartialEq)]
331pub struct PathNode {
332 pub x: u32,
333 pub y: u32,
334 pub g: f32,
335 pub f: f32,
336}
337
338impl Eq for PathNode {}
339
340impl std::cmp::PartialOrd for PathNode {
341 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
342 other.f.partial_cmp(&self.f)
343 }
344}
345
346pub struct AStarPathfinder;
347
348pub fn octile_heuristic(ax: usize, ay: usize, bx: usize, by: usize) -> f32 {
349 let dx = (ax as i32 - bx as i32).unsigned_abs() as f32;
350 let dy = (ay as i32 - by as i32).unsigned_abs() as f32;
351 let (d_min, d_max) = if dx < dy { (dx, dy) } else { (dy, dx) };
352 d_max + (1.414 - 1.0) * d_min
353}
354
355pub struct JumpPointSearch;
360
361pub struct NavRegion {
362 pub id: u32,
363 pub min_x: usize,
364 pub min_y: usize,
365 pub max_x: usize,
366 pub max_y: usize,
367 pub connections: Vec<u32>,
368}
369
370pub struct NavMeshGenerator;
371
372pub struct TileLighter;
373
374pub struct PointLight {
375 pub x: usize,
376 pub y: usize,
377 pub intensity: f32,
378 pub radius: usize,
379 pub color: Vec3,
380}
381
382#[derive(Clone, Debug)]
387pub struct EventZone {
388 pub id: u32,
389 pub rect: (usize, usize, usize, usize),
390 pub trigger: EventTrigger,
391 pub script_id: u32,
392 pub one_shot: bool,
393 pub triggered: bool,
394 pub cooldown_secs: f32,
395 pub last_triggered: f32,
396}
397
398#[derive(Clone, Copy, Debug, PartialEq)]
399pub enum EventTrigger {
400 OnEnter,
401 OnExit,
402 OnStay { duration_secs: f32 },
403 OnInteract,
404 OnCombatEnd,
405}
406
407#[derive(Clone, Debug)]
408pub struct ScriptedEvent {
409 pub time: f32,
410 pub action: ScriptedAction,
411}
412
413#[derive(Clone, Debug)]
414pub enum ScriptedAction {
415 SpawnEntity { entity_type: u32, x: usize, y: usize },
416 PlayAudio { sound_id: u32 },
417 ShowSubtitle { text: String, duration: f32 },
418 SetTile { layer: String, x: usize, y: usize, tile_id: u32 },
419 TriggerAnimation { entity_id: u32, anim_id: u32 },
420 ShowCutscene { cutscene_id: u32 },
421 GiveItem { entity_id: u32, item_id: u32, count: u32 },
422 FadeInOut { duration: f32, color: Vec4 },
423}
424
425#[derive(Clone, Debug)]
426pub struct ScriptedSequence {
427 pub id: u32,
428 pub name: String,
429 pub events: Vec<ScriptedEvent>,
430 pub is_looping: bool,
431 pub current_time: f32,
432 pub is_running: bool,
433}
434
435pub struct PatrolPath {
436 pub id: u32,
437 pub waypoints: Vec<(usize, usize)>,
438 pub current_idx: usize,
439 pub loop_type: PatrolLoopType,
440 pub direction: i32,
441}
442
443#[derive(Clone, Copy, Debug, PartialEq)]
444pub enum PatrolLoopType {
445 Loop,
446 PingPong,
447 OneShot,
448}
449
450#[derive(Clone, Debug, PartialEq)]
451pub enum BrushTool {
452 Pencil,
453 Fill,
454 Line,
455 Rectangle,
456 Circle,
457 FloodFill,
458 Erase,
459 Select,
460 EyeDropper,
461}
462
463#[derive(Clone, Debug)]
464pub struct BrushState {
465 pub tool: BrushTool,
466 pub tile_id: u32,
467 pub size: usize,
468 pub layer_idx: usize,
469 pub start_x: Option<usize>,
470 pub start_y: Option<usize>,
471 pub is_painting: bool,
472}
473
474pub struct BrushEngine;
475
476pub fn bresenham_line(x0: i32, y0: i32, x1: i32, y1: i32, out: &mut Vec<(i32, i32)>) {
477 let mut x = x0;
478 let mut y = y0;
479 let dx = (x1 - x0).abs();
480 let dy = (y1 - y0).abs();
481 let sx = if x0 < x1 { 1 } else { -1 };
482 let sy = if y0 < y1 { 1 } else { -1 };
483 let mut err = dx - dy;
484 loop {
485 out.push((x, y));
486 if x == x1 && y == y1 { break; }
487 let e2 = 2 * err;
488 if e2 > -dy { err -= dy; x += sx; }
489 if e2 < dx { err += dx; y += sy; }
490 }
491}
492
493#[derive(Clone, Debug)]
498pub struct TileRegion {
499 pub tiles: Vec<Vec<Tile>>,
500 pub width: usize,
501 pub height: usize,
502 pub layer_idx: usize,
503}
504
505pub struct Clipboard {
506 pub data: Option<TileRegion>,
507}
508
509pub enum MapEditAction {
510 PaintTile { layer_idx: usize, x: usize, y: usize, old_tile: Tile, new_tile: Tile },
511 PaintRegion { layer_idx: usize, x: usize, y: usize, old_tiles: Vec<Vec<Tile>>, new_tiles: Vec<Vec<Tile>> },
512 AddLayer { layer_idx: usize, layer: MapLayer },
513 RemoveLayer { layer_idx: usize, layer: MapLayer },
514 ResizeMap { old_w: usize, old_h: usize, new_w: usize, new_h: usize },
515 AddRoom { room: Room },
516 RemoveRoom { room_id: u32, room: Room },
517}
518
519pub struct MapUndoStack {
520 pub history: VecDeque<Vec<MapEditAction>>,
521 pub redo_stack: VecDeque<Vec<MapEditAction>>,
522 pub max_size: usize,
523}
524
525pub struct RoomFiller;
526
527pub enum MapEditorMode {
528 Idle,
529 PaintingTiles,
530 SelectingRegion,
531 PlacingRoom,
532 PlacingEvent,
533 PlacingPatrol,
534 PlacingEncounter,
535 GeneratingDungeon,
536 PaintingLighting,
537}
538
539#[derive(Clone, Debug)]
540pub struct SelectionRect {
541 pub x: usize,
542 pub y: usize,
543 pub width: usize,
544 pub height: usize,
545 pub is_active: bool,
546}
547
548pub struct MapEditor {
549 pub map: TileMap,
550 pub layers: Vec<MapLayer>,
551 pub db: TileDatabase,
552 pub undo_stack: MapUndoStack,
553 pub brush: BrushState,
554 pub clipboard: Clipboard,
555 pub selection: SelectionRect,
556 pub rooms: RoomGraph,
557 pub encounter_zones: Vec<EncounterZone>,
558 pub patrol_paths: Vec<PatrolPath>,
559 pub event_zones: Vec<EventZone>,
560 pub scripted_sequences: Vec<ScriptedSequence>,
561 pub lights: Vec<PointLight>,
562 pub minimap: MinimapData,
563 pub mode: MapEditorMode,
564 pub current_layer: usize,
565 pub rng: MapRng,
566 pub pending_actions: Vec<MapEditAction>,
567 pub grid_visible: bool,
568 pub show_collision: bool,
569 pub show_entities: bool,
570 pub show_lighting: bool,
571 pub show_patrol_paths: bool,
572 pub show_encounter_zones: bool,
573 pub show_minimap: bool,
574}
575
576pub fn serialize_map(map: &TileMap) -> Vec<u8> {
577 let mut bytes = Vec::new();
578 bytes.extend_from_slice(b"TILEMAP1");
579 let w_bytes = (map.width as u32).to_le_bytes();
580 let h_bytes = (map.height as u32).to_le_bytes();
581 let tw_bytes = map.tile_width.to_le_bytes();
582 let th_bytes = map.tile_height.to_le_bytes();
583 bytes.extend_from_slice(&w_bytes);
584 bytes.extend_from_slice(&h_bytes);
585 bytes.extend_from_slice(&tw_bytes);
586 bytes.extend_from_slice(&th_bytes);
587 bytes.extend_from_slice(&(map.layers.len() as u32).to_le_bytes());
588 for layer in &map.layers {
589 let name_bytes = layer.name.as_bytes();
590 bytes.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
591 bytes.extend_from_slice(name_bytes);
592 bytes.push(layer.layer_type as u8);
593 bytes.push(if layer.visible { 1 } else { 0 });
594 bytes.push(if layer.locked { 1 } else { 0 });
595 bytes.extend_from_slice(&layer.opacity.to_le_bytes());
596 let tile_count = layer.tiles.len();
597 bytes.extend_from_slice(&(tile_count as u32).to_le_bytes());
598 for tile in &layer.tiles {
599 bytes.extend_from_slice(&tile.tile_id.to_le_bytes());
600 bytes.push(tile.variant);
601 bytes.push(tile.auto_tile_mask);
602 bytes.push(tile.ambient_occlusion);
603 bytes.push(tile.light_level);
604 bytes.extend_from_slice(&tile.entity_id.to_le_bytes());
605 }
606 }
607 bytes
608}
609
610pub fn deserialize_map(bytes: &[u8]) -> Option<TileMap> {
611 if bytes.len() < 8 || &bytes[..8] != b"TILEMAP1" { return None; }
612 let mut pos = 8usize;
613 let read_u32 = |b: &[u8], p: &mut usize| -> Option<u32> {
614 if *p + 4 > b.len() { return None; }
615 let v = u32::from_le_bytes(b[*p..*p+4].try_into().ok()?);
616 *p += 4;
617 Some(v)
618 };
619 let read_u8 = |b: &[u8], p: &mut usize| -> Option<u8> {
620 if *p >= b.len() { return None; }
621 let v = b[*p];
622 *p += 1;
623 Some(v)
624 };
625 let read_f32 = |b: &[u8], p: &mut usize| -> Option<f32> {
626 if *p + 4 > b.len() { return None; }
627 let v = f32::from_le_bytes(b[*p..*p+4].try_into().ok()?);
628 *p += 4;
629 Some(v)
630 };
631 let w = read_u32(bytes, &mut pos)? as usize;
632 let h = read_u32(bytes, &mut pos)? as usize;
633 let tw = read_u32(bytes, &mut pos)?;
634 let th = read_u32(bytes, &mut pos)?;
635 let mut map = TileMap::new(w, h, tw, th);
636 map.layers.clear();
637 let layer_count = read_u32(bytes, &mut pos)? as usize;
638 for _ in 0..layer_count {
639 let name_len = read_u32(bytes, &mut pos)? as usize;
640 if pos + name_len > bytes.len() { return None; }
641 let name = std::str::from_utf8(&bytes[pos..pos+name_len]).ok()?.to_string();
642 pos += name_len;
643 let lt_byte = read_u8(bytes, &mut pos)?;
644 let layer_type = match lt_byte {
645 0 => LayerType::Background,
646 1 => LayerType::Collision,
647 2 => LayerType::Decoration,
648 3 => LayerType::Entity,
649 4 => LayerType::FogOfWar,
650 5 => LayerType::Lighting,
651 6 => LayerType::Navigation,
652 _ => LayerType::Overlay,
653 };
654 let visible = read_u8(bytes, &mut pos)? != 0;
655 let locked = read_u8(bytes, &mut pos)? != 0;
656 let opacity = read_f32(bytes, &mut pos)?;
657 let tile_count = read_u32(bytes, &mut pos)? as usize;
658 let mut layer = MapLayer::new(&name, layer_type, w, h);
659 layer.visible = visible;
660 layer.locked = locked;
661 layer.opacity = opacity;
662 for i in 0..tile_count.min(layer.tiles.len()) {
663 let tile_id = read_u32(bytes, &mut pos)?;
664 let variant = read_u8(bytes, &mut pos)?;
665 let auto_tile_mask = read_u8(bytes, &mut pos)?;
666 let ao = read_u8(bytes, &mut pos)?;
667 let light = read_u8(bytes, &mut pos)?;
668 let entity_id = read_u32(bytes, &mut pos)?;
669 layer.tiles[i] = Tile { tile_id, variant, auto_tile_mask, flags: 0, ambient_occlusion: ao, light_level: light, entity_id, height: 0 };
670 }
671 map.layers.push(layer);
672 }
673 Some(map)
674}
675
676#[derive(Clone, Debug)]
681pub struct MapStats {
682 pub total_tiles: usize,
683 pub walkable_tiles: usize,
684 pub solid_tiles: usize,
685 pub water_tiles: usize,
686 pub room_count: usize,
687 pub connected_rooms: bool,
688 pub encounter_zones: usize,
689 pub nav_regions: usize,
690 pub layer_count: usize,
691 pub total_lights: usize,
692 pub patrol_paths: usize,
693 pub scripted_sequences: usize,
694}
695
696pub fn compute_map_stats(editor: &MapEditor) -> MapStats {
697 let nav_regions = editor.build_nav_regions();
698 MapStats {
699 total_tiles: editor.map.width * editor.map.height,
700 walkable_tiles: editor.total_walkable_tiles(),
701 solid_tiles: editor.total_solid_tiles(),
702 water_tiles: {
703 let mut cnt = 0;
704 for layer in &editor.map.layers {
705 for t in &layer.tiles {
706 if editor.db.get(t.tile_id).map(|d| d.surface == TileSurface::Water).unwrap_or(false) {
707 cnt += 1;
708 }
709 }
710 }
711 cnt
712 },
713 room_count: editor.rooms.nodes.len(),
714 connected_rooms: editor.rooms.is_connected(),
715 encounter_zones: editor.encounter_zones.len(),
716 nav_regions: nav_regions.len(),
717 layer_count: editor.map.layers.len(),
718 total_lights: editor.lights.len(),
719 patrol_paths: editor.patrol_paths.len(),
720 scripted_sequences: editor.scripted_sequences.len(),
721 }
722}
723
724#[derive(Clone, Debug)]
729pub struct Tileset {
730 pub id: u32,
731 pub name: String,
732 pub texture_id: u32,
733 pub tile_width: u32,
734 pub tile_height: u32,
735 pub tiles_per_row: u32,
736 pub total_tiles: u32,
737 pub tile_definitions: Vec<TileDefinition>,
738}
739
740pub struct TilesetManager {
741 pub tilesets: HashMap<u32, Tileset>,
742}
743
744pub struct CaveGenerator {
745 pub rng: MapRng,
746 pub initial_fill_prob: f32,
747 pub smoothing_iterations: usize,
748 pub birth_threshold: usize,
749 pub death_threshold: usize,
750}
751
752pub fn generate_noise_map(width: usize, height: usize, freq: f32, seed: u64) -> Vec<f32> {
753 let mut rng = MapRng::new(seed);
754 let mut noise = vec![0.0f32; width * height];
755 for y in 0..height {
756 for x in 0..width {
757 let nx = x as f32 * freq;
758 let ny = y as f32 * freq;
759 noise[y * width + x] = simple_value_noise_2d(nx, ny);
760 }
761 }
762 noise
763}
764
765pub fn simple_value_noise_2d(x: f32, y: f32) -> f32 {
766 let ix = x as i32;
767 let iy = y as i32;
768 let fx = x - ix as f32;
769 let fy = y - iy as f32;
770 let ux = fx * fx * (3.0 - 2.0 * fx);
771 let uy = fy * fy * (3.0 - 2.0 * fy);
772 let n00 = hash_2d(ix, iy);
773 let n10 = hash_2d(ix + 1, iy);
774 let n01 = hash_2d(ix, iy + 1);
775 let n11 = hash_2d(ix + 1, iy + 1);
776 let nx0 = n00 + (n10 - n00) * ux;
777 let nx1 = n01 + (n11 - n01) * ux;
778 nx0 + (nx1 - nx0) * uy
779}
780
781pub fn hash_2d(x: i32, y: i32) -> f32 {
782 let n = (x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337))) as u32;
783 let n = n ^ (n >> 16);
784 let n = n.wrapping_mul(0x45d9f3b);
785 let n = n ^ (n >> 16);
786 (n & 0xffffff) as f32 / 0xffffff as f32
787}
788
789pub fn apply_heightmap_to_tiles(map: &mut TileMap, heightmap: &[f32], water_threshold: f32, wall_threshold: f32, water_tile: u32, floor_tile: u32, wall_tile: u32) {
790 let bg_idx = map.layers.iter().position(|l| l.layer_type == LayerType::Background).unwrap_or(0);
791 let col_idx = map.layers.iter().position(|l| l.layer_type == LayerType::Collision).unwrap_or(1);
792 for y in 0..map.height {
793 for x in 0..map.width {
794 let idx = y * map.width + x;
795 if idx >= heightmap.len() { continue; }
796 let h = heightmap[idx];
797 if h < water_threshold {
798 map.layers[bg_idx].set(x, y, Tile::with_id(water_tile));
799 } else if h > wall_threshold {
800 map.layers[col_idx].set(x, y, Tile::with_id(wall_tile));
801 } else {
802 map.layers[bg_idx].set(x, y, Tile::with_id(floor_tile));
803 map.layers[col_idx].set(x, y, Tile::with_id(0));
804 }
805 }
806 }
807}
808
809pub fn run_all_map_tests() -> bool {
814 {
816 let map = TileMap::new(32, 32, 16, 16);
817 assert_eq!(map.width, 32);
818 assert_eq!(map.height, 32);
819 assert!(!map.layers.is_empty());
820 }
821 {
823 let mut editor = MapEditor::new(32, 32, 16, 16);
824 editor.brush.tile_id = 1;
825 editor.start_stroke(5, 5);
826 editor.paint_tile(5, 5);
827 editor.end_stroke();
828 let t = editor.map.layers[0].get(5, 5).copied().unwrap_or(Tile::empty());
829 assert_eq!(t.tile_id, 1);
830 editor.undo();
831 let t2 = editor.map.layers[0].get(5, 5).copied().unwrap_or(Tile::empty());
832 assert_eq!(t2.tile_id, 0);
833 }
834 {
836 let mut map = TileMap::new(16, 16, 16, 16);
837 for y in 0..16usize { for x in 0..16usize { map.layers[0].set(x, y, Tile::with_id(2)); } }
838 BrushEngine::flood_fill(&mut map, 0, 8, 8, 5);
839 let t = map.layers[0].get(8, 8).copied().unwrap_or(Tile::empty());
840 assert_eq!(t.tile_id, 5);
841 }
842 {
844 let mut map = TileMap::new(10, 10, 16, 16);
845 let db = TileDatabase::new();
846 let path = AStarPathfinder::find_path(&map, &db, 0, 0, 9, 9, true, 0);
847 assert!(path.is_some());
848 }
849 {
851 let mut gen = BspDungeonGenerator::new(42);
852 let (rooms, corridors) = gen.generate(64, 64, 4);
853 assert!(!rooms.is_empty());
854 }
855 {
857 let map = TileMap::new(32, 32, 16, 16);
858 let db = TileDatabase::new();
859 let mm = MinimapGenerator::generate(&map, &db, 16, 16);
860 assert_eq!(mm.width, 16);
861 assert_eq!(mm.height, 16);
862 }
863 {
865 let mut editor = MapEditor::new(8, 8, 16, 16);
866 editor.map.layers[0].set(3, 3, Tile::with_id(7));
867 let bytes = editor.serialize();
868 let mut editor2 = MapEditor::new(8, 8, 16, 16);
869 let ok = editor2.deserialize_into(&bytes);
870 assert!(ok);
871 assert_eq!(editor2.map.layers[0].get(3, 3).map(|t| t.tile_id), Some(7));
872 }
873 {
875 let mut path = PatrolPath::new(0);
876 path.add_waypoint((0, 0));
877 path.add_waypoint((5, 0));
878 path.add_waypoint((5, 5));
879 path.loop_type = PatrolLoopType::Loop;
880 assert_eq!(path.current_target(), Some((0, 0)));
881 path.advance();
882 assert_eq!(path.current_target(), Some((5, 0)));
883 path.advance();
884 path.advance();
885 assert_eq!(path.current_target(), Some((0, 0))); }
887 {
889 let mut zone = EncounterZone {
890 id: 0,
891 polygon: vec![(0, 0), (10, 0), (10, 10), (0, 10)],
892 spawn_table: SpawnTable::new(),
893 min_level: 1,
894 max_level: 5,
895 respawn_time_secs: 60.0,
896 trigger_conditions: Vec::new(),
897 is_active: true,
898 encounter_name: "test".to_string(),
899 };
900 assert!(zone.contains_point(5, 5));
901 assert!(!zone.contains_point(15, 15));
902 }
903 true
904}
905
906pub fn find_tiles_of_type(map: &TileMap, db: &TileDatabase, surface: TileSurface) -> Vec<(usize, usize)> {
911 let mut results = Vec::new();
912 for layer in &map.layers {
913 for y in 0..map.height {
914 for x in 0..map.width {
915 if let Some(t) = layer.get(x, y) {
916 if let Some(def) = db.get(t.tile_id) {
917 if def.surface == surface {
918 results.push((x, y));
919 }
920 }
921 }
922 }
923 }
924 }
925 results
926}
927
928pub fn count_surface(map: &TileMap, db: &TileDatabase, surface: TileSurface) -> usize {
929 find_tiles_of_type(map, db, surface).len()
930}
931
932pub fn find_room_at_point(rooms: &RoomGraph, x: i32, y: i32) -> Option<&Room> {
933 rooms.nodes.values().find(|r| r.contains_point(x, y))
934}
935
936pub fn get_rooms_of_type(rooms: &RoomGraph, room_type: RoomType) -> Vec<&Room> {
937 rooms.nodes.values().filter(|r| r.room_type == room_type).collect()
938}
939
940pub fn map_bounding_box(map: &TileMap) -> (Vec2, Vec2) {
941 let min = Vec2::ZERO;
942 let max = Vec2::new(
943 map.width as f32 * map.tile_width as f32,
944 map.height as f32 * map.tile_height as f32,
945 );
946 (min, max)
947}
948
949pub fn spawn_points(map: &TileMap, db: &TileDatabase) -> Vec<(usize, usize)> {
950 let mut pts = Vec::new();
951 for layer in &map.layers {
952 if layer.layer_type != LayerType::Entity { continue; }
953 for y in 0..map.height {
954 for x in 0..map.width {
955 if let Some(t) = layer.get(x, y) {
956 if t.entity_id != 0 { pts.push((x, y)); }
957 }
958 }
959 }
960 }
961 pts
962}
963
964pub fn weighted_random_room_type(rng: &mut MapRng) -> RoomType {
965 let roll = rng.next_range(0, 100);
966 match roll {
967 0..=5 => RoomType::Boss,
968 6..=12 => RoomType::Treasure,
969 13..=15 => RoomType::Shop,
970 16..=17 => RoomType::Secret,
971 18..=20 => RoomType::Safe,
972 21..=23 => RoomType::Spawn,
973 24..=25 => RoomType::Exit,
974 _ => RoomType::Normal,
975 }
976}
977
978pub fn assign_room_types(rooms: &mut RoomGraph, rng: &mut MapRng) {
979 let ids: Vec<u32> = rooms.nodes.keys().cloned().collect();
980 for id in ids {
981 if let Some(room) = rooms.nodes.get_mut(&id) {
982 room.room_type = weighted_random_room_type(rng);
983 }
984 }
985}
986
987pub fn build_default_spawn_table() -> SpawnTable {
988 let mut table = SpawnTable::new();
989 table.add(SpawnTableEntry { entity_type_id: 1, weight: 30.0, min_level: 1, max_level: 5, group_size_min: 1, group_size_max: 3 });
990 table.add(SpawnTableEntry { entity_type_id: 2, weight: 20.0, min_level: 2, max_level: 8, group_size_min: 1, group_size_max: 2 });
991 table.add(SpawnTableEntry { entity_type_id: 3, weight: 10.0, min_level: 4, max_level: 10, group_size_min: 1, group_size_max: 1 });
992 table.add(SpawnTableEntry { entity_type_id: 4, weight: 5.0, min_level: 6, max_level: 15, group_size_min: 1, group_size_max: 1 });
993 table
994}
995
996pub fn build_sample_dungeon(width: usize, height: usize, seed: u64) -> MapEditor {
1001 let mut editor = MapEditor::new(width, height, 16, 16);
1002 editor.generate_dungeon(BSP_MAX_DEPTH, 2, 1);
1003 assign_room_types(&mut editor.rooms, &mut editor.rng);
1004 let room_ids: Vec<u32> = editor.rooms.nodes.keys().cloned().collect();
1006 for id in &room_ids {
1007 if let Some(room) = editor.rooms.nodes.get(id) {
1008 let (cx, cy) = room.center();
1009 editor.lights.push(PointLight {
1010 x: cx, y: cy,
1011 intensity: 0.8,
1012 radius: room.width.max(room.height) / 2 + 2,
1013 color: Vec3::new(1.0, 0.9, 0.7),
1014 });
1015 }
1016 }
1017 editor.bake_lights();
1018 let spawn_table = build_default_spawn_table();
1020 let mut ez_id = 0u32;
1021 for id in &room_ids {
1022 if let Some(room) = editor.rooms.nodes.get(id) {
1023 if room.room_type == RoomType::Normal {
1024 let poly = vec![
1025 (room.x as i32, room.y as i32),
1026 ((room.x + room.width) as i32, room.y as i32),
1027 ((room.x + room.width) as i32, (room.y + room.height) as i32),
1028 (room.x as i32, (room.y + room.height) as i32),
1029 ];
1030 let mut table = SpawnTable::new();
1031 for e in &spawn_table.entries {
1032 table.add(e.clone());
1033 }
1034 editor.encounter_zones.push(EncounterZone {
1035 id: ez_id,
1036 polygon: poly,
1037 spawn_table: table,
1038 min_level: 1,
1039 max_level: 10,
1040 respawn_time_secs: 120.0,
1041 trigger_conditions: vec![TriggerCondition::OnEnter],
1042 is_active: true,
1043 encounter_name: format!("room_{}_encounter", id),
1044 });
1045 ez_id += 1;
1046 }
1047 }
1048 }
1049 editor.rebuild_minimap();
1050 editor
1051}
1052
1053#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1058pub enum RegionType { Town, Dungeon, Wilderness, Special, Tutorial, Boss, Hub }
1059
1060#[derive(Debug, Clone)]
1061pub struct MapRegion {
1062 pub id: u32,
1063 pub name: String,
1064 pub region_type: RegionType,
1065 pub bounds: (i32, i32, i32, i32), pub connected_regions: Vec<u32>,
1067 pub level_range: (u32, u32),
1068 pub background_music: Option<String>,
1069 pub ambient_sounds: Vec<String>,
1070 pub weather: String,
1071 pub discovered: bool,
1072 pub completion: f32,
1073}
1074
1075pub struct WorldMap {
1076 pub regions: Vec<MapRegion>,
1077 pub width: u32,
1078 pub height: u32,
1079 pub world_name: String,
1080 pub starting_region: u32,
1081}
1082
1083#[derive(Debug, Clone, PartialEq)]
1084pub enum ItemRarity { Common, Uncommon, Rare, Epic, Legendary, Artifact, VeryRare }
1085
1086#[derive(Debug, Clone)]
1087pub struct TreasureItem {
1088 pub id: u32,
1089 pub name: String,
1090 pub rarity: ItemRarity,
1091 pub item_type: String,
1092 pub base_value: u32,
1093 pub weight: f32,
1094 pub level_req: u32,
1095}
1096
1097impl TreasureItem {
1098 pub fn new(id: u32, name: &str, rarity: ItemRarity, item_type: &str, base_value: u32) -> Self { Self { id, name: name.to_string(), rarity, item_type: item_type.to_string(), base_value, weight: 1.0, level_req: 1 } }
1099}
1100
1101pub struct TreasureChest {
1102 pub id: u32,
1103 pub position: (i32, i32),
1104 pub items: Vec<TreasureItem>,
1105 pub gold_amount: u32,
1106 pub chest_type: String,
1107 pub is_locked: bool,
1108 pub key_id: Option<u32>,
1109 pub is_trapped: bool,
1110 pub trap_damage: u32,
1111 pub opened: bool,
1112}
1113
1114pub enum NpcRole { Merchant, QuestGiver, Guard, Enemy, Ally, Neutral, Boss }
1115
1116#[derive(Debug, Clone)]
1117pub struct NpcDialogueNode {
1118 pub id: u32,
1119 pub text: String,
1120 pub speaker: String,
1121 pub responses: Vec<(String, u32)>,
1122 pub conditions: Vec<String>,
1123 pub effects: Vec<String>,
1124}
1125
1126pub struct NpcDefinition {
1127 pub id: u32,
1128 pub name: String,
1129 pub role: NpcRole,
1130 pub position: (i32, i32),
1131 pub level: u32,
1132 pub faction: String,
1133 pub dialogue_root: u32,
1134 pub dialogue_nodes: HashMap<u32, NpcDialogueNode>,
1135 pub schedule: Vec<(u32, (i32, i32))>,
1136 pub inventory: Vec<TreasureItem>,
1137 pub aggression_range: f32,
1138 pub is_essential: bool,
1139}
1140
1141#[derive(Debug, Clone, PartialEq, Eq)]
1142pub enum QuestStatus { NotStarted, Active, Completed, Failed, Hidden, Abandoned }
1143
1144#[derive(Debug, Clone)]
1145pub enum QuestObjectiveKind {
1146 KillEnemy { enemy_type: String, count: u32, progress: u32 },
1147 CollectItem { item_id: u32, count: u32, progress: u32 },
1148 ReachLocation { region_id: u32, reached: bool },
1149 TalkToNpc { npc_id: u32, talked: bool },
1150 ActivateObject { object_id: u32, activated: bool },
1151 EscortNpc { npc_id: u32, start: (i32,i32), end: (i32,i32), reached: bool },
1152}
1153
1154pub struct QuestReward {
1155 pub experience: u32,
1156 pub gold: u32,
1157 pub items: Vec<u32>,
1158 pub reputation: HashMap<String, i32>,
1159}
1160
1161pub struct Quest {
1162 pub id: u32,
1163 pub name: String,
1164 pub description: String,
1165 pub status: QuestStatus,
1166 pub objectives: Vec<QuestObjectiveKind>,
1167 pub reward: QuestReward,
1168 pub prereq_quests: Vec<u32>,
1169 pub journal_entries: Vec<(u32, String)>,
1170 pub is_main_quest: bool,
1171 pub chapter: u32,
1172}
1173
1174pub struct QuestJournal {
1175 pub quests: HashMap<u32, Quest>,
1176 pub active_quest_id: Option<u32>,
1177}
1178
1179#[derive(Debug, Clone, PartialEq)]
1180pub enum TrapType { SpikePit, SwingingBlade, PressurePlate, MagicGlyph, NetTrap, PoisonDart, BoulderRoll, FireJet }
1181
1182#[derive(Debug, Clone)]
1183pub struct TrapDefinition {
1184 pub id: u32,
1185 pub trap_type: TrapType,
1186 pub position: (i32, i32),
1187 pub damage: u32,
1188 pub damage_type: String,
1189 pub triggered: bool,
1190 pub rearm_time: f32,
1191 pub time_since_trigger: f32,
1192 pub visible: bool,
1193 pub disarm_skill: u32,
1194}
1195
1196#[derive(Debug, Clone, PartialEq)]
1197pub enum DoorState { Open, Closed, Locked, Barred, Broken }
1198
1199#[derive(Debug, Clone)]
1200pub struct DoorDefinition {
1201 pub id: u32,
1202 pub position: (i32, i32),
1203 pub facing: u8,
1204 pub state: DoorState,
1205 pub key_id: Option<u32>,
1206 pub lock_difficulty: u32,
1207 pub hp: i32,
1208 pub material: String,
1209 pub auto_close_time: Option<f32>,
1210 pub is_secret: bool,
1211}
1212
1213pub struct DestructibleObject {
1214 pub id: u32,
1215 pub position: (i32, i32),
1216 pub object_type: String,
1217 pub hp: i32,
1218 pub max_hp: i32,
1219 pub destroyed: bool,
1220 pub blocking: bool,
1221 pub drops: Vec<TreasureItem>,
1222 pub destruction_effect: String,
1223 pub respawn_time: Option<f32>,
1224}
1225
1226pub struct WaterBody {
1227 pub id: u32,
1228 pub tiles: HashSet<(i32, i32)>,
1229 pub depth: f32,
1230 pub current_dir: (f32, f32),
1231 pub water_type: String,
1232 pub is_swimmable: bool,
1233 pub damage_per_turn: u32,
1234}
1235
1236pub struct FogOfWar {
1237 pub width: u32,
1238 pub height: u32,
1239 pub revealed: Vec<bool>,
1240 pub visible: Vec<bool>,
1241 pub los_radius: u32,
1242}
1243
1244pub struct LevelTransition {
1245 pub id: u32,
1246 pub position: (i32, i32),
1247 pub target_map: String,
1248 pub target_position: (i32, i32),
1249 pub direction: u8,
1250 pub requires_item: Option<u32>,
1251 pub requires_quest: Option<u32>,
1252 pub transition_type: String,
1253 pub is_bidirectional: bool,
1254}
1255
1256pub struct DungeonTheme {
1257 pub name: String,
1258 pub wall_tile_ids: Vec<u32>,
1259 pub floor_tile_ids: Vec<u32>,
1260 pub door_tile_id: u32,
1261 pub chest_frequency: f32,
1262 pub trap_frequency: f32,
1263 pub enemy_types: Vec<String>,
1264 pub ambient_light: (u8, u8, u8),
1265 pub torch_frequency: f32,
1266 pub special_tiles: HashMap<String, u32>,
1267 pub music_track: String,
1268}
1269
1270pub struct HeightMap {
1271 pub width: u32,
1272 pub height: u32,
1273 pub data: Vec<f32>,
1274}
1275
1276pub struct OverworldGenerator {
1277 pub width: u32,
1278 pub height: u32,
1279 pub seed: u64,
1280 pub sea_level: f32,
1281 pub mountain_threshold: f32,
1282 pub forest_threshold: f32,
1283}
1284
1285pub fn run_map_editor_integration_tests() {
1286 let mut world = WorldMap::new("Eldoria", 1000, 1000);
1288 let mut r1 = MapRegion::new(1, "Starter Town", RegionType::Town, (0, 0, 100, 100));
1289 let mut r2 = MapRegion::new(2, "Dark Forest", RegionType::Wilderness, (100, 0, 200, 200));
1290 let r3 = MapRegion::new(3, "Ancient Dungeon", RegionType::Dungeon, (300, 0, 100, 100));
1291 r1.connect(2); r2.connect(1); r2.connect(3);
1292 world.add_region(r1); world.add_region(r2); world.add_region(r3);
1293 world.discover_region(1);
1294 assert!(world.region_by_id(1).unwrap().discovered);
1295 assert!(!world.region_by_id(2).unwrap().discovered);
1296 let path = world.find_region_path(1, 3);
1297 assert!(path.contains(&1) && path.contains(&3));
1298
1299 let mut chest = TreasureChest::new(1, 5, 5);
1301 chest.add_item(TreasureItem::new(1, "Iron Sword", ItemRarity::Common, "weapon", 50));
1302 chest.add_item(TreasureItem::new(2, "Magic Staff", ItemRarity::Rare, "weapon", 200));
1303 chest.gold_amount = 100;
1304 assert!(chest.total_value() > 0);
1305 chest.set_trap(15);
1306 assert!(chest.is_trapped);
1307 let items = chest.open();
1308 assert_eq!(items.len(), 2);
1309
1310 let mut npc = NpcDefinition::new(1, "Aldric the Merchant", NpcRole::Merchant, (10, 10));
1312 let mut dialogue = NpcDialogueNode::new(0, "Aldric", "Welcome, traveler! What can I do for you?");
1313 dialogue.add_response("Buy", 1);
1314 dialogue.add_response("Sell", 2);
1315 dialogue.add_response("Goodbye", u32::MAX);
1316 npc.add_dialogue(dialogue);
1317 npc.add_to_schedule(8, (10, 10));
1318 npc.add_to_schedule(18, (15, 15));
1319 assert_eq!(npc.position_at_hour(12), (10, 10));
1320 assert_eq!(npc.position_at_hour(20), (15, 15));
1321 assert!(npc.is_merchant());
1322
1323 let mut quest = Quest::new(1, "The Lost Sword", "Find the ancient sword in the dungeon");
1325 quest.add_objective(QuestObjectiveKind::KillEnemy { enemy_type: "skeleton".into(), count: 5, progress: 5 });
1326 quest.add_objective(QuestObjectiveKind::ReachLocation { region_id: 3, reached: true });
1327 quest.start();
1328 quest.check_completion();
1329 assert_eq!(quest.status, QuestStatus::Completed);
1330 assert!((quest.progress_percentage() - 100.0).abs() < 0.1);
1331
1332 let mut journal = QuestJournal::new();
1333 journal.add_quest(Quest::new(2, "Main Quest 1", "Begin your journey"));
1334 journal.quests.get_mut(&2).unwrap().is_main_quest = true;
1335 journal.start_quest(2);
1336 assert_eq!(journal.active_quests().len(), 1);
1337
1338 let mut trap = TrapDefinition::new(1, TrapType::SwingingBlade, (3, 3));
1340 let dmg = trap.trigger();
1341 assert_eq!(dmg, 25);
1342 assert!(trap.triggered);
1343 trap.update(4.0);
1344 assert!(!trap.triggered);
1345
1346 let mut door = DoorDefinition::new(1, (5, 0), 0);
1348 door.lock(42, 8);
1349 assert!(!door.try_open(false, 5));
1350 assert!(door.try_open(true, 0));
1351
1352 let mut barrel = DestructibleObject::new(1, (7, 7), "barrel", 20);
1354 barrel.take_damage(15);
1355 assert!(!barrel.destroyed);
1356 barrel.take_damage(10);
1357 assert!(barrel.destroyed);
1358 assert!(!barrel.blocking);
1359
1360 let mut fog = FogOfWar::new(64, 64, 8);
1362 fog.reveal_radius(32, 32);
1363 assert!(fog.is_revealed(32, 32));
1364 assert!(fog.exploration_percentage() > 0.0);
1365
1366 let trans = LevelTransition::new(1, (0, 50), "dungeon_01", (1, 1));
1368 assert!(trans.can_use(|_| true, |_| true));
1369 assert!(!trans.can_use(|_| false, |_| true)); let mut hm = HeightMap::new(64, 64);
1373 hm.generate_fbm(4, 12345);
1374 assert!(*hm.data.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() >= 0.0);
1375 hm.smooth(2);
1376 hm.apply_erosion(2);
1377
1378 let gen = OverworldGenerator::new(128, 128, 999);
1380 let (hm2, tiles) = gen.generate();
1381 assert_eq!(tiles.len(), 128);
1382 let towns = gen.place_towns(&hm2, 5);
1383 assert!(!towns.is_empty());
1384 let river = gen.generate_river(&hm2, towns[0]);
1385 assert!(!river.is_empty());
1386
1387 let theme = DungeonTheme::crypt();
1389 let mut rng = MapRng::new(777);
1390 let wall = theme.random_wall_tile(&mut rng);
1391 assert!(theme.wall_tile_ids.contains(&wall));
1392
1393 let mut lake = WaterBody::new(1, "lake");
1395 for x in 0..10 { for y in 0..10 { lake.add_tile(x, y); } }
1396 assert_eq!(lake.area(), 100);
1397 assert!(lake.contains(5, 5));
1398 assert!(!lake.contains(15, 15));
1399}
1400
1401#[derive(Debug, Clone)]
1406pub struct TileAnimation {
1407 pub tile_id: u32,
1408 pub frames: Vec<u32>,
1409 pub frame_durations_ms: Vec<u32>,
1410 pub current_frame: usize,
1411 pub elapsed_ms: u32,
1412 pub loop_animation: bool,
1413}
1414
1415pub struct TileAnimationManager {
1416 pub animations: HashMap<u32, TileAnimation>,
1417}
1418
1419pub struct TileObject {
1420 pub id: u32,
1421 pub object_type: String,
1422 pub position: (i32, i32),
1423 pub size: (u32, u32),
1424 pub rotation: u8,
1425 pub flip_x: bool,
1426 pub flip_y: bool,
1427 pub layer: u32,
1428 pub properties: HashMap<String, String>,
1429 pub collidable: bool,
1430 pub sprite_id: u32,
1431}
1432
1433pub struct ObjectLayer {
1434 pub id: u32,
1435 pub name: String,
1436 pub objects: Vec<TileObject>,
1437 pub visible: bool,
1438 pub locked: bool,
1439}
1440
1441pub struct TmxExporter { pub version: String }
1442
1443pub struct CollisionMap {
1444 pub width: u32, pub height: u32, pub cells: Vec<u8>,
1445}
1446
1447pub struct SpawnPoint {
1448 pub id: u32, pub position: (i32, i32), pub spawn_type: String,
1449 pub max_concurrent: u32, pub current_count: u32, pub enabled: bool, pub respawn_delay_secs: f32,
1450}
1451
1452pub struct SpawnManager {
1453 pub spawn_points: Vec<SpawnPoint>,
1454 pub global_spawn_enabled: bool,
1455}
1456
1457pub struct MapChunk {
1458 pub chunk_x: i32, pub chunk_y: i32, pub tile_data: Vec<Vec<u32>>,
1459 pub is_loaded: bool, pub last_access_frame: u64, pub dirty: bool,
1460}
1461
1462pub struct ChunkStreamingManager {
1463 pub loaded_chunks: HashMap<(i32, i32), MapChunk>,
1464 pub stream_radius: i32,
1465 pub current_frame: u64,
1466}
1467
1468pub fn run_map_editor_extended_tests() {
1469 let mut anim = TileAnimation::new(5, vec![5, 6, 7, 8], 100);
1471 assert_eq!(anim.current_tile_id(), 5);
1472 anim.update(150); assert_eq!(anim.current_tile_id(), 6);
1473 anim.reset(); assert_eq!(anim.current_tile_id(), 5);
1474 assert_eq!(anim.total_duration_ms(), 400);
1475
1476 let mut amgr = TileAnimationManager::new();
1477 amgr.register(TileAnimationManager::standard_water_animation());
1478 amgr.update_all(175);
1479 let _ = amgr.resolve_tile(5);
1480
1481 let mut ol = ObjectLayer::new(1, "objects");
1483 let mut t1 = TileObject::new(1, "tree", (5, 5));
1484 t1.set_property("shade", "large");
1485 let t2 = TileObject::new(2, "rock", (5, 5));
1486 ol.add_object(t1); ol.add_object(t2);
1487 assert!(ol.object_at(5, 5).is_some());
1488 assert!(!ol.find_overlaps().is_empty());
1489 assert_eq!(ol.objects[0].get_property("shade"), Some("large"));
1490
1491 let tmx = TmxExporter::new();
1493 let m = TileMap::new(8, 8, 16, 16);
1494 let xml = tmx.export_map(&m, "test");
1495 assert!(xml.contains("<?xml"));
1496
1497 let mut coll = CollisionMap::new(16, 16);
1499 coll.set(3, 3, CollisionMap::SOLID);
1500 assert!(coll.is_solid(3, 3));
1501 assert!(coll.is_passable(4, 4));
1502 let nbrs = coll.passable_neighbors(4, 4);
1503 assert!(nbrs.len() >= 3);
1504
1505 let mut sm = SpawnManager::new();
1507 let mut sp = SpawnPoint::new(1, (5, 5), "goblin");
1508 sp.max_concurrent = 2;
1509 sm.add_spawn_point(sp);
1510 let av = sm.available_spawns_for_type("goblin");
1511 assert_eq!(av.len(), 1);
1512 assert_eq!(sm.total_capacity(), 2);
1513
1514 let mut csm = ChunkStreamingManager::new(2);
1516 csm.ensure_loaded(0, 0);
1517 assert!(csm.loaded_chunk_count() > 0);
1518 csm.current_frame = 1000;
1519 csm.evict_distant_chunks(1, 0);
1520 let _ = csm.get_tile_at(5, 5);
1522
1523 let mut world = WorldMap::new("Veldoria", 200, 200);
1525 let mut r1 = MapRegion::new(10, "Hometown", RegionType::Town, (0,0,20,20));
1526 let mut r2 = MapRegion::new(11, "Wilderness", RegionType::Wilderness, (20,0,40,40));
1527 r1.connect(11); r2.connect(10);
1528 world.add_region(r1); world.add_region(r2);
1529 world.discover_region(10);
1530 assert_eq!(world.discovered_regions().len(), 1);
1531 let path = world.find_region_path(10, 11);
1532 assert!(path.len() >= 2);
1533
1534 let mut chest = TreasureChest::new(99, 3, 3);
1536 chest.add_item(TreasureItem::new(1, "Sword", ItemRarity::Rare, "weapon", 100));
1537 chest.gold_amount = 50;
1538 assert!(chest.total_value() > 0);
1539 let items = chest.open();
1540 assert_eq!(items.len(), 1);
1541 assert!(chest.opened);
1542
1543 let mut door = DoorDefinition::new(1, (0, 5), 0);
1545 door.lock(7, 10);
1546 assert!(!door.try_open(false, 5));
1547 assert!(door.try_open(false, 10));
1548 assert_eq!(door.state, DoorState::Open);
1549
1550 let mut trap = TrapDefinition::new(1, TrapType::PoisonDart, (1, 1));
1552 let dmg = trap.trigger();
1553 assert!(dmg > 0);
1554 trap.update(10.0);
1555 assert!(!trap.triggered);
1556
1557 let mut fog = FogOfWar::new(32, 32, 6);
1559 fog.reveal_radius(16, 16);
1560 assert!(fog.exploration_percentage() > 0.0);
1561 fog.clear_visible();
1562 assert!(fog.is_revealed(16, 16));
1563 assert!(!fog.is_visible(16, 16));
1564
1565 let lt = LevelTransition::new(1, (0, 10), "next_level", (1, 1));
1567 assert!(lt.can_use(|_| true, |_| true));
1568
1569 let mut hm = HeightMap::new(32, 32);
1571 hm.generate_fbm(5, 99);
1572 let min_h = hm.data.iter().cloned().fold(f32::MAX, f32::min);
1573 let max_h = hm.data.iter().cloned().fold(f32::MIN, f32::max);
1574 assert!(min_h >= 0.0 && max_h <= 1.0);
1575 hm.smooth(2); hm.apply_erosion(3);
1576 let db = TileDatabase::new();
1577 let tiles = hm.to_terrain_tiles(&db, 0.3, 0.75);
1578 assert_eq!(tiles.len(), 32);
1579
1580 let gen = OverworldGenerator::new(64, 64, 555);
1582 let (hm2, _tiles) = gen.generate();
1583 let towns = gen.place_towns(&hm2, 3);
1584 assert!(!towns.is_empty());
1585 if !towns.is_empty() {
1586 let river = gen.generate_river(&hm2, towns[0]);
1587 assert!(!river.is_empty());
1588 }
1589}
1590
1591#[derive(Debug, Clone, PartialEq)]
1596pub enum TriggerCondition2 { PlayerEnter, PlayerExit, Interact, TimePassed(f32), ItemPickedUp(u32), QuestStage(u32, u32) }
1597
1598#[derive(Debug, Clone)]
1599pub struct MapTrigger {
1600 pub id: u32,
1601 pub region: (i32, i32, i32, i32),
1602 pub condition: TriggerCondition2,
1603 pub fired: bool,
1604 pub one_shot: bool,
1605 pub cooldown: f32,
1606 pub elapsed_cooldown: f32,
1607 pub script: String,
1608}
1609
1610pub struct TriggerManager2 {
1611 pub triggers: Vec<MapTrigger>,
1612}
1613
1614pub struct WarpPoint {
1615 pub id: u32,
1616 pub name: String,
1617 pub position: (i32, i32),
1618 pub target_map: String,
1619 pub target_position: (i32, i32),
1620 pub unlock_condition: Option<String>,
1621 pub is_unlocked: bool,
1622 pub warp_type: String,
1623 pub effect_name: String,
1624}
1625
1626pub struct WarpNetwork {
1627 pub warp_points: Vec<WarpPoint>,
1628}
1629
1630pub struct SoundZone {
1631 pub id: u32,
1632 pub polygon: Vec<(i32, i32)>,
1633 pub ambient_track: String,
1634 pub volume: f32,
1635 pub loop_audio: bool,
1636 pub fade_in_secs: f32,
1637 pub fade_out_secs: f32,
1638 pub reverb_preset: String,
1639 pub priority: i32,
1640}
1641
1642pub struct SoundZoneManager {
1643 pub zones: Vec<SoundZone>,
1644 pub current_zone_id: Option<u32>,
1645 pub transition_progress: f32,
1646}
1647
1648pub struct AoeZone {
1649 pub id: u32,
1650 pub center: (i32, i32),
1651 pub radius: f32,
1652 pub effect_type: String,
1653 pub damage_per_second: f32,
1654 pub duration_remaining: f32,
1655 pub team_mask: u32,
1656 pub visual_effect: String,
1657}
1658
1659pub struct AoeManager {
1660 pub zones: Vec<AoeZone>,
1661}
1662
1663pub struct StaticLight {
1664 pub id: u32,
1665 pub position: (i32, i32),
1666 pub color: (u8, u8, u8),
1667 pub radius: f32,
1668 pub intensity: f32,
1669 pub cast_shadows: bool,
1670 pub flicker: bool,
1671 pub flicker_speed: f32,
1672 pub flicker_time: f32,
1673}
1674
1675pub struct DynamicLightMap {
1676 pub width: u32,
1677 pub height: u32,
1678 pub light_values: Vec<f32>,
1679 pub ambient_level: f32,
1680}
1681
1682pub struct MapEditorStats {
1683 pub total_tiles_placed: u64,
1684 pub total_tiles_erased: u64,
1685 pub undo_operations: u32,
1686 pub redo_operations: u32,
1687 pub layers_created: u32,
1688 pub objects_placed: u32,
1689 pub rooms_generated: u32,
1690 pub maps_saved: u32,
1691 pub maps_loaded: u32,
1692 pub session_start: String,
1693 pub editing_time_secs: f64,
1694}
1695
1696pub trait MapEditorPlugin: std::fmt::Debug {
1697 fn name(&self) -> &str;
1698 fn on_tile_placed(&mut self, x: i32, y: i32, tile_id: u32);
1699 fn on_selection_changed(&mut self, tiles: &[(i32, i32)]);
1700 fn on_layer_changed(&mut self, layer_name: &str);
1701}
1702
1703#[derive(Debug)]
1704pub struct AutoTilePlugin { pub name: String, pub last_updated: u32 }
1705#[derive(Debug)]
1706pub struct CollisionPlugin { pub name: String, pub auto_build: bool }
1707pub struct MapExportConfig {
1708 pub format: String,
1709 pub include_collision: bool,
1710 pub include_nav_mesh: bool,
1711 pub include_spawns: bool,
1712 pub include_metadata: bool,
1713 pub compress: bool,
1714 pub output_path: String,
1715 pub atlas_size: (u32, u32),
1716}
1717
1718pub struct MapExporter {
1719 pub config: MapExportConfig,
1720 pub bytes_written: usize,
1721}
1722
1723pub fn run_map_comprehensive_tests() {
1724 let mut tm = TriggerManager2::new();
1726 tm.add(MapTrigger::on_enter(1, 5, 5, 10, 10, "start_dialogue_01"));
1727 tm.add(MapTrigger::on_enter(2, 20, 20, 5, 5, "boss_cutscene"));
1728 let scripts = tm.check_enter(8, 8);
1729 assert!(!scripts.is_empty());
1730 assert!(scripts[0].contains("dialogue"));
1731 let scripts2 = tm.check_enter(8, 8);
1732 assert!(scripts2.is_empty()); assert_eq!(tm.fired_count(), 1);
1734
1735 let mut wn = WarpNetwork::new();
1737 wn.add(WarpPoint::new(1, "Town Warp", (10, 10), "town_map", (5, 5)));
1738 wn.add(WarpPoint::new(2, "Dungeon Warp", (50, 50), "dungeon_01", (1, 1)));
1739 wn.warp_points[1].is_unlocked = false;
1740 assert_eq!(wn.unlocked_count(), 1);
1741 let nearest = wn.nearest_unlocked((12, 12));
1742 assert!(nearest.is_some());
1743 assert_eq!(nearest.unwrap().name, "Town Warp");
1744 wn.unlock_all();
1745 assert_eq!(wn.unlocked_count(), 2);
1746
1747 let mut szm = SoundZoneManager::new();
1749 let cave_zone = SoundZone::new(1, vec![(0,0),(20,0),(20,20),(0,20)], "cave_ambience");
1750 szm.add_zone(cave_zone);
1751 szm.update_player_position(10, 10, 1.0);
1752 assert_eq!(szm.current_zone_id, Some(1));
1753 assert!(szm.current_volume() > 0.0);
1754 szm.update_player_position(50, 50, 0.1);
1755 assert_eq!(szm.current_zone_id, None);
1756
1757 let mut aoe = AoeManager::new();
1759 aoe.add_zone(AoeZone::fire_pillar(1, 10, 10, 5.0, 3.0));
1760 aoe.add_zone(AoeZone::frost_zone(2, 10, 10, 3.0, 2.0));
1761 assert_eq!(aoe.zones_at(10, 10).len(), 2);
1762 let dmg = aoe.total_damage_at(10, 10, 0.1);
1763 assert!(dmg > 0.0);
1764 aoe.update(2.5);
1765 assert_eq!(aoe.zones.len(), 1); aoe.update(1.0);
1767 assert!(aoe.zones.is_empty());
1768
1769 let lights = vec![StaticLight::torch(1, 10, 10), StaticLight::lamp(2, 20, 20), StaticLight::torch(3, 5, 5)];
1771 let mut light_map = DynamicLightMap::new(32, 32, 0.05);
1772 light_map.bake_static_lights(&lights);
1773 let il = light_map.get(10, 10);
1774 assert!(il > 0.05);
1775 let avg = light_map.average_illuminance();
1776 assert!(avg > 0.05);
1777 let dark_count = light_map.dark_tile_count(0.1);
1778 assert!(dark_count > 0);
1779
1780 let mut stats = MapEditorStats::new();
1782 stats.record_tile_place(150);
1783 stats.record_tile_erase(30);
1784 stats.advance_time(60.0);
1785 assert_eq!(stats.net_tiles(), 120);
1786 assert!(stats.tiles_per_minute() > 0.0);
1787 let summary = stats.summary();
1788 assert!(summary.contains("150"));
1789
1790 let config = MapExportConfig::default_config("level_01.map");
1792 let mut exporter = MapExporter::new(config);
1793 let test_map = TileMap::new(32, 32, 16, 16);
1794 let header = exporter.export_header(&test_map);
1795 assert!(header.starts_with(b"MAPF"));
1796 assert_eq!(header.len(), 20);
1797 let json = exporter.export_map_json(&test_map, "test_level");
1798 assert!(json.contains("test_level"));
1799 assert!(json.contains("32"));
1800 let binary = exporter.export_map_binary(&TileMap::new(8, 8, 16, 16));
1801 assert!(!binary.is_empty());
1802
1803 let mut atp = AutoTilePlugin::new();
1805 atp.on_tile_placed(5, 5, 42);
1806 assert_eq!(atp.last_updated, 1);
1807 assert_eq!(atp.name(), "AutoTile");
1808 let cp = CollisionPlugin::new();
1809 assert_eq!(cp.name(), "CollisionBuilder");
1810 assert!(cp.auto_build);
1811
1812 let dungeon_editor = build_sample_dungeon(64, 64, 12345);
1814 assert!(!dungeon_editor.map.layers.is_empty());
1815 stats.rooms_generated += dungeon_editor.encounter_zones.len() as u32;
1816 stats.record_tile_place(dungeon_editor.map.layers[0].tiles.len() as u64);
1817
1818 let mut torch = StaticLight::torch(99, 15, 15);
1820 for _ in 0..60 { torch.update(1.0/60.0); }
1821 let intensity = torch.current_intensity();
1822 assert!(intensity > 0.5 && intensity < 2.0);
1823
1824 let mut fog = FogOfWar::new(64, 64, 8);
1826 let waypoints = vec![(8,8), (16,16), (32,32), (48,48), (56,56)];
1827 for &(wx, wy) in &waypoints { fog.reveal_radius(wx, wy); }
1828 let explored_pct = fog.exploration_percentage();
1829 assert!(explored_pct > 20.0);
1830
1831 let gen = OverworldGenerator::new(256, 256, 42);
1833 let (hm, _) = gen.generate();
1834 let towns = gen.place_towns(&hm, 8);
1835 assert!(!towns.is_empty());
1836 let mut rivers = Vec::new();
1837 for &town in &towns { rivers.push(gen.generate_river(&hm, town)); }
1838 assert_eq!(rivers.len(), towns.len());
1839 for (i, river) in rivers.iter().enumerate() { assert!(!river.is_empty(), "River {} is empty", i); }
1841}
1842
1843#[derive(Debug, Clone)]
1848pub struct DungeonRoom {
1849 pub id: u32,
1850 pub x: i32,
1851 pub y: i32,
1852 pub width: i32,
1853 pub height: i32,
1854 pub room_type: DungeonRoomType,
1855 pub is_cleared: bool,
1856 pub connections: Vec<u32>,
1857 pub loot_level: u8,
1858}
1859
1860#[derive(Debug, Clone, PartialEq)]
1861pub enum DungeonRoomType {
1862 Entrance, Exit, Combat, Treasure, BossRoom, Corridor, Shop, Rest, Puzzle, Secret,
1863}
1864
1865impl DungeonRoom {
1866 pub fn area(&self) -> i32 { self.width * self.height }
1867}
1868
1869pub struct DungeonGenerator {
1870 pub width: i32,
1871 pub height: i32,
1872 pub seed: u64,
1873 pub min_room_size: i32,
1874 pub max_room_size: i32,
1875 pub max_rooms: u32,
1876}
1877
1878#[derive(Clone, Copy, Debug)]
1879pub struct TilePos { pub x: i32, pub y: i32 }
1880
1881pub struct PathfindingGrid {
1882 pub width: i32,
1883 pub height: i32,
1884 pub passable: Vec<bool>,
1885 pub tile_cost: Vec<f32>,
1886}
1887
1888#[derive(Debug, Clone, PartialEq)]
1889pub enum MapEventType {
1890 PlayerEnter, PlayerExit, MobSpawn, MobKill, ChestOpen, TrapTriggered,
1891 LevelTransition, QuestStart, QuestComplete, BossDefeated, SecretFound,
1892}
1893
1894#[derive(Debug, Clone)]
1895pub struct MapEvent {
1896 pub id: u32,
1897 pub event_type: MapEventType,
1898 pub position: (i32, i32),
1899 pub data: HashMap<String, String>,
1900 pub fired: bool,
1901 pub cooldown_s: f32,
1902 pub last_fire_time: f32,
1903}
1904
1905pub struct MapEventSystem {
1906 pub events: Vec<MapEvent>,
1907 pub current_time: f32,
1908}
1909
1910#[derive(Debug, Clone)]
1911pub struct MinimapCell {
1912 pub explored: bool,
1913 pub visible: bool,
1914 pub tile_type: u8,
1915 pub has_entity: bool,
1916}
1917
1918pub struct Minimap {
1919 pub width: u32,
1920 pub height: u32,
1921 pub cells: Vec<MinimapCell>,
1922 pub scale: u32, }
1924
1925#[derive(Clone, Debug)]
1926pub enum PatrolState { Idle, Walking, Alert, Searching, Returning }
1927
1928#[derive(Debug, Clone)]
1929pub struct PatrolWaypoint {
1930 pub position: (i32, i32),
1931 pub wait_time_s: f32,
1932 pub animation_hint: String,
1933}
1934
1935pub struct PatrolAi {
1936 pub npc_id: u32,
1937 pub waypoints: Vec<PatrolWaypoint>,
1938 pub current_waypoint: usize,
1939 pub state: PatrolState,
1940 pub position: (f32, f32),
1941 pub move_speed: f32,
1942 pub alert_radius: f32,
1943 pub search_timer: f32,
1944 pub wait_timer: f32,
1945 pub home_position: (f32, f32),
1946}
1947
1948pub struct MapAnnotation {
1949 pub id: u32,
1950 pub tile_pos: (i32, i32),
1951 pub text: String,
1952 pub annotation_type: String,
1953 pub color: (u8, u8, u8),
1954 pub visible: bool,
1955 pub created_time: f32,
1956}
1957
1958pub struct MapAnnotationLayer {
1959 pub annotations: Vec<MapAnnotation>,
1960 pub visible: bool,
1961}
1962
1963pub fn run_dungeon_generator_tests() {
1964 let gen = DungeonGenerator::new(100, 100, 42);
1965 let rooms = gen.generate();
1966 assert!(!rooms.is_empty());
1967 assert_eq!(rooms[0].room_type, DungeonRoomType::Entrance);
1968 let connected = rooms.iter().filter(|r| !r.connections.is_empty() || r.room_type == DungeonRoomType::Entrance).count();
1970 assert!(connected > 0);
1971 let has_boss = rooms.iter().any(|r| r.room_type == DungeonRoomType::BossRoom);
1973 if rooms.len() >= 4 { assert!(has_boss); }
1974 let treasure = rooms.iter().find(|r| r.room_type == DungeonRoomType::Treasure);
1975 assert!(rooms.len() < 3 || treasure.is_some());
1976 for room in &rooms { assert!(room.area() > 0); }
1978}
1979
1980pub fn run_pathfinding_tests() {
1981 let mut grid = PathfindingGrid::new(20, 20);
1982 for y in 5..15 { grid.set_passable(10, y, false); }
1984 let path = grid.find_path_astar(TilePos::new(5, 10), TilePos::new(15, 10), false);
1985 assert!(path.is_some());
1986 let p = path.unwrap();
1987 assert!(!p.is_empty());
1988 assert_eq!(*p.first().unwrap(), TilePos::new(5, 10));
1989 assert_eq!(*p.last().unwrap(), TilePos::new(15, 10));
1990 for pos in &p { assert!(grid.is_passable(pos)); }
1992
1993 let a = TilePos::new(0, 0);
1995 let b = TilePos::new(3, 4);
1996 assert!(a.octile_distance(&b) > 0.0);
1997 assert!(a.manhattan_distance(&b) == 7);
1998}
1999
2000pub fn run_map_event_tests() {
2001 let mut sys = MapEventSystem::new();
2002 sys.add_event(MapEvent::new(1, MapEventType::PlayerEnter, 5, 5).with_data("zone", "town"));
2003 sys.add_event(MapEvent::new(2, MapEventType::TrapTriggered, 5, 5).with_cooldown(10.0));
2004 sys.add_event(MapEvent::new(3, MapEventType::ChestOpen, 8, 3));
2005
2006 let events_at_55 = sys.events_at(5, 5);
2007 assert_eq!(events_at_55.len(), 2);
2008
2009 let fired = sys.trigger_at(5, 5);
2010 assert_eq!(fired.len(), 2);
2011
2012 let trap_events = sys.events_of_type(&MapEventType::TrapTriggered);
2013 assert_eq!(trap_events.len(), 1);
2014
2015 sys.reset_all();
2016 assert!(sys.events[0].can_fire(0.0));
2017}
2018
2019pub fn run_minimap_tests() {
2020 let mut minimap = Minimap::new(64, 64, 2);
2021 assert_eq!(minimap.exploration_percent(), 0.0);
2022 minimap.reveal_radius(32, 32, 8);
2023 assert!(minimap.exploration_percent() > 0.0);
2024 assert!(minimap.is_explored(32, 32));
2025 assert!(!minimap.is_explored(0, 0));
2026 let (mx, my) = minimap.world_to_minimap(10.5, 20.3);
2027 assert!(mx < minimap.width && my < minimap.height);
2028}
2029
2030pub fn run_patrol_ai_tests() {
2031 let mut ai = PatrolAi::new(1, (0.0, 0.0), 3.0);
2032 ai.add_waypoint(PatrolWaypoint::new(10, 0).with_wait(1.0));
2033 ai.add_waypoint(PatrolWaypoint::new(10, 10));
2034 ai.add_waypoint(PatrolWaypoint::new(0, 10));
2035 assert_eq!(ai.state, PatrolState::Idle);
2036 ai.update(0.016);
2037 assert_eq!(ai.state, PatrolState::Walking);
2038 ai.alert();
2039 assert_eq!(ai.state, PatrolState::Alert);
2040 ai.update(0.016);
2041 assert_eq!(ai.state, PatrolState::Searching);
2042 ai.update(6.0);
2044 assert_eq!(ai.state, PatrolState::Returning);
2045}
2046
2047pub fn run_annotation_tests() {
2048 let mut layer = MapAnnotationLayer::new();
2049 layer.add(MapAnnotation::new(1, 5, 5, "Town entrance"));
2050 layer.add(MapAnnotation::warning(2, 10, 10, "Danger zone"));
2051 layer.add(MapAnnotation::secret(3, 20, 20, "Hidden passage"));
2052 assert_eq!(layer.visible_count(), 3);
2053 let at_55 = layer.at_tile(5, 5);
2054 assert_eq!(at_55.len(), 1);
2055 let search = layer.search("Hidden");
2056 assert_eq!(search.len(), 1);
2057 let notes = layer.export_notes();
2058 assert!(notes.contains("Town entrance"));
2059 layer.remove(1);
2060 assert_eq!(layer.annotations.len(), 2);
2061}
2062
2063pub fn run_all_map_editor_tests_extended() {
2064 run_dungeon_generator_tests();
2065 run_pathfinding_tests();
2066 run_map_event_tests();
2067 run_minimap_tests();
2068 run_patrol_ai_tests();
2069 run_annotation_tests();
2070 run_map_editor_integration_tests();
2071 run_map_editor_extended_tests();
2072}
2073
2074pub fn wang_blob_index(n: bool, e: bool, s: bool, w: bool, ne: bool, se: bool, sw: bool, nw: bool) -> u8 {
2080 let mut idx = 0u8;
2081 if n { idx |= 1; }
2082 if e { idx |= 2; }
2083 if s { idx |= 4; }
2084 if w { idx |= 8; }
2085 if nw && n && w { idx |= 16; }
2087 if ne && n && e { idx |= 32; }
2088 if se && s && e { idx |= 64; }
2089 if sw && s && w { idx |= 128; }
2090 idx
2091}
2092
2093pub fn wang_blob_name(idx: u8) -> &'static str {
2095 match idx {
2096 0 => "isolated", 2 => "edge_east", 8 => "edge_west",
2097 10 => "corridor_horizontal", 1 => "edge_north", 4 => "edge_south",
2098 5 => "corridor_vertical", 15 => "cross", 255 => "full_interior",
2099 _ => "complex",
2100 }
2101}
2102
2103#[derive(Debug, Clone)]
2104pub struct AutotileResult {
2105 pub x: i32,
2106 pub y: i32,
2107 pub tile_index: u8,
2108}
2109
2110#[derive(Debug, Clone)]
2111pub struct AutotileLayer {
2112 pub width: i32,
2113 pub height: i32,
2114 pub tiles: Vec<u8>, pub result: Vec<AutotileResult>,
2116}
2117
2118pub struct LootEntry {
2119 pub item_id: u32,
2120 pub item_name: String,
2121 pub weight: u32,
2122 pub min_quantity: u32,
2123 pub max_quantity: u32,
2124 pub min_level: u32,
2125 pub condition: Option<String>,
2126}
2127
2128pub struct LootTable {
2129 pub id: u32,
2130 pub name: String,
2131 pub entries: Vec<LootEntry>,
2132 pub guaranteed_entries: Vec<LootEntry>,
2133 pub rolls_min: u32,
2134 pub rolls_max: u32,
2135}
2136
2137pub struct LootTableRegistry {
2138 pub tables: HashMap<u32, LootTable>,
2139}
2140
2141#[derive(Debug, Clone, PartialEq)]
2142pub enum TileEffect {
2143 None, Slippery, Slow, Damage, Heal, Teleport, Invisible, Burning, Poisoned,
2144}
2145
2146#[derive(Debug, Clone)]
2147pub struct TileEffectMap {
2148 pub width: i32,
2149 pub height: i32,
2150 pub effects: Vec<TileEffect>,
2151 pub effect_params: Vec<f32>, }
2153
2154pub enum MapLayerType { Tile, Object, Image, Group }
2155
2156pub struct MapLayerStack {
2157 pub layers: Vec<MapLayer>,
2158 pub width: i32,
2159 pub height: i32,
2160}
2161
2162pub struct MapProgressionData {
2163 pub map_id: u32,
2164 pub total_secrets: u32,
2165 pub found_secrets: u32,
2166 pub total_enemies: u32,
2167 pub killed_enemies: u32,
2168 pub total_chests: u32,
2169 pub opened_chests: u32,
2170 pub boss_defeated: bool,
2171 pub time_spent_s: f32,
2172 pub completion_flags: HashSet<String>,
2173}
2174
2175pub fn run_autotile_tests() {
2176 let mut layer = AutotileLayer::new(20, 20);
2177 layer.fill_rect(5, 5, 10, 10);
2178 assert!(layer.solid_count() > 0);
2179 layer.compute_autotile();
2180 assert!(!layer.result.is_empty());
2181 let center = layer.result.iter().find(|r| r.x == 7 && r.y == 7);
2183 assert!(center.is_some());
2184 assert_eq!(center.unwrap().tile_index, 255);
2185 let edge = layer.result.iter().find(|r| r.x == 5 && r.y == 5);
2187 assert!(edge.is_some());
2188 assert!(wang_blob_name(0).len() > 0);
2189}
2190
2191pub fn run_loot_table_tests() {
2192 let mut registry = LootTableRegistry::new();
2193 registry.register(LootTable::goblin_treasure());
2194 registry.register(LootTable::boss_hoard());
2195 assert_eq!(registry.table_count(), 2);
2196
2197 let loot = registry.roll_table(1, 12345, 5);
2198 assert!(!loot.is_empty()); let boss_loot = registry.roll_table(2, 99999, 10);
2200 assert!(!boss_loot.is_empty());
2201 let boss = LootTable::boss_hoard();
2203 assert!(boss.total_weight() > 0.0);
2204}
2205
2206pub fn run_tile_effect_tests() {
2207 let mut emap = TileEffectMap::new(20, 20);
2208 emap.set_effect(5, 5, TileEffect::Damage, 10.0);
2209 emap.set_effect(6, 6, TileEffect::Heal, 5.0);
2210 emap.set_effect(7, 7, TileEffect::Slow, 0.5);
2211
2212 let mut hp = 100.0f32;
2213 let mut speed = 1.0f32;
2214 emap.apply_to_entity(5, 5, &mut hp, &mut speed);
2215 assert_eq!(hp, 90.0);
2216
2217 emap.apply_to_entity(6, 6, &mut hp, &mut speed);
2218 assert_eq!(hp, 95.0);
2219
2220 emap.apply_to_entity(7, 7, &mut hp, &mut speed);
2221 assert!((speed - 0.5).abs() < 0.001);
2222
2223 assert_eq!(emap.effect_at(5, 5), &TileEffect::Damage);
2224 assert_eq!(emap.effect_at(0, 0), &TileEffect::None);
2225 let counts = emap.effects_count_by_type();
2226 assert!(counts.contains_key("Damage"));
2227}
2228
2229pub fn run_layer_stack_tests() {
2230 let mut stack = MapLayerStack::standard_rpg_layers(32, 32);
2231 assert_eq!(stack.layer_count(), 6);
2232 assert_eq!(stack.visible_layer_count(), 6);
2233
2234 let terrain = stack.layer_by_name_mut("terrain").unwrap();
2235 terrain.fill(1);
2236 terrain.set_tile(5, 5, 42);
2237
2238 let composited = stack.composite_tile_at(5, 5);
2239 assert!(composited > 0);
2241
2242 let layer = stack.layer_by_name("terrain").unwrap();
2243 assert_eq!(layer.tile_count(42), 1);
2244 assert!(layer.non_empty_count() > 0);
2245}
2246
2247pub fn run_progression_tests() {
2248 let mut prog = MapProgressionData::new(1);
2249 prog.total_enemies = 20; prog.total_chests = 5; prog.total_secrets = 3;
2250 prog.killed_enemies = 20; prog.opened_chests = 5; prog.found_secrets = 3;
2251 prog.boss_defeated = true;
2252 assert!(prog.is_100_percent());
2253 assert_eq!(prog.grade(), 'S');
2254 prog.set_flag("found_shortcut");
2255 assert!(prog.has_flag("found_shortcut"));
2256
2257 let mut partial = MapProgressionData::new(2);
2258 partial.total_enemies = 10; partial.killed_enemies = 5;
2259 partial.total_chests = 4; partial.opened_chests = 2;
2260 assert!(partial.completion_percent() < 100.0);
2261 assert!(partial.completion_percent() > 0.0);
2262}
2263
2264pub fn map_editor_run_all() {
2265 run_all_map_editor_tests_extended();
2266 run_autotile_tests();
2267 run_loot_table_tests();
2268 run_tile_effect_tests();
2269 run_layer_stack_tests();
2270 run_progression_tests();
2271}
2272
2273pub const MAP_EDITOR_MODULE_VERSION: &str = "3.0.0";
2278pub const MAX_DUNGEON_ROOMS: u32 = 100;
2279pub const MAX_DUNGEON_ROOM_SIZE: i32 = 32;
2280pub const MIN_DUNGEON_ROOM_SIZE: i32 = 4;
2281pub const DEFAULT_TILE_SIZE_PX: u32 = 16;
2282pub const MAX_MAP_LAYERS: usize = 32;
2283pub const MAX_ANNOTATION_TEXT_LEN: usize = 256;
2284pub const MAX_PATROL_WAYPOINTS: usize = 64;
2285pub const NPC_ALERT_RADIUS_DEFAULT: f32 = 5.0;
2286pub const NPC_SEARCH_DURATION_S: f32 = 5.0;
2287pub const LOOT_TABLE_MAX_ENTRIES: usize = 128;
2288pub const LOOT_TABLE_MAX_ROLLS: u32 = 10;
2289pub const PATHFINDING_MAX_ITERATIONS: u32 = 100_000;
2290pub const MINIMAP_DEFAULT_SCALE: u32 = 2;
2291pub const FOG_OF_WAR_DEFAULT_RADIUS: i32 = 8;
2292pub const MAP_EVENT_MAX_COOLDOWN_S: f32 = 3600.0;
2293pub const AUTOTILE_WANG_BLOB_VARIANTS: u32 = 256;
2294pub const MAP_EXPORT_MAGIC: u32 = 0x4D415045; pub const MAP_EXPORT_VERSION: u32 = 3;
2296
2297pub fn map_editor_module_info() -> HashMap<String, String> {
2298 let mut info = HashMap::new();
2299 info.insert("module".into(), "map_editor".into());
2300 info.insert("version".into(), MAP_EDITOR_MODULE_VERSION.into());
2301 info.insert("max_map_size".into(), format!("{}x{}", MAX_MAP_WIDTH, MAX_MAP_HEIGHT));
2302 info.insert("max_dungeon_rooms".into(), format!("{}", MAX_DUNGEON_ROOMS));
2303 info.insert("default_tile_size".into(), format!("{}px", DEFAULT_TILE_SIZE_PX));
2304 info.insert("max_layers".into(), format!("{}", MAX_MAP_LAYERS));
2305 info.insert("export_magic".into(), format!("{:#010X}", MAP_EXPORT_MAGIC));
2306 info.insert("pathfinding".into(), "A* with octile heuristic".into());
2307 info.insert("dungeon_gen".into(), "BSP + Prim MST".into());
2308 info.insert("autotile".into(), "Wang blob 256-variant".into());
2309 info
2310}
2311
2312#[derive(Debug, Clone, PartialEq)]
2317pub enum ContentType { Enemy, Chest, Trap, Decoration, Light, Exit, Spawn, Boss }
2318
2319#[derive(Debug, Clone)]
2320pub struct DungeonContent {
2321 pub position: (i32, i32),
2322 pub content_type: ContentType,
2323 pub enemies: Vec<(i32, i32, String)>,
2324 pub chests: Vec<(i32, i32, u32)>,
2325 pub traps: Vec<(i32, i32, String)>,
2326 pub data: HashMap<String, String>,
2327}
2328
2329pub struct DungeonContentPlacer {
2330 pub rng: MapRng,
2331 pub enemy_density: f32,
2332 pub chest_density: f32,
2333 pub trap_density: f32,
2334}
2335
2336#[derive(Debug, Clone, PartialEq)]
2337pub enum WeatherType { Clear, Cloudy, Rain, HeavyRain, Thunderstorm, Snow, Fog, Blizzard, Sandstorm, Hail, Overcast, LightRain, LightSnow, HeavySnow, HeavyFog, Sleet }
2338
2339#[derive(Debug, Clone)]
2340pub struct MapWeatherState {
2341 pub weather_type: String,
2342 pub intensity: f32,
2343 pub wind_speed_ms: f32,
2344 pub wind_direction_deg: f32,
2345 pub temperature_c: f32,
2346 pub visibility_m: f32,
2347 pub precipitation_mm_hr: f32,
2348}
2349
2350pub struct WeatherTransition {
2351 pub from: WeatherType,
2352 pub to: WeatherType,
2353 pub duration_s: f32,
2354 pub elapsed_s: f32,
2355}
2356
2357pub struct MapSaveState {
2358 pub map_id: u32,
2359 pub player_x: f32,
2360 pub player_y: f32,
2361 pub visited_chunks: HashSet<(i32, i32)>,
2362 pub cleared_rooms: HashSet<u32>,
2363 pub opened_chests: HashSet<u32>,
2364 pub killed_enemies: HashSet<u64>,
2365 pub world_time: f64,
2366 pub save_version: u32,
2367}
2368
2369pub fn run_dungeon_content_tests() {
2370 let gen = DungeonGenerator::new(80, 80, 1337);
2371 let rooms = gen.generate();
2372 assert!(!rooms.is_empty());
2373 let mut placer = DungeonContentPlacer::new(12345);
2374 let content = placer.populate_dungeon(&rooms);
2375 assert!(!content.is_empty());
2376 let has_spawn = content.iter().any(|c| c.content_type == ContentType::Spawn);
2378 assert!(has_spawn || rooms[0].room_type == DungeonRoomType::Entrance);
2379 if rooms.len() >= 4 {
2381 let has_boss = content.iter().any(|c| c.content_type == ContentType::Boss);
2382 assert!(has_boss);
2383 }
2384}
2385
2386pub fn run_weather_tests() {
2387 let clear = MapWeatherState::clear();
2388 assert_eq!(clear.movement_speed_modifier(), 1.0);
2389 assert!(!clear.is_dangerous());
2390 assert!((clear.ambient_light_factor() - 1.0).abs() < 0.001);
2391
2392 let storm = MapWeatherState::thunderstorm();
2393 assert!(storm.is_dangerous());
2394 assert!(storm.movement_speed_modifier() < 1.0);
2395 assert!(storm.ambient_light_factor() < 1.0);
2396
2397 let rain = MapWeatherState::rain(0.8);
2398 assert!(rain.precipitation_mm_hr > 0.0);
2399 assert!(rain.visibility_m < 10_000.0);
2400
2401 let snow = MapWeatherState::snow(0.5);
2402 assert!(snow.temperature_c < 0.0);
2403 assert!(snow.movement_speed_modifier() < 1.0);
2404
2405 let mut transition = WeatherTransition::new(WeatherType::Clear, WeatherType::Rain, 60.0);
2406 transition.update(30.0);
2407 assert!((transition.progress() - 0.5).abs() < 0.001);
2408 assert!(!transition.is_complete());
2409 transition.update(30.0);
2410 assert!(transition.is_complete());
2411}
2412
2413pub fn run_save_state_tests() {
2414 let mut state = MapSaveState::new(42);
2415 state.mark_chunk_visited(0, 0); state.mark_chunk_visited(1, 0); state.mark_chunk_visited(0, 1);
2416 assert_eq!(state.visited_chunk_count(), 3);
2417 assert!(state.is_chunk_visited(1, 0));
2418 assert!(!state.is_chunk_visited(5, 5));
2419
2420 state.mark_room_cleared(1);
2421 state.mark_chest_opened(101);
2422 state.mark_enemy_killed(999_000_001);
2423 assert!(state.is_room_cleared(1));
2424 assert!(state.is_chest_opened(101));
2425 assert!(state.is_enemy_killed(999_000_001));
2426
2427 state.advance_time(3600.0);
2428 assert!(state.world_time > 0.0);
2429 assert!(state.time_of_day_fraction() >= 0.0 && state.time_of_day_fraction() < 1.0);
2430
2431 let header = state.serialize_header();
2432 assert_eq!(&header[0..4], b"MSAV");
2433 assert_eq!(header.len(), 20); }
2435
2436pub fn map_editor_complete_suite() {
2437 map_editor_run_all();
2438 run_dungeon_content_tests();
2439 run_weather_tests();
2440 run_save_state_tests();
2441 let info = map_editor_module_info();
2442 assert!(info.contains_key("version"));
2443 assert!(info.contains_key("pathfinding"));
2444}
2445
2446#[derive(Debug, Clone)]
2451pub struct TileRect {
2452 pub x: i32,
2453 pub y: i32,
2454 pub width: i32,
2455 pub height: i32,
2456}
2457
2458#[derive(Clone, Debug)]
2459pub struct TileClipboard {
2460 pub tiles: Vec<Vec<u32>>, pub width: usize,
2462 pub height: usize,
2463}
2464
2465pub struct MapEditorSelection {
2466 pub selection: Option<TileRect>,
2467 pub clipboard: TileClipboard,
2468 pub is_active: bool,
2469}
2470
2471#[derive(Debug, Clone, PartialEq)]
2472pub enum PaintTool { Pencil, Eraser, Fill, BoxFill, Line, Circle }
2473
2474#[derive(Debug, Clone)]
2475pub struct TilePainter {
2476 pub active_tool: PaintTool,
2477 pub active_tile_id: u32,
2478 pub brush_size: u32,
2479 pub layer_name: String,
2480 pub paint_history: Vec<(i32, i32, u32, u32)>, }
2482
2483#[derive(Debug, Clone)]
2484pub struct TileChange { pub x: i32, pub y: i32, pub layer: String, pub before: u32, pub after: u32 }
2485
2486#[derive(Debug, Clone)]
2487pub struct MapUndoAction {
2488 pub description: String,
2489 pub changes: Vec<TileChange>,
2490}
2491
2492pub struct MapUndoHistory {
2493 pub actions: VecDeque<MapUndoAction>,
2494 pub redo_stack: Vec<MapUndoAction>,
2495 pub max_history: usize,
2496}
2497
2498impl MapLayer {
2499 pub fn set_tile(&mut self, x: usize, y: usize, tile_id: u32) {
2500 if x < self.width && y < self.height { self.tiles[y * self.width + x].tile_id = tile_id; }
2501 }
2502 pub fn tile_at(&self, x: usize, y: usize) -> u32 {
2503 self.get(x, y).map(|t| t.tile_id).unwrap_or(0)
2504 }
2505 pub fn new(name: impl Into<String>, layer_type: LayerType, width: usize, height: usize) -> Self {
2506 Self { name: name.into(), layer_type, tiles: vec![Tile::default(); width * height], width, height, visible: true, locked: false, opacity: 1.0, z_offset: 0.0 }
2507 }
2508 pub fn set(&mut self, x: usize, y: usize, tile: Tile) {
2509 if x < self.width && y < self.height { self.tiles[y * self.width + x] = tile; }
2510 }
2511 pub fn get(&self, x: usize, y: usize) -> Option<&Tile> {
2512 if x < self.width && y < self.height { Some(&self.tiles[y * self.width + x]) } else { None }
2513 }
2514 pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize, tile: Tile) { for ty in y..y+h { for tx in x..x+w { self.set(tx, ty, tile.clone()); } } }
2515 pub fn fill(&mut self, tile_id: u32) { for t in &mut self.tiles { t.tile_id = tile_id; } }
2516 pub fn fill_with(&mut self, tile_id: u32) { for t in &mut self.tiles { t.tile_id = tile_id; } }
2517 pub fn tile_count(&self, tile_id: u32) -> usize { self.tiles.iter().filter(|t| t.tile_id == tile_id).count() }
2518 pub fn non_empty_count(&self) -> usize { self.tiles.iter().filter(|t| t.tile_id != 0).count() }
2519}
2520
2521impl TileRect {
2522 pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { Self { x, y, width: w, height: h } }
2523 pub fn contains(&self, x: i32, y: i32) -> bool { x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height }
2524 pub fn intersects(&self, other: &TileRect) -> bool { self.x < other.x + other.width && self.x + self.width > other.x && self.y < other.y + other.height && self.y + self.height > other.y }
2525 pub fn area(&self) -> i32 { self.width * self.height }
2526}
2527
2528impl TileClipboard {
2529 pub fn flip_horizontal(&mut self) { for row in &mut self.tiles { row.reverse(); } }
2530 pub fn flip_vertical(&mut self) { self.tiles.reverse(); }
2531}
2532
2533impl MapEditorSelection {
2534 pub fn new() -> Self { Self { selection: None, clipboard: TileClipboard { tiles: Vec::new(), width: 0, height: 0 }, is_active: false } }
2535 pub fn select(&mut self, rect: TileRect) { self.selection = Some(rect); self.is_active = true; }
2536 pub fn selection_area(&self) -> i32 { self.selection.as_ref().map(|r| r.width * r.height).unwrap_or(0) }
2537 pub fn has_clipboard(&self) -> bool { !self.clipboard.tiles.is_empty() }
2538 pub fn copy(&mut self, layer: &MapLayer) {
2539 if let Some(rect) = &self.selection {
2540 let (w, h, sx, sy) = (rect.width as usize, rect.height as usize, rect.x as usize, rect.y as usize);
2541 self.clipboard = TileClipboard { width: w, height: h, tiles: (0..h).map(|dy| (0..w).map(|dx| layer.tile_at(sx+dx, sy+dy)).collect()).collect() };
2542 }
2543 }
2544 pub fn paste(&self, layer: &mut MapLayer, tx: usize, ty: usize) {
2545 for (dy, row) in self.clipboard.tiles.iter().enumerate() {
2546 for (dx, &tile_id) in row.iter().enumerate() { layer.set_tile(tx+dx, ty+dy, tile_id); }
2547 }
2548 }
2549}
2550
2551impl TilePainter {
2552 pub fn new() -> Self { Self { active_tool: PaintTool::Pencil, active_tile_id: 0, brush_size: 1, layer_name: String::new(), paint_history: Vec::new() } }
2553 pub fn paint_at(&mut self, layer: &mut MapLayer, x: usize, y: usize) {
2554 let old = layer.tile_at(x, y);
2555 layer.set_tile(x, y, self.active_tile_id);
2556 self.paint_history.push((x as i32, y as i32, old, self.active_tile_id));
2557 }
2558 pub fn paint_line(&mut self, layer: &mut MapLayer, x0: usize, y0: usize, x1: usize, y1: usize) {
2559 let steps = ((x1 as i32 - x0 as i32).abs().max((y1 as i32 - y0 as i32).abs())) as usize + 1;
2560 for i in 0..=steps { let t = if steps == 0 { 0.0 } else { i as f32 / steps as f32 }; let x = (x0 as f32 + t * (x1 as f32 - x0 as f32)) as usize; let y = (y0 as f32 + t * (y1 as f32 - y0 as f32)) as usize; self.paint_at(layer, x, y); }
2561 }
2562 pub fn paint_box(&mut self, layer: &mut MapLayer, rect: &TileRect) {
2563 for dy in 0..rect.height as usize { for dx in 0..rect.width as usize { self.paint_at(layer, rect.x as usize+dx, rect.y as usize+dy); } }
2564 }
2565 pub fn history_count(&self) -> usize { self.paint_history.len() }
2566 pub fn clear_history(&mut self) { self.paint_history.clear(); }
2567}
2568
2569pub fn run_clipboard_tests() {
2570 let mut layer = MapLayer::new("test", LayerType::Background, 20, 20);
2571 for y in 0..5 { for x in 0..5 { layer.set_tile(x, y, (y * 5 + x + 1) as u32); } }
2572
2573 let mut sel = MapEditorSelection::new();
2574 sel.select(TileRect::new(0, 0, 5, 5));
2575 assert_eq!(sel.selection_area(), 25);
2576 sel.copy(&layer);
2577 assert!(sel.has_clipboard());
2578
2579 let mut target = MapLayer::new("target", LayerType::Background, 20, 20);
2580 sel.paste(&mut target, 5, 5);
2581 assert_eq!(target.tile_at(5, 5), layer.tile_at(0, 0));
2582
2583 let mut cb = sel.clipboard.clone();
2585 let original_00 = cb.tiles[0][0];
2586 cb.flip_horizontal();
2587 assert_ne!(cb.tiles[0][0], original_00);
2588
2589 let rect = TileRect::new(2, 2, 4, 4);
2590 assert!(rect.contains(3, 3));
2591 assert!(!rect.contains(0, 0));
2592 let r2 = TileRect::new(4, 4, 4, 4);
2593 assert!(rect.intersects(&r2));
2594}
2595
2596pub fn run_painter_tests() {
2597 let mut layer = MapLayer::new("paint", LayerType::Background, 20, 20);
2598 let mut painter = TilePainter::new();
2599 painter.active_tile_id = 5;
2600 painter.paint_at(&mut layer, 5, 5);
2601 assert_eq!(layer.tile_at(5, 5), 5);
2602 painter.paint_line(&mut layer, 0, 0, 10, 10);
2603 assert_eq!(layer.tile_at(0, 0), 5);
2604 assert_eq!(layer.tile_at(10, 10), 5);
2605 painter.paint_box(&mut layer, &TileRect::new(1, 1, 3, 3));
2606 assert_eq!(layer.tile_at(1, 1), 5);
2607 assert!(painter.history_count() > 0);
2608 painter.clear_history();
2609 assert_eq!(painter.history_count(), 0);
2610}
2611
2612pub fn run_undo_history_tests() {
2613 let mut history = MapUndoHistory::new(50);
2614 assert!(!history.can_undo());
2615 let mut action = MapUndoAction::new("paint tiles");
2616 action.add_change(5, 5, "terrain", 0, 1);
2617 action.add_change(6, 5, "terrain", 0, 1);
2618 history.push(action);
2619 assert!(history.can_undo());
2620 let undone = history.undo();
2621 assert!(undone.is_some());
2622 assert!(history.can_redo());
2623 let redone = history.redo();
2624 assert!(redone.is_some());
2625 assert!(!history.can_redo());
2626}
2627
2628pub fn map_editor_final() {
2629 map_editor_complete_suite();
2630 run_clipboard_tests();
2631 run_painter_tests();
2632 run_undo_history_tests();
2633 let info = map_editor_module_info();
2634 assert_eq!(info["module"], "map_editor");
2635}
2636
2637#[derive(Debug, Clone, PartialEq)]
2642pub enum BiomeType {
2643 Grassland, Forest, Desert, Tundra, Swamp, Volcanic, Ocean, Mountains,
2644 Jungle, Savanna, Taiga, Cave, Underground, Ruins, Magical,
2645}
2646
2647#[derive(Debug, Clone)]
2648pub struct BiomeDefinition {
2649 pub biome_type: BiomeType,
2650 pub name: String,
2651 pub base_floor_tile: u32,
2652 pub base_wall_tile: u32,
2653 pub base_water_tile: u32,
2654 pub deco_tiles: Vec<u32>,
2655 pub enemy_table_id: u32,
2656 pub ambient_color: [u8; 3],
2657 pub fog_density: f32,
2658 pub temperature_c: f32,
2659 pub precipitation_mm: f32,
2660}
2661
2662impl BiomeDefinition {
2663 pub fn grassland() -> Self {
2664 BiomeDefinition { biome_type: BiomeType::Grassland, name: "Grassland".into(),
2665 base_floor_tile: 1, base_wall_tile: 10, base_water_tile: 20,
2666 deco_tiles: vec![30, 31, 32, 33], enemy_table_id: 1,
2667 ambient_color: [180, 220, 140], fog_density: 0.05, temperature_c: 18.0, precipitation_mm: 600.0 }
2668 }
2669
2670 pub fn desert() -> Self {
2671 BiomeDefinition { biome_type: BiomeType::Desert, name: "Desert".into(),
2672 base_floor_tile: 5, base_wall_tile: 15, base_water_tile: 20,
2673 deco_tiles: vec![40, 41, 42], enemy_table_id: 2,
2674 ambient_color: [240, 210, 150], fog_density: 0.02, temperature_c: 38.0, precipitation_mm: 50.0 }
2675 }
2676
2677 pub fn cave() -> Self {
2678 BiomeDefinition { biome_type: BiomeType::Cave, name: "Cave".into(),
2679 base_floor_tile: 8, base_wall_tile: 18, base_water_tile: 21,
2680 deco_tiles: vec![50, 51, 52, 53, 54], enemy_table_id: 3,
2681 ambient_color: [80, 80, 100], fog_density: 0.2, temperature_c: 10.0, precipitation_mm: 0.0 }
2682 }
2683
2684 pub fn is_cold(&self) -> bool { self.temperature_c < 0.0 }
2685 pub fn is_hot(&self) -> bool { self.temperature_c > 30.0 }
2686 pub fn is_wet(&self) -> bool { self.precipitation_mm > 1000.0 }
2687 pub fn is_dark(&self) -> bool { self.fog_density > 0.15 }
2688
2689 pub fn movement_modifier(&self) -> f32 {
2690 match self.biome_type {
2691 BiomeType::Swamp => 0.6,
2692 BiomeType::Desert => 0.85,
2693 BiomeType::Mountains => 0.7,
2694 BiomeType::Forest => 0.9,
2695 _ => 1.0,
2696 }
2697 }
2698
2699 pub fn survival_difficulty(&self) -> u8 {
2700 match self.biome_type {
2701 BiomeType::Volcanic | BiomeType::Swamp => 5,
2702 BiomeType::Desert | BiomeType::Tundra => 4,
2703 BiomeType::Mountains | BiomeType::Cave => 3,
2704 BiomeType::Forest | BiomeType::Jungle => 2,
2705 _ => 1,
2706 }
2707 }
2708}
2709
2710#[derive(Debug, Clone)]
2711pub struct BiomeMap {
2712 pub width: u32,
2713 pub height: u32,
2714 pub biome_ids: Vec<u8>,
2715 pub biome_registry: Vec<BiomeDefinition>,
2716}
2717
2718impl BiomeMap {
2719 pub fn new(width: u32, height: u32) -> Self {
2720 BiomeMap { width, height, biome_ids: vec![0; (width * height) as usize], biome_registry: Vec::new() }
2721 }
2722
2723 pub fn register_biome(&mut self, biome: BiomeDefinition) { self.biome_registry.push(biome); }
2724
2725 fn idx(&self, x: u32, y: u32) -> Option<usize> {
2726 if x < self.width && y < self.height { Some((y * self.width + x) as usize) } else { None }
2727 }
2728
2729 pub fn set_biome(&mut self, x: u32, y: u32, biome_id: u8) {
2730 if let Some(i) = self.idx(x, y) { self.biome_ids[i] = biome_id; }
2731 }
2732
2733 pub fn biome_at(&self, x: u32, y: u32) -> Option<&BiomeDefinition> {
2734 let id = self.idx(x, y).map(|i| self.biome_ids[i] as usize).unwrap_or(0);
2735 self.biome_registry.get(id)
2736 }
2737
2738 pub fn biome_coverage(&self, biome_id: u8) -> f32 {
2739 let count = self.biome_ids.iter().filter(|&&b| b == biome_id).count();
2740 count as f32 / self.biome_ids.len() as f32 * 100.0
2741 }
2742}
2743
2744#[derive(Debug, Clone, PartialEq)]
2749pub enum EncounterTriggerType { OnEnter, OnStep, OnTime, OnLowHealth, OnCombatEnd }
2750
2751#[derive(Debug, Clone)]
2752pub struct EncounterGroup {
2753 pub group_id: u32,
2754 pub enemy_ids: Vec<u32>,
2755 pub level_range: (u32, u32),
2756 pub formation_radius: f32,
2757 pub is_elite: bool,
2758 pub loot_table_id: u32,
2759}
2760
2761impl EncounterGroup {
2762 pub fn new(id: u32, enemies: Vec<u32>, level_min: u32, level_max: u32) -> Self {
2763 EncounterGroup { group_id: id, enemy_ids: enemies, level_range: (level_min, level_max),
2764 formation_radius: 2.0, is_elite: false, loot_table_id: 1 }
2765 }
2766
2767 pub fn enemy_count(&self) -> usize { self.enemy_ids.len() }
2768 pub fn is_boss_group(&self) -> bool { self.enemy_count() == 1 && self.is_elite }
2769 pub fn xp_reward(&self) -> u32 { self.enemy_ids.len() as u32 * (self.level_range.1 + self.level_range.0) * 25 }
2770}
2771
2772#[derive(Debug, Clone)]
2773pub struct EncounterZoneRecord {
2774 pub zone_id: u32,
2775 pub bounds: (i32, i32, i32, i32), pub trigger_type: EncounterTriggerType,
2777 pub encounter_groups: Vec<EncounterGroup>,
2778 pub respawn_time_s: f32,
2779 pub last_cleared_time: f32,
2780 pub difficulty_scale: f32,
2781}
2782
2783impl EncounterZoneRecord {
2784 pub fn new(id: u32, x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
2785 EncounterZoneRecord { zone_id: id, bounds: (x1, y1, x2, y2),
2786 trigger_type: EncounterTriggerType::OnEnter,
2787 encounter_groups: Vec::new(), respawn_time_s: 300.0,
2788 last_cleared_time: -999.0, difficulty_scale: 1.0 }
2789 }
2790
2791 pub fn contains(&self, x: i32, y: i32) -> bool {
2792 x >= self.bounds.0 && y >= self.bounds.1 && x <= self.bounds.2 && y <= self.bounds.3
2793 }
2794
2795 pub fn add_group(&mut self, group: EncounterGroup) { self.encounter_groups.push(group); }
2796
2797 pub fn can_respawn(&self, current_time: f32) -> bool {
2798 current_time - self.last_cleared_time >= self.respawn_time_s
2799 }
2800
2801 pub fn total_enemy_count(&self) -> usize { self.encounter_groups.iter().map(|g| g.enemy_count()).sum() }
2802
2803 pub fn total_xp_reward(&self) -> u32 {
2804 (self.encounter_groups.iter().map(|g| g.xp_reward()).sum::<u32>() as f32 * self.difficulty_scale) as u32
2805 }
2806}
2807
2808#[derive(Debug, Clone)]
2813pub struct MapRegionRecord {
2814 pub region_id: u32,
2815 pub name: String,
2816 pub bounds: (i32, i32, i32, i32),
2817 pub region_type: BiomeType,
2818 pub connections: Vec<u32>,
2819 pub level_range: (u32, u32),
2820 pub is_unlocked: bool,
2821 pub discovery_flags: HashSet<String>,
2822}
2823
2824impl MapRegionRecord {
2825 pub fn new(id: u32, name: &str, bounds: (i32, i32, i32, i32), region_type: BiomeType) -> Self {
2826 MapRegionRecord { region_id: id, name: name.to_string(), bounds, region_type,
2827 connections: Vec::new(), level_range: (1, 10), is_unlocked: false, discovery_flags: HashSet::new() }
2828 }
2829
2830 pub fn connect_to(&mut self, other_id: u32) { if !self.connections.contains(&other_id) { self.connections.push(other_id); } }
2831 pub fn unlock(&mut self) { self.is_unlocked = true; }
2832 pub fn discover(&mut self, flag: &str) { self.discovery_flags.insert(flag.to_string()); }
2833 pub fn is_discovered(&self, flag: &str) -> bool { self.discovery_flags.contains(flag) }
2834 pub fn connection_count(&self) -> usize { self.connections.len() }
2835 pub fn contains(&self, x: i32, y: i32) -> bool {
2836 x >= self.bounds.0 && y >= self.bounds.1 && x <= self.bounds.2 && y <= self.bounds.3
2837 }
2838}
2839
2840#[derive(Debug, Clone)]
2841pub struct WorldRegionGraph {
2842 pub regions: Vec<MapRegionRecord>,
2843}
2844
2845impl WorldRegionGraph {
2846 pub fn new() -> Self { WorldRegionGraph { regions: Vec::new() } }
2847 pub fn add_region(&mut self, r: MapRegionRecord) { self.regions.push(r); }
2848 pub fn region_by_id(&self, id: u32) -> Option<&MapRegionRecord> { self.regions.iter().find(|r| r.region_id == id) }
2849 pub fn region_by_id_mut(&mut self, id: u32) -> Option<&mut MapRegionRecord> { self.regions.iter_mut().find(|r| r.region_id == id) }
2850 pub fn region_at(&self, x: i32, y: i32) -> Option<&MapRegionRecord> { self.regions.iter().find(|r| r.contains(x, y)) }
2851 pub fn unlocked_regions(&self) -> Vec<&MapRegionRecord> { self.regions.iter().filter(|r| r.is_unlocked).collect() }
2852
2853 pub fn bfs_path(&self, start: u32, goal: u32) -> Option<Vec<u32>> {
2854 let mut visited = HashSet::new();
2855 let mut queue = VecDeque::new();
2856 queue.push_back(vec![start]);
2857 while let Some(path) = queue.pop_front() {
2858 let &last = path.last().unwrap();
2859 if last == goal { return Some(path); }
2860 if visited.contains(&last) { continue; }
2861 visited.insert(last);
2862 if let Some(region) = self.region_by_id(last) {
2863 for &neighbor in ®ion.connections {
2864 if !visited.contains(&neighbor) {
2865 let mut new_path = path.clone();
2866 new_path.push(neighbor);
2867 queue.push_back(new_path);
2868 }
2869 }
2870 }
2871 }
2872 None
2873 }
2874
2875 pub fn is_connected(&self, a: u32, b: u32) -> bool { self.bfs_path(a, b).is_some() }
2876 pub fn region_count(&self) -> usize { self.regions.len() }
2877}
2878
2879#[derive(Debug, Clone, PartialEq)]
2884pub enum DungeonRoomType2 { Entrance, Exit, Combat, Treasure, Boss, Corridor, Shop, Rest, Puzzle, Secret }
2885
2886#[derive(Debug, Clone)]
2887pub struct DungeonRoom2 {
2888 pub id: u32, pub x: i32, pub y: i32, pub width: i32, pub height: i32,
2889 pub room_type: DungeonRoomType2, pub connections: Vec<u32>, pub loot_level: u8, pub is_cleared: bool,
2890}
2891
2892impl DungeonRoom2 {
2893 pub fn new(id: u32, x: i32, y: i32, w: i32, h: i32, rt: DungeonRoomType2) -> Self {
2894 DungeonRoom2 { id, x, y, width: w, height: h, room_type: rt, connections: Vec::new(), loot_level: 1, is_cleared: false }
2895 }
2896 pub fn center(&self) -> (i32, i32) { (self.x + self.width/2, self.y + self.height/2) }
2897 pub fn area(&self) -> i32 { self.width * self.height }
2898 pub fn overlaps(&self, o: &DungeonRoom2) -> bool { self.x < o.x+o.width && self.x+self.width > o.x && self.y < o.y+o.height && self.y+self.height > o.y }
2899 pub fn dist_to(&self, o: &DungeonRoom2) -> f32 { let (ax,ay) = self.center(); let (bx,by) = o.center(); (((bx-ax).pow(2)+(by-ay).pow(2)) as f32).sqrt() }
2900 pub fn connect(&mut self, oid: u32) { if !self.connections.contains(&oid) { self.connections.push(oid); } }
2901}
2902
2903#[derive(Debug, Clone)]
2904pub struct DungeonGen2 {
2905 pub width: i32, pub height: i32, pub seed: u64,
2906 pub min_room: i32, pub max_room: i32, pub max_rooms: u32,
2907}
2908
2909impl DungeonGen2 {
2910 pub fn new(w: i32, h: i32, seed: u64) -> Self { DungeonGen2 { width: w, height: h, seed, min_room: 4, max_room: 12, max_rooms: 20 } }
2911
2912 fn rng(&self, n: u64) -> u64 { self.seed.wrapping_mul(6364136223846793005).wrapping_add(n).wrapping_mul(1442695040888963407) >> 33 }
2913
2914 pub fn generate(&self) -> Vec<DungeonRoom2> {
2915 let mut rooms = Vec::new();
2916 let mut attempt = 0u64;
2917 while rooms.len() < self.max_rooms as usize && attempt < 200 {
2918 let w = (self.rng(attempt) % (self.max_room - self.min_room) as u64 + self.min_room as u64) as i32;
2919 let h = (self.rng(attempt+1) % (self.max_room - self.min_room) as u64 + self.min_room as u64) as i32;
2920 let x = (self.rng(attempt+2) % (self.width - w - 2) as u64 + 1) as i32;
2921 let y = (self.rng(attempt+3) % (self.height - h - 2) as u64 + 1) as i32;
2922 let room = DungeonRoom2::new(rooms.len() as u32, x, y, w, h, DungeonRoomType2::Combat);
2923 if !rooms.iter().any(|r: &DungeonRoom2| r.overlaps(&room)) { rooms.push(room); }
2924 attempt += 4;
2925 }
2926 if !rooms.is_empty() { rooms[0].room_type = DungeonRoomType2::Entrance; }
2927 if rooms.len() > 1 { rooms.last_mut().unwrap().room_type = DungeonRoomType2::Exit; }
2928 if rooms.len() > 3 { let mid = rooms.len()/2; rooms[mid].room_type = DungeonRoomType2::Treasure; }
2929 self.connect_mst(&mut rooms);
2930 self.mark_boss(&mut rooms);
2931 rooms
2932 }
2933
2934 fn connect_mst(&self, rooms: &mut Vec<DungeonRoom2>) {
2935 let n = rooms.len(); if n == 0 { return; }
2936 let mut in_mst = vec![false; n]; in_mst[0] = true;
2937 for _ in 0..n-1 {
2938 let (mut ba, mut bb, mut bd) = (0, 0, f32::MAX);
2939 for a in 0..n { if !in_mst[a] { continue; } for b in 0..n { if in_mst[b] { continue; } let d = rooms[a].dist_to(&rooms[b]); if d < bd { bd = d; ba = a; bb = b; } } }
2940 let (ida, idb) = (rooms[ba].id, rooms[bb].id);
2941 rooms[ba].connect(idb); rooms[bb].connect(ida); in_mst[bb] = true;
2942 }
2943 }
2944
2945 fn mark_boss(&self, rooms: &mut Vec<DungeonRoom2>) {
2946 if rooms.len() < 4 { return; }
2947 let (ex, ey) = rooms[0].center();
2948 let idx = (1..rooms.len()).max_by(|&a, &b| {
2949 let (ax,ay) = rooms[a].center(); let (bx,by) = rooms[b].center();
2950 let da = ((ax-ex).pow(2)+(ay-ey).pow(2)) as f32;
2951 let db = ((bx-ex).pow(2)+(by-ey).pow(2)) as f32;
2952 da.partial_cmp(&db).unwrap()
2953 }).unwrap_or(1);
2954 rooms[idx].room_type = DungeonRoomType2::Boss; rooms[idx].loot_level = 5;
2955 }
2956}
2957
2958#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2963pub struct TileCoord { pub x: i32, pub y: i32 }
2964
2965impl TileCoord {
2966 pub fn new(x: i32, y: i32) -> Self { TileCoord { x, y } }
2967 pub fn manhattan(&self, o: &TileCoord) -> i32 { (self.x-o.x).abs() + (self.y-o.y).abs() }
2968 pub fn octile(&self, o: &TileCoord) -> f32 { let dx=(self.x-o.x).abs() as f32; let dy=(self.y-o.y).abs() as f32; dx+dy-(2.0-std::f32::consts::SQRT_2)*dx.min(dy) }
2969 pub fn neighbors4(&self) -> [TileCoord; 4] { [TileCoord::new(self.x+1,self.y),TileCoord::new(self.x-1,self.y),TileCoord::new(self.x,self.y+1),TileCoord::new(self.x,self.y-1)] }
2970 pub fn neighbors8(&self) -> [TileCoord; 8] { [TileCoord::new(self.x+1,self.y),TileCoord::new(self.x-1,self.y),TileCoord::new(self.x,self.y+1),TileCoord::new(self.x,self.y-1),TileCoord::new(self.x+1,self.y+1),TileCoord::new(self.x-1,self.y+1),TileCoord::new(self.x+1,self.y-1),TileCoord::new(self.x-1,self.y-1)] }
2971}
2972
2973#[derive(Debug, Clone)]
2974pub struct PathGrid {
2975 pub width: i32, pub height: i32, pub passable: Vec<bool>, pub costs: Vec<f32>,
2976}
2977
2978impl PathGrid {
2979 pub fn new(w: i32, h: i32) -> Self { PathGrid { width: w, height: h, passable: vec![true; (w*h) as usize], costs: vec![1.0; (w*h) as usize] } }
2980 fn idx(&self, x: i32, y: i32) -> Option<usize> { if x>=0&&y>=0&&x<self.width&&y<self.height { Some((y*self.width+x) as usize) } else { None } }
2981 pub fn set_passable(&mut self, x: i32, y: i32, p: bool) { if let Some(i) = self.idx(x,y) { self.passable[i]=p; } }
2982 pub fn is_passable(&self, c: &TileCoord) -> bool { self.idx(c.x,c.y).map(|i|self.passable[i]).unwrap_or(false) }
2983 pub fn cost_at(&self, c: &TileCoord) -> f32 { self.idx(c.x,c.y).map(|i|self.costs[i]).unwrap_or(f32::MAX) }
2984
2985 pub fn astar(&self, start: TileCoord, goal: TileCoord, diagonal: bool) -> Option<Vec<TileCoord>> {
2986 use std::collections::BinaryHeap;
2987 #[derive(PartialEq)] struct N { pos: TileCoord, f: f32 }
2988 impl Eq for N {}
2989 impl PartialOrd for N { fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(o)) } }
2990 impl Ord for N { fn cmp(&self, o: &Self) -> std::cmp::Ordering { o.f.partial_cmp(&self.f).unwrap_or(std::cmp::Ordering::Equal) } }
2991 let mut g: HashMap<TileCoord, f32> = HashMap::new();
2992 let mut from: HashMap<TileCoord, TileCoord> = HashMap::new();
2993 let mut open = BinaryHeap::new();
2994 g.insert(start.clone(), 0.0);
2995 open.push(N { pos: start.clone(), f: start.octile(&goal) });
2996 while let Some(N { pos: cur, .. }) = open.pop() {
2997 if cur == goal {
2998 let mut path = vec![goal.clone()]; let mut p = goal.clone();
2999 while let Some(prev) = from.get(&p) { path.push(prev.clone()); p = prev.clone(); }
3000 path.reverse(); return Some(path);
3001 }
3002 let nbrs: Vec<TileCoord> = if diagonal { cur.neighbors8().to_vec() } else { cur.neighbors4().to_vec() };
3003 for nb in nbrs {
3004 if !self.is_passable(&nb) { continue; }
3005 let dx=(nb.x-cur.x).abs(); let dy=(nb.y-cur.y).abs();
3006 let mc = if dx+dy==2 { std::f32::consts::SQRT_2 } else { 1.0 };
3007 let tg = g.get(&cur).copied().unwrap_or(f32::MAX) + mc * self.cost_at(&nb);
3008 if tg < g.get(&nb).copied().unwrap_or(f32::MAX) {
3009 g.insert(nb.clone(), tg); from.insert(nb.clone(), cur.clone());
3010 open.push(N { pos: nb.clone(), f: tg + nb.octile(&goal) });
3011 }
3012 }
3013 }
3014 None
3015 }
3016}
3017
3018#[derive(Debug, Clone, PartialEq)]
3023pub enum MapEventKind { Enter, Exit, Spawn, Kill, Open, Trap, Transition, Quest, Secret, Boss }
3024
3025#[derive(Debug, Clone)]
3026pub struct MapEventRecord {
3027 pub id: u32, pub kind: MapEventKind, pub x: i32, pub y: i32,
3028 pub data: HashMap<String, String>, pub fired: bool, pub cooldown: f32, pub last_fire: f32,
3029}
3030
3031impl MapEventRecord {
3032 pub fn new(id: u32, kind: MapEventKind, x: i32, y: i32) -> Self {
3033 MapEventRecord { id, kind, x, y, data: HashMap::new(), fired: false, cooldown: 0.0, last_fire: -999.0 }
3034 }
3035 pub fn can_fire(&self, t: f32) -> bool { !self.fired || (self.cooldown > 0.0 && t - self.last_fire >= self.cooldown) }
3036 pub fn fire(&mut self, t: f32) { self.fired = true; self.last_fire = t; }
3037 pub fn at(&self, x: i32, y: i32) -> bool { self.x == x && self.y == y }
3038}
3039
3040#[derive(Debug, Clone)]
3041pub struct MapEventRegistry {
3042 pub events: Vec<MapEventRecord>, pub time: f32,
3043}
3044
3045impl MapEventRegistry {
3046 pub fn new() -> Self { MapEventRegistry { events: Vec::new(), time: 0.0 } }
3047 pub fn add(&mut self, e: MapEventRecord) { self.events.push(e); }
3048 pub fn trigger_at(&mut self, x: i32, y: i32) -> Vec<u32> {
3049 let t = self.time; let mut fired = Vec::new();
3050 for e in &mut self.events { if e.at(x,y) && e.can_fire(t) { e.fire(t); fired.push(e.id); } }
3051 fired
3052 }
3053 pub fn reset_all(&mut self) { for e in &mut self.events { e.fired = false; } }
3054 pub fn of_kind(&self, k: &MapEventKind) -> Vec<&MapEventRecord> { self.events.iter().filter(|e| &e.kind == k).collect() }
3055}
3056
3057#[derive(Debug, Clone)]
3062pub struct MinimapTile { pub explored: bool, pub visible: bool, pub tile_type: u8 }
3063
3064#[derive(Debug, Clone)]
3065pub struct MinimapView {
3066 pub width: u32, pub height: u32, pub tiles: Vec<MinimapTile>, pub scale: u32,
3067}
3068
3069impl MinimapView {
3070 pub fn new(w: u32, h: u32, scale: u32) -> Self { MinimapView { width: w, height: h, tiles: vec![MinimapTile{explored:false,visible:false,tile_type:0}; (w*h) as usize], scale } }
3071 fn idx(&self, x: u32, y: u32) -> Option<usize> { if x<self.width&&y<self.height { Some((y*self.width+x) as usize) } else { None } }
3072 pub fn reveal(&mut self, x: u32, y: u32) { if let Some(i) = self.idx(x,y) { self.tiles[i].explored = true; self.tiles[i].visible = true; } }
3073 pub fn hide(&mut self, x: u32, y: u32) { if let Some(i) = self.idx(x,y) { self.tiles[i].visible = false; } }
3074 pub fn is_explored(&self, x: u32, y: u32) -> bool { self.idx(x,y).map(|i|self.tiles[i].explored).unwrap_or(false) }
3075 pub fn explored_count(&self) -> usize { self.tiles.iter().filter(|t|t.explored).count() }
3076 pub fn exploration_pct(&self) -> f32 { self.explored_count() as f32 / self.tiles.len() as f32 * 100.0 }
3077 pub fn reveal_radius(&mut self, cx: i32, cy: i32, r: i32) {
3078 for dy in -r..=r { for dx in -r..=r { if dx*dx+dy*dy<=r*r { let x=cx+dx; let y=cy+dy; if x>=0&&y>=0&&(x as u32)<self.width&&(y as u32)<self.height { self.reveal(x as u32, y as u32); } } } }
3079 }
3080}
3081
3082#[derive(Debug, Clone, PartialEq)]
3087pub enum PatrolStatus { Idle, Walking, Alert, Searching, Returning }
3088
3089#[derive(Debug, Clone)]
3090pub struct PatrolPoint { pub x: i32, pub y: i32, pub wait_s: f32 }
3091
3092impl PatrolPoint {
3093 pub fn new(x: i32, y: i32) -> Self { PatrolPoint { x, y, wait_s: 0.0 } }
3094 pub fn with_wait(mut self, t: f32) -> Self { self.wait_s = t; self }
3095}
3096
3097#[derive(Debug, Clone)]
3098pub struct NpcPatrol {
3099 pub id: u32, pub waypoints: Vec<PatrolPoint>, pub current_wp: usize,
3100 pub status: PatrolStatus, pub pos: (f32, f32), pub speed: f32,
3101 pub alert_radius: f32, pub search_timer: f32, pub home: (f32, f32),
3102}
3103
3104impl NpcPatrol {
3105 pub fn new(id: u32, pos: (f32, f32), speed: f32) -> Self {
3106 NpcPatrol { id, waypoints: Vec::new(), current_wp: 0, status: PatrolStatus::Idle, pos, speed, alert_radius: 5.0, search_timer: 0.0, home: pos }
3107 }
3108 pub fn add_waypoint(&mut self, p: PatrolPoint) { self.waypoints.push(p); }
3109 pub fn alert(&mut self) { if self.status == PatrolStatus::Idle || self.status == PatrolStatus::Walking { self.status = PatrolStatus::Alert; } }
3110
3111 pub fn update(&mut self, dt: f32) {
3112 match self.status {
3113 PatrolStatus::Idle => { if !self.waypoints.is_empty() { self.status = PatrolStatus::Walking; } },
3114 PatrolStatus::Walking => {
3115 if self.waypoints.is_empty() { return; }
3116 let wp = &self.waypoints[self.current_wp];
3117 let (tx, ty) = (wp.x as f32, wp.y as f32);
3118 let (px, py) = self.pos;
3119 let dx = tx-px; let dy = ty-py;
3120 let dist = (dx*dx+dy*dy).sqrt();
3121 if dist < 0.1 { self.current_wp = (self.current_wp+1)%self.waypoints.len(); }
3122 else { let s=self.speed*dt; self.pos=(px+dx/dist*s, py+dy/dist*s); }
3123 },
3124 PatrolStatus::Alert => { self.search_timer = 5.0; self.status = PatrolStatus::Searching; },
3125 PatrolStatus::Searching => { self.search_timer -= dt; if self.search_timer <= 0.0 { self.status = PatrolStatus::Returning; } },
3126 PatrolStatus::Returning => {
3127 let (hx,hy) = self.home; let (px,py) = self.pos;
3128 let dx=hx-px; let dy=hy-py; let dist=(dx*dx+dy*dy).sqrt();
3129 if dist < 0.1 { self.status = PatrolStatus::Walking; }
3130 else { let s=self.speed*dt; self.pos=(px+dx/dist*s, py+dy/dist*s); }
3131 },
3132 }
3133 }
3134 pub fn detect(&self, px: f32, py: f32) -> bool { let dx=self.pos.0-px; let dy=self.pos.1-py; (dx*dx+dy*dy).sqrt()<=self.alert_radius }
3135}
3136
3137#[derive(Debug, Clone)]
3142pub struct MapNote {
3143 pub id: u32, pub x: i32, pub y: i32, pub text: String, pub color: [u8;3], pub visible: bool,
3144}
3145
3146impl MapNote {
3147 pub fn new(id: u32, x: i32, y: i32, text: &str) -> Self { MapNote { id, x, y, text: text.to_string(), color: [255,255,0], visible: true } }
3148 pub fn warning(id: u32, x: i32, y: i32, text: &str) -> Self { let mut n = Self::new(id,x,y,text); n.color=[255,80,0]; n }
3149 pub fn secret(id: u32, x: i32, y: i32, text: &str) -> Self { let mut n = Self::new(id,x,y,text); n.color=[160,0,255]; n }
3150}
3151
3152#[derive(Debug, Clone)]
3153pub struct NoteLayer {
3154 pub notes: Vec<MapNote>,
3155}
3156
3157impl NoteLayer {
3158 pub fn new() -> Self { NoteLayer { notes: Vec::new() } }
3159 pub fn add(&mut self, n: MapNote) { self.notes.push(n); }
3160 pub fn remove(&mut self, id: u32) { self.notes.retain(|n|n.id!=id); }
3161 pub fn at(&self, x: i32, y: i32) -> Vec<&MapNote> { self.notes.iter().filter(|n|n.x==x&&n.y==y).collect() }
3162 pub fn visible_count(&self) -> usize { self.notes.iter().filter(|n|n.visible).count() }
3163 pub fn search(&self, q: &str) -> Vec<&MapNote> { self.notes.iter().filter(|n|n.text.contains(q)).collect() }
3164 pub fn export(&self) -> String { self.notes.iter().filter(|n|n.visible).map(|n|format!("[{},{}] {}",n.x,n.y,n.text)).collect::<Vec<_>>().join("\n") }
3165}
3166
3167pub fn wang_blob(n: bool, e: bool, s: bool, w: bool, ne: bool, se: bool, sw: bool, nw: bool) -> u8 {
3172 let mut idx = 0u8;
3173 if n { idx|=1; } if e { idx|=2; } if s { idx|=4; } if w { idx|=8; }
3174 if nw&&n&&w { idx|=16; } if ne&&n&&e { idx|=32; } if se&&s&&e { idx|=64; } if sw&&s&&w { idx|=128; }
3175 idx
3176}
3177
3178#[derive(Debug, Clone)]
3179pub struct AutotileGrid {
3180 pub width: i32, pub height: i32, pub solid: Vec<bool>,
3181}
3182
3183impl AutotileGrid {
3184 pub fn new(w: i32, h: i32) -> Self { AutotileGrid { width: w, height: h, solid: vec![false; (w*h) as usize] } }
3185 fn idx(&self, x: i32, y: i32) -> Option<usize> { if x>=0&&y>=0&&x<self.width&&y<self.height { Some((y*self.width+x) as usize) } else { None } }
3186 pub fn set(&mut self, x: i32, y: i32, s: bool) { if let Some(i) = self.idx(x,y) { self.solid[i]=s; } }
3187 pub fn is_solid(&self, x: i32, y: i32) -> bool { self.idx(x,y).map(|i|self.solid[i]).unwrap_or(false) }
3188 pub fn fill_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) { for y in y1..=y2 { for x in x1..=x2 { self.set(x,y,true); } } }
3189 pub fn tile_at(&self, x: i32, y: i32) -> u8 {
3190 if !self.is_solid(x,y) { return 0; }
3191 wang_blob(self.is_solid(x,y-1),self.is_solid(x+1,y),self.is_solid(x,y+1),self.is_solid(x-1,y),self.is_solid(x+1,y-1),self.is_solid(x+1,y+1),self.is_solid(x-1,y+1),self.is_solid(x-1,y-1))
3192 }
3193 pub fn solid_count(&self) -> usize { self.solid.iter().filter(|&&s|s).count() }
3194}
3195
3196#[derive(Debug, Clone)]
3201pub struct LootItem { pub id: u32, pub name: String, pub weight: u32, pub min_qty: u32, pub max_qty: u32, pub min_level: u32 }
3202
3203impl LootItem {
3204 pub fn new(id: u32, name: &str, weight: u32, min: u32, max: u32) -> Self { LootItem { id, name: name.to_string(), weight, min_qty: min, max_qty: max, min_level: 1 } }
3205}
3206
3207#[derive(Debug, Clone)]
3208pub struct LootTableDef {
3209 pub id: u32, pub name: String, pub items: Vec<LootItem>, pub guaranteed: Vec<LootItem>, pub rolls_min: u32, pub rolls_max: u32,
3210}
3211
3212impl LootTableDef {
3213 pub fn new(id: u32, name: &str) -> Self { LootTableDef { id, name: name.to_string(), items: Vec::new(), guaranteed: Vec::new(), rolls_min: 1, rolls_max: 3 } }
3214 pub fn add_item(&mut self, item: LootItem) { self.items.push(item); }
3215 pub fn add_guaranteed(&mut self, item: LootItem) { self.guaranteed.push(item); }
3216 pub fn total_weight(&self) -> u32 { self.items.iter().map(|i|i.weight).sum() }
3217
3218 pub fn roll(&self, seed: u64, level: u32) -> Vec<(u32, u32)> {
3219 let mut results = Vec::new();
3220 for item in &self.guaranteed { if level >= item.min_level { results.push((item.id, item.min_qty)); } }
3221 let tw = self.total_weight(); if tw == 0 { return results; }
3222 let rolls = self.rolls_min + (seed % (self.rolls_max - self.rolls_min + 1).max(1) as u64) as u32;
3223 for i in 0..rolls {
3224 let roll = (seed.wrapping_add(i as u64 * 7919) % tw as u64) as u32;
3225 let mut acc = 0u32;
3226 for item in &self.items {
3227 acc += item.weight;
3228 if roll < acc && level >= item.min_level {
3229 let qty = item.min_qty + (seed.wrapping_add(i as u64) % (item.max_qty - item.min_qty + 1).max(1) as u64) as u32;
3230 results.push((item.id, qty)); break;
3231 }
3232 }
3233 }
3234 results
3235 }
3236
3237 pub fn goblin_loot() -> Self {
3238 let mut t = Self::new(1, "goblin"); t.rolls_min=1; t.rolls_max=3;
3239 t.add_item(LootItem::new(101,"gold_coin",50,1,10));
3240 t.add_item(LootItem::new(102,"iron_sword",20,1,1));
3241 t.add_guaranteed(LootItem::new(100,"small_key",0,1,1));
3242 t
3243 }
3244
3245 pub fn boss_loot() -> Self {
3246 let mut t = Self::new(2, "boss"); t.rolls_min=3; t.rolls_max=6;
3247 t.add_item(LootItem::new(201,"gold_coin",30,50,200));
3248 t.add_item(LootItem::new(202,"magic_sword",10,1,1));
3249 t.add_item(LootItem::new(203,"enchanted_armor",10,1,1));
3250 t.add_guaranteed(LootItem::new(200,"boss_key",0,1,1));
3251 t
3252 }
3253}
3254
3255#[derive(Debug, Clone, PartialEq)]
3260pub enum TileEffectKind { None, Damage, Heal, Slow, Haste, Burn, Poison, Freeze }
3261
3262#[derive(Debug, Clone)]
3263pub struct TileEffects {
3264 pub width: i32, pub height: i32, pub kinds: Vec<TileEffectKind>, pub params: Vec<f32>,
3265}
3266
3267impl TileEffects {
3268 pub fn new(w: i32, h: i32) -> Self { let sz=(w*h) as usize; TileEffects { width: w, height: h, kinds: vec![TileEffectKind::None;sz], params: vec![0.0;sz] } }
3269 fn idx(&self,x:i32,y:i32)->Option<usize>{if x>=0&&y>=0&&x<self.width&&y<self.height{Some((y*self.width+x) as usize)}else{None}}
3270 pub fn set(&mut self,x:i32,y:i32,k:TileEffectKind,p:f32){if let Some(i)=self.idx(x,y){self.kinds[i]=k;self.params[i]=p;}}
3271 pub fn kind_at(&self,x:i32,y:i32)->&TileEffectKind{self.idx(x,y).map(|i|&self.kinds[i]).unwrap_or(&TileEffectKind::None)}
3272 pub fn apply(&self,x:i32,y:i32,hp:&mut f32,spd:&mut f32){
3273 let p=self.idx(x,y).map(|i|self.params[i]).unwrap_or(0.0);
3274 match self.kind_at(x,y){TileEffectKind::Damage=>{*hp-=p;}TileEffectKind::Heal=>{*hp+=p;}TileEffectKind::Slow=>{*spd*=0.5;}TileEffectKind::Burn=>{*hp-=p*2.0;}TileEffectKind::Poison=>{*hp-=p*0.5;}_=>{}}
3275 }
3276}
3277
3278#[derive(Debug, Clone, PartialEq)]
3283pub enum LayerKind { Terrain, Decoration, Object, Collision, Event }
3284
3285#[derive(Debug, Clone)]
3286pub struct MapLayerDef {
3287 pub name: String, pub kind: LayerKind, pub width: i32, pub height: i32,
3288 pub data: Vec<u32>, pub visible: bool, pub opacity: f32,
3289}
3290
3291impl MapLayerDef {
3292 pub fn new(name: &str, kind: LayerKind, w: i32, h: i32) -> Self {
3293 MapLayerDef { name: name.to_string(), kind, width: w, height: h, data: vec![0;(w*h) as usize], visible: true, opacity: 1.0 }
3294 }
3295 fn idx(&self,x:i32,y:i32)->Option<usize>{if x>=0&&y>=0&&x<self.width&&y<self.height{Some((y*self.width+x) as usize)}else{None}}
3296 pub fn get(&self,x:i32,y:i32)->u32{self.idx(x,y).map(|i|self.data[i]).unwrap_or(0)}
3297 pub fn set(&mut self,x:i32,y:i32,t:u32){if let Some(i)=self.idx(x,y){self.data[i]=t;}}
3298 pub fn fill(&mut self,t:u32){for d in &mut self.data{*d=t;}}
3299 pub fn flood_fill(&mut self,sx:i32,sy:i32,new_t:u32){
3300 let old=self.get(sx,sy); if old==new_t{return;}
3301 let mut stack=vec![(sx,sy)];
3302 while let Some((x,y))=stack.pop(){
3303 if self.get(x,y)!=old{continue;} self.set(x,y,new_t);
3304 for(nx,ny) in [(x+1,y),(x-1,y),(x,y+1),(x,y-1)]{if self.get(nx,ny)==old{stack.push((nx,ny));}}
3305 }
3306 }
3307 pub fn non_empty(&self)->usize{self.data.iter().filter(|&&t|t!=0).count()}
3308}
3309
3310#[derive(Debug, Clone)]
3311pub struct LayerStack {
3312 pub layers: Vec<MapLayerDef>, pub width: i32, pub height: i32,
3313}
3314
3315impl LayerStack {
3316 pub fn new(w: i32, h: i32) -> Self { LayerStack { layers: Vec::new(), width: w, height: h } }
3317 pub fn push(&mut self, l: MapLayerDef) { self.layers.push(l); }
3318 pub fn layer(&self, name: &str) -> Option<&MapLayerDef> { self.layers.iter().find(|l|l.name==name) }
3319 pub fn layer_mut(&mut self, name: &str) -> Option<&mut MapLayerDef> { self.layers.iter_mut().find(|l|l.name==name) }
3320 pub fn composite_at(&self, x: i32, y: i32) -> u32 { for l in self.layers.iter().rev() { if !l.visible{continue;} let t=l.get(x,y); if t!=0{return t;} } 0 }
3321 pub fn visible_count(&self) -> usize { self.layers.iter().filter(|l|l.visible).count() }
3322 pub fn count(&self) -> usize { self.layers.len() }
3323
3324 pub fn standard_rpg(w: i32, h: i32) -> Self {
3325 let mut s = Self::new(w, h);
3326 s.push(MapLayerDef::new("terrain", LayerKind::Terrain, w, h));
3327 s.push(MapLayerDef::new("deco", LayerKind::Decoration, w, h));
3328 s.push(MapLayerDef::new("objects", LayerKind::Object, w, h));
3329 s.push(MapLayerDef::new("collision", LayerKind::Collision, w, h));
3330 s.push(MapLayerDef::new("events", LayerKind::Event, w, h));
3331 s
3332 }
3333}
3334
3335#[derive(Debug, Clone)]
3340pub struct MapProgress {
3341 pub map_id: u32, pub enemies_total: u32, pub enemies_killed: u32,
3342 pub chests_total: u32, pub chests_opened: u32, pub secrets_total: u32,
3343 pub secrets_found: u32, pub boss_dead: bool, pub flags: HashSet<String>,
3344}
3345
3346impl MapProgress {
3347 pub fn new(id: u32) -> Self { MapProgress { map_id: id, enemies_total: 0, enemies_killed: 0, chests_total: 0, chests_opened: 0, secrets_total: 0, secrets_found: 0, boss_dead: false, flags: HashSet::new() } }
3348 pub fn completion_pct(&self) -> f32 {
3349 let mut s=0.0f32; let mut t=0.0f32;
3350 if self.enemies_total>0{s+=self.enemies_killed as f32/self.enemies_total as f32*40.0;t+=40.0;}
3351 if self.chests_total>0{s+=self.chests_opened as f32/self.chests_total as f32*30.0;t+=30.0;}
3352 if self.secrets_total>0{s+=self.secrets_found as f32/self.secrets_total as f32*20.0;t+=20.0;}
3353 s+=if self.boss_dead{10.0}else{0.0};t+=10.0;
3354 if t>0.0{s/t*100.0}else{0.0}
3355 }
3356 pub fn is_100pct(&self)->bool{self.enemies_killed==self.enemies_total&&self.chests_opened==self.chests_total&&self.secrets_found==self.secrets_total&&self.boss_dead}
3357 pub fn grade(&self)->char{match self.completion_pct() as u32{90..=100=>'S',80..=89=>'A',70..=79=>'B',60..=69=>'C',_=>'D'}}
3358 pub fn set_flag(&mut self,f:&str){self.flags.insert(f.to_string());}
3359 pub fn has_flag(&self,f:&str)->bool{self.flags.contains(f)}
3360}
3361
3362pub fn run_map_region_tests() {
3367 let mut g = WorldRegionGraph::new();
3368 let mut r1 = MapRegionRecord::new(1,"Village",(0,0,50,50),BiomeType::Grassland); r1.unlock();
3369 let r2 = MapRegionRecord::new(2,"Forest",(60,0,120,50),BiomeType::Forest);
3370 let r3 = MapRegionRecord::new(3,"Desert",(130,0,200,50),BiomeType::Desert);
3371 g.add_region(r1); g.add_region(r2); g.add_region(r3);
3372 g.region_by_id_mut(1).unwrap().connect_to(2); g.region_by_id_mut(2).unwrap().connect_to(1);
3373 g.region_by_id_mut(2).unwrap().connect_to(3); g.region_by_id_mut(3).unwrap().connect_to(2);
3374 let path = g.bfs_path(1,3); assert!(path.is_some()); assert_eq!(path.unwrap(),vec![1,2,3]);
3375 assert!(g.is_connected(1,3)); assert_eq!(g.unlocked_regions().len(),1);
3376 let region = g.region_at(25,25); assert!(region.is_some());
3377}
3378
3379pub fn run_dungeon_gen2_tests() {
3380 let gen = DungeonGen2::new(80,80,42);
3381 let rooms = gen.generate();
3382 assert!(!rooms.is_empty());
3383 assert_eq!(rooms[0].room_type, DungeonRoomType2::Entrance);
3384 if rooms.len() >= 4 { assert!(rooms.iter().any(|r| r.room_type == DungeonRoomType2::Boss)); }
3385 for r in &rooms { assert!(r.area() > 0); }
3386}
3387
3388pub fn run_pathgrid_tests() {
3389 let mut grid = PathGrid::new(20,20);
3390 for y in 5..15 { grid.set_passable(10,y,false); }
3391 let path = grid.astar(TileCoord::new(5,10),TileCoord::new(15,10),false);
3392 assert!(path.is_some());
3393 let p = path.unwrap();
3394 assert_eq!(*p.first().unwrap(), TileCoord::new(5,10));
3395 assert_eq!(*p.last().unwrap(), TileCoord::new(15,10));
3396 for pos in &p { assert!(grid.is_passable(pos)); }
3397}
3398
3399pub fn run_map_event_registry_tests() {
3400 let mut reg = MapEventRegistry::new();
3401 reg.add(MapEventRecord::new(1,MapEventKind::Enter,5,5));
3402 reg.add(MapEventRecord::new(2,MapEventKind::Trap,5,5));
3403 let fired = reg.trigger_at(5,5); assert_eq!(fired.len(),2);
3404 reg.reset_all();
3405 assert!(reg.events[0].can_fire(0.0));
3406}
3407
3408pub fn run_minimap_view_tests() {
3409 let mut mm = MinimapView::new(64,64,2);
3410 mm.reveal_radius(32,32,8);
3411 assert!(mm.exploration_pct()>0.0);
3412 assert!(mm.is_explored(32,32));
3413 assert!(!mm.is_explored(0,0));
3414}
3415
3416pub fn run_npc_patrol_tests() {
3417 let mut npc = NpcPatrol::new(1,(0.0,0.0),3.0);
3418 npc.add_waypoint(PatrolPoint::new(10,0).with_wait(1.0));
3419 npc.add_waypoint(PatrolPoint::new(10,10));
3420 assert_eq!(npc.status,PatrolStatus::Idle);
3421 npc.update(0.016); assert_eq!(npc.status,PatrolStatus::Walking);
3422 npc.alert(); assert_eq!(npc.status,PatrolStatus::Alert);
3423 npc.update(0.016); assert_eq!(npc.status,PatrolStatus::Searching);
3424 npc.update(6.0); assert_eq!(npc.status,PatrolStatus::Returning);
3425}
3426
3427pub fn run_autotile_grid_tests() {
3428 let mut grid = AutotileGrid::new(20,20);
3429 grid.fill_rect(5,5,10,10);
3430 assert!(grid.solid_count()>0);
3431 assert_eq!(grid.tile_at(7,7),255);
3432 let edge = grid.tile_at(5,5);
3433 assert_ne!(edge,255);
3434}
3435
3436pub fn run_loot_table_tests2() {
3437 let goblin = LootTableDef::goblin_loot();
3438 assert!(goblin.total_weight()>0);
3439 let loot = goblin.roll(12345,5); assert!(!loot.is_empty());
3440 let boss = LootTableDef::boss_loot();
3441 let boss_loot = boss.roll(99999,10); assert!(!boss_loot.is_empty());
3442}
3443
3444pub fn run_tile_effects_tests() {
3445 let mut ef = TileEffects::new(20,20);
3446 ef.set(5,5,TileEffectKind::Damage,10.0);
3447 ef.set(6,6,TileEffectKind::Heal,5.0);
3448 ef.set(7,7,TileEffectKind::Slow,0.5);
3449 let mut hp=100.0f32; let mut spd=1.0f32;
3450 ef.apply(5,5,&mut hp,&mut spd); assert_eq!(hp,90.0);
3451 ef.apply(6,6,&mut hp,&mut spd); assert_eq!(hp,95.0);
3452 ef.apply(7,7,&mut hp,&mut spd); assert!((spd-0.5).abs()<0.001);
3453}
3454
3455pub fn run_layer_stack_tests2() {
3456 let mut stack = LayerStack::standard_rpg(32,32);
3457 assert_eq!(stack.count(),5);
3458 if let Some(layer) = stack.layer_mut("terrain") { layer.fill(1); layer.set(5,5,42); }
3459 let comp = stack.composite_at(5,5); assert!(comp>0);
3460 assert!(stack.visible_count()>0);
3461}
3462
3463pub fn run_map_progress_tests() {
3464 let mut p = MapProgress::new(1);
3465 p.enemies_total=10; p.enemies_killed=10; p.chests_total=5; p.chests_opened=5;
3466 p.secrets_total=3; p.secrets_found=3; p.boss_dead=true;
3467 assert!(p.is_100pct()); assert_eq!(p.grade(),'S');
3468 p.set_flag("secret_passage"); assert!(p.has_flag("secret_passage"));
3469}
3470
3471pub fn run_note_layer_tests() {
3472 let mut layer = NoteLayer::new();
3473 layer.add(MapNote::new(1,5,5,"Town entrance"));
3474 layer.add(MapNote::warning(2,10,10,"Danger"));
3475 layer.add(MapNote::secret(3,20,20,"Hidden"));
3476 assert_eq!(layer.visible_count(),3);
3477 assert_eq!(layer.at(5,5).len(),1);
3478 assert_eq!(layer.search("Hidden").len(),1);
3479 let exported = layer.export();
3480 assert!(exported.contains("Town entrance"));
3481 layer.remove(1); assert_eq!(layer.notes.len(),2);
3482}
3483
3484pub fn map_editor_mega_test() {
3485 run_map_region_tests();
3486 run_dungeon_gen2_tests();
3487 run_pathgrid_tests();
3488 run_map_event_registry_tests();
3489 run_minimap_view_tests();
3490 run_npc_patrol_tests();
3491 run_autotile_grid_tests();
3492 run_loot_table_tests2();
3493 run_tile_effects_tests();
3494 run_layer_stack_tests2();
3495 run_map_progress_tests();
3496 run_note_layer_tests();
3497 map_editor_final();
3498}
3499
3500pub const MAP_MODULE_VERSION: &str = "3.0.0";
3501pub const MAP_MAX_SIZE: i32 = 4096;
3502pub const MAP_CHUNK_SZ: i32 = 16;
3503pub const MAP_MAX_LAYERS: usize = 32;
3504pub const MAP_AUTOTILE_VARIANTS: u32 = 256;
3505
3506pub fn map_editor_info() -> HashMap<String, String> {
3507 let mut info = HashMap::new();
3508 info.insert("module".into(), "map_editor".into());
3509 info.insert("version".into(), MAP_MODULE_VERSION.into());
3510 info.insert("max_size".into(), format!("{}x{}", MAP_MAX_SIZE, MAP_MAX_SIZE));
3511 info.insert("pathfinding".into(), "A* octile heuristic".into());
3512 info.insert("dungeon_gen".into(), "BSP + Prim MST".into());
3513 info.insert("autotile".into(), "Wang blob 256-variant".into());
3514 info
3515}
3516
3517impl ChunkStreamingManager {
3519 pub fn new(stream_radius: i32) -> Self { Self { loaded_chunks: HashMap::new(), stream_radius, current_frame: 0 } }
3520 pub fn ensure_loaded(&mut self, cx: i32, cz: i32) { self.loaded_chunks.entry((cx, cz)).or_insert_with(|| MapChunk { chunk_x: cx, chunk_y: cz, tile_data: Vec::new(), is_loaded: true, last_access_frame: 0, dirty: false }); }
3521 pub fn evict_distant_chunks(&mut self, cx: i32, cz: i32) { let r = self.stream_radius; self.loaded_chunks.retain(|&(x,z),_| (x-cx).abs() <= r && (z-cz).abs() <= r); }
3522 pub fn get_tile_at(&self, _x: i32, _z: i32) -> Option<u32> { None }
3523 pub fn loaded_chunk_count(&self) -> usize { self.loaded_chunks.len() }
3524}
3525impl FogOfWar {
3526 pub fn new(w: u32, h: u32, los_radius: u32) -> Self { let n = (w*h) as usize; Self { width: w, height: h, revealed: vec![false; n], visible: vec![false; n], los_radius } }
3527 pub fn reveal_radius(&mut self, cx: u32, cy: u32) { let r = self.los_radius as i32; for dy in -r..=r { for dx in -r..=r { if dx*dx+dy*dy <= r*r { let nx = cx as i32+dx; let ny = cy as i32+dy; if nx>=0 && ny>=0 && (nx as u32)<self.width && (ny as u32)<self.height { let idx = ny as usize*self.width as usize+nx as usize; self.revealed[idx]=true; self.visible[idx]=true; }}}} }
3528 pub fn clear_visible(&mut self) { for v in &mut self.visible { *v = false; } }
3529 pub fn is_revealed(&self, x: u32, y: u32) -> bool { let idx = y as usize*self.width as usize+x as usize; self.revealed.get(idx).copied().unwrap_or(false) }
3530 pub fn is_visible(&self, x: u32, y: u32) -> bool { let idx = y as usize*self.width as usize+x as usize; self.visible.get(idx).copied().unwrap_or(false) }
3531 pub fn exploration_percentage(&self) -> f32 { if self.revealed.is_empty() { return 0.0; } self.revealed.iter().filter(|&&v| v).count() as f32 / self.revealed.len() as f32 * 100.0 }
3532}
3533impl HeightMap {
3534 pub fn new(w: u32, h: u32) -> Self { Self { width: w, height: h, data: vec![0.0; (w*h) as usize] } }
3535 pub fn generate_fbm(&mut self, octaves: u32, seed: u64) { let mut s = seed ^ 0x9e3779b97f4a7c15; for i in 0..self.data.len() { let x = (i % self.width as usize) as f32 / self.width as f32; let y = (i / self.width as usize) as f32 / self.height as f32; let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; for _ in 0..octaves { s ^= s<<13; s ^= s>>7; s ^= s<<17; let nx = x*freq + (s as f32 / u64::MAX as f32); let ny = y*freq + ((s>>32) as f32 / u32::MAX as f32); v += (nx.sin()*ny.cos()) * amp; amp *= 0.5; freq *= 2.0; } self.data[i] = (v + 2.0).max(0.0); } }
3536 pub fn smooth(&mut self, passes: u32) { for _ in 0..passes { let mut out = self.data.clone(); let w = self.width as usize; let h = self.height as usize; for y in 1..h-1 { for x in 1..w-1 { out[y*w+x] = (self.data[(y-1)*w+x]+self.data[(y+1)*w+x]+self.data[y*w+x-1]+self.data[y*w+x+1]+self.data[y*w+x])/5.0; }} self.data = out; } }
3537 pub fn apply_erosion(&mut self, passes: u32) { for _ in 0..passes { let w = self.width as usize; let h = self.height as usize; let mut out = self.data.clone(); for y in 1..h-1 { for x in 1..w-1 { let c = self.data[y*w+x]; let neighbors = [self.data[(y-1)*w+x],self.data[(y+1)*w+x],self.data[y*w+x-1],self.data[y*w+x+1]]; let min_n = neighbors.iter().cloned().fold(f32::MAX, f32::min); if c > min_n { out[y*w+x] = c - (c-min_n)*0.1; } }} self.data = out; } }
3538 pub fn to_terrain_tiles(&self, _db: &TileDatabase, water: f32, mountain: f32) -> Vec<Vec<u32>> { (0..self.height as usize).map(|y| (0..self.width as usize).map(|x| { let v = self.data[y*self.width as usize+x]; if v < water { 0 } else if v < mountain { 1 } else { 2 } }).collect()).collect() }
3539 pub fn to_terrain_tiles_simple(&self) -> Vec<Vec<u32>> { self.to_terrain_tiles(&TileDatabase::new(), 0.3, 0.75) }
3540}
3541impl LevelTransition {
3542 pub fn new(id: u32, position: (i32, i32), target_map: &str, target_position: (i32, i32)) -> Self { Self { id, position, target_map: target_map.to_string(), target_position, direction: 0, requires_item: None, requires_quest: None, transition_type: String::from("door"), is_bidirectional: true } }
3543 pub fn can_use<FI: Fn(u32) -> bool, FQ: Fn(u32) -> bool>(&self, has_item: FI, has_quest: FQ) -> bool { self.requires_item.map_or(true, |i| has_item(i)) && self.requires_quest.map_or(true, |q| has_quest(q)) }
3544}
3545impl Minimap {
3546 pub fn new(w: u32, h: u32, scale: u32) -> Self { Self { width: w, height: h, cells: vec![MinimapCell { explored: false, visible: false, tile_type: 0, has_entity: false }; (w*h) as usize], scale } }
3547 pub fn reveal_radius(&mut self, cx: u32, cy: u32, r: u32) { let r = r as i32; for dy in -r..=r { for dx in -r..=r { let nx = cx as i32+dx; let ny = cy as i32+dy; if nx>=0 && ny>=0 && (nx as u32)<self.width && (ny as u32)<self.height { let idx = ny as usize*self.width as usize+nx as usize; if let Some(c) = self.cells.get_mut(idx) { c.explored=true; c.visible=true; }}}} }
3548 pub fn is_explored(&self, x: u32, y: u32) -> bool { self.cells.get((y*self.width+x) as usize).map_or(false, |c| c.explored) }
3549 pub fn exploration_percent(&self) -> f32 { if self.cells.is_empty() { return 0.0; } self.cells.iter().filter(|c| c.explored).count() as f32 / self.cells.len() as f32 * 100.0 }
3550 pub fn world_to_minimap(&self, wx: f32, wy: f32) -> (u32, u32) { ((wx as u32).min(self.width.saturating_sub(1)), (wy as u32).min(self.height.saturating_sub(1))) }
3551}
3552impl QuestJournal {
3553 pub fn new() -> Self { Self { quests: HashMap::new(), active_quest_id: None } }
3554 pub fn add_quest(&mut self, q: Quest) { self.quests.insert(q.id, q); }
3555 pub fn start_quest(&mut self, id: u32) { if let Some(q) = self.quests.get_mut(&id) { q.status = QuestStatus::Active; self.active_quest_id = Some(id); } }
3556 pub fn active_quests(&self) -> Vec<&Quest> { self.quests.values().filter(|q| matches!(q.status, QuestStatus::Active)).collect() }
3557}
3558impl Quest {
3559 pub fn new(id: u32, name: &str, description: &str) -> Self { Self { id, name: name.to_string(), description: description.to_string(), status: QuestStatus::NotStarted, objectives: Vec::new(), reward: QuestReward { experience: 0, gold: 0, items: Vec::new(), reputation: HashMap::new() }, prereq_quests: Vec::new(), journal_entries: Vec::new(), is_main_quest: false, chapter: 1 } }
3560 pub fn add_objective(&mut self, obj: QuestObjectiveKind) { self.objectives.push(obj); }
3561 pub fn start(&mut self) { self.status = QuestStatus::Active; }
3562 pub fn check_completion(&mut self) { let done = self.objectives.iter().all(|o| match o { QuestObjectiveKind::KillEnemy { count, progress, .. } => progress >= count, QuestObjectiveKind::CollectItem { count, progress, .. } => progress >= count, QuestObjectiveKind::ReachLocation { reached, .. } => *reached, QuestObjectiveKind::TalkToNpc { talked, .. } => *talked, QuestObjectiveKind::ActivateObject { activated, .. } => *activated, QuestObjectiveKind::EscortNpc { reached, .. } => *reached, }); if done { self.status = QuestStatus::Completed; } }
3563 pub fn progress_percentage(&self) -> f32 { if self.objectives.is_empty() { return 100.0; } self.objectives.iter().map(|o| match o { QuestObjectiveKind::KillEnemy { count, progress, .. } => (*progress as f32 / *count as f32).min(1.0), QuestObjectiveKind::CollectItem { count, progress, .. } => (*progress as f32 / *count as f32).min(1.0), QuestObjectiveKind::ReachLocation { reached, .. } => if *reached { 1.0 } else { 0.0 }, QuestObjectiveKind::TalkToNpc { talked, .. } => if *talked { 1.0 } else { 0.0 }, QuestObjectiveKind::ActivateObject { activated, .. } => if *activated { 1.0 } else { 0.0 }, QuestObjectiveKind::EscortNpc { reached, .. } => if *reached { 1.0 } else { 0.0 }, }).sum::<f32>() / self.objectives.len() as f32 * 100.0 }
3564}
3565impl SpawnTable {
3566 pub fn new() -> Self { Self { entries: Vec::new(), total_weight: 0.0 } }
3567 pub fn add(&mut self, entry: SpawnTableEntry) { self.total_weight += entry.weight; self.entries.push(entry); }
3568}
3569impl TreasureChest {
3570 pub fn new(id: u32, x: i32, y: i32) -> Self { Self { id, position: (x, y), items: Vec::new(), gold_amount: 0, chest_type: String::from("wooden"), is_locked: false, key_id: None, is_trapped: false, trap_damage: 0, opened: false } }
3571 pub fn add_item(&mut self, item: TreasureItem) { self.items.push(item); }
3572 pub fn total_value(&self) -> u32 { self.gold_amount + self.items.iter().map(|i| i.base_value).sum::<u32>() }
3573 pub fn set_trap(&mut self, damage: u32) { self.is_trapped = true; self.trap_damage = damage; }
3574 pub fn open(&mut self) -> Vec<TreasureItem> { self.opened = true; self.items.drain(..).collect() }
3575}
3576impl WeatherTransition {
3577 pub fn new(from: WeatherType, to: WeatherType, duration_s: f32) -> Self { Self { from, to, duration_s, elapsed_s: 0.0 } }
3578 pub fn update(&mut self, dt: f32) { self.elapsed_s = (self.elapsed_s + dt).min(self.duration_s); }
3579 pub fn progress(&self) -> f32 { if self.duration_s <= 0.0 { 1.0 } else { self.elapsed_s / self.duration_s } }
3580 pub fn is_complete(&self) -> bool { self.elapsed_s >= self.duration_s }
3581 pub fn movement_speed_modifier(&self) -> f32 { match self.to { WeatherType::Snow | WeatherType::Thunderstorm => 0.7, WeatherType::Rain => 0.9, _ => 1.0 } }
3582}
3583impl WorldMap {
3584 pub fn new(name: &str, width: u32, height: u32) -> Self { Self { regions: Vec::new(), width, height, world_name: name.to_string(), starting_region: 0 } }
3585 pub fn add_region(&mut self, r: MapRegion) { self.regions.push(r); }
3586 pub fn discover_region(&mut self, id: u32) { if let Some(r) = self.regions.iter_mut().find(|r| r.id == id) { r.discovered = true; } }
3587 pub fn region_by_id(&self, id: u32) -> Option<&MapRegion> { self.regions.iter().find(|r| r.id == id) }
3588 pub fn discovered_regions(&self) -> Vec<&MapRegion> { self.regions.iter().filter(|r| r.discovered).collect() }
3589 pub fn find_region_path(&self, from: u32, to: u32) -> Vec<u32> { let mut visited = std::collections::HashSet::new(); let mut queue = std::collections::VecDeque::new(); queue.push_back(vec![from]); while let Some(path) = queue.pop_front() { let cur = *path.last().unwrap(); if cur == to { return path; } if visited.contains(&cur) { continue; } visited.insert(cur); if let Some(r) = self.regions.iter().find(|r| r.id == cur) { for &nb in &r.connected_regions { let mut np = path.clone(); np.push(nb); queue.push_back(np); } } } Vec::new() }
3590}
3591impl AStarPathfinder {
3592 pub fn find_path(_map: &TileMap, _db: &TileDatabase, _sx: u32, _sy: u32, _ex: u32, _ey: u32, _allow_diag: bool, _layer: usize) -> Option<Vec<(u32, u32)>> { Some(vec![(_sx, _sy), (_ex, _ey)]) }
3593}
3594
3595impl Room {
3596 pub fn contains_point(&self, x: i32, y: i32) -> bool {
3597 let x = x as usize; let y = y as usize;
3598 x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
3599 }
3600 pub fn center(&self) -> (usize, usize) {
3601 (self.x + self.width / 2, self.y + self.height / 2)
3602 }
3603}
3604impl LootTable {
3605 pub fn boss_hoard() -> Self { Self { id: 9001, name: String::from("BossHoard"), entries: Vec::new(), guaranteed_entries: Vec::new(), rolls_min: 3, rolls_max: 7 } }
3606 pub fn goblin_treasure() -> Self { Self { id: 9002, name: String::from("GoblinTreasure"), entries: Vec::new(), guaranteed_entries: Vec::new(), rolls_min: 1, rolls_max: 3 } }
3607 pub fn total_weight(&self) -> f32 { self.entries.iter().map(|e| e.weight as f32).sum() }
3608}
3609impl DungeonTheme {
3610 pub fn crypt() -> Self { Self { name: String::from("Crypt"), wall_tile_ids: vec![10,11], floor_tile_ids: vec![12,13], door_tile_id: 14, chest_frequency: 0.1, trap_frequency: 0.2, enemy_types: vec![String::from("Skeleton"),String::from("Zombie")], ambient_light: (20,20,30), torch_frequency: 0.3, special_tiles: std::collections::HashMap::new(), music_track: String::from("crypt_ambient") } }
3611 pub fn random_wall_tile(&self, rng: &mut MapRng) -> u32 { rng.next_u64(); self.wall_tile_ids[rng.state as usize % self.wall_tile_ids.len()] }
3612}
3613
3614
3615#[derive(Debug, Clone, PartialEq)]
3620pub enum ExtRoomShape {
3621 Rectangular,
3622 LShape,
3623 TShape,
3624 CrossShape,
3625 Circular,
3626 Irregular,
3627}
3628
3629#[derive(Debug, Clone, PartialEq)]
3630pub enum RoomTheme {
3631 Stone,
3632 Wood,
3633 Cave,
3634 Crypt,
3635 Library,
3636 Throne,
3637 Dungeon,
3638 Sewers,
3639 Ruins,
3640 Temple,
3641}
3642
3643#[derive(Debug, Clone)]
3644pub struct ExtDungeonRoom {
3645 pub id: u32,
3646 pub name: String,
3647 pub shape: ExtRoomShape,
3648 pub theme: RoomTheme,
3649 pub width: u32,
3650 pub height: u32,
3651 pub position: Vec2,
3652 pub connections: Vec<u32>,
3653 pub loot_table_id: Option<u32>,
3654 pub monster_group_id: Option<u32>,
3655 pub trap_ids: Vec<u32>,
3656 pub secret: bool,
3657 pub boss_room: bool,
3658 pub visited: bool,
3659 pub light_level: f32,
3660 pub ambient_sound: Option<String>,
3661}
3662
3663impl ExtDungeonRoom {
3664 pub fn new(id: u32, shape: ExtRoomShape, theme: RoomTheme, w: u32, h: u32) -> Self {
3665 ExtDungeonRoom {
3666 id,
3667 name: format!("Room {}", id),
3668 shape, theme, width: w, height: h,
3669 position: Vec2::ZERO,
3670 connections: Vec::new(),
3671 loot_table_id: None,
3672 monster_group_id: None,
3673 trap_ids: Vec::new(),
3674 secret: false,
3675 boss_room: false,
3676 visited: false,
3677 light_level: 0.5,
3678 ambient_sound: None,
3679 }
3680 }
3681
3682 pub fn area(&self) -> f32 {
3683 match self.shape {
3684 ExtRoomShape::Rectangular => (self.width * self.height) as f32,
3685 ExtRoomShape::LShape => (self.width * self.height) as f32 * 0.75,
3686 ExtRoomShape::TShape => (self.width * self.height) as f32 * 0.75,
3687 ExtRoomShape::CrossShape => (self.width * self.height) as f32 * 0.6,
3688 ExtRoomShape::Circular => {
3689 let r = self.width.min(self.height) as f32 * 0.5;
3690 std::f32::consts::PI * r * r
3691 },
3692 ExtRoomShape::Irregular => (self.width * self.height) as f32 * 0.65,
3693 }
3694 }
3695
3696 pub fn connect(&mut self, other_id: u32) {
3697 if !self.connections.contains(&other_id) {
3698 self.connections.push(other_id);
3699 }
3700 }
3701
3702 pub fn bounds(&self) -> (Vec2, Vec2) {
3703 (self.position, self.position + Vec2::new(self.width as f32, self.height as f32))
3704 }
3705
3706 pub fn center(&self) -> Vec2 {
3707 self.position + Vec2::new(self.width as f32 * 0.5, self.height as f32 * 0.5)
3708 }
3709}
3710
3711#[derive(Debug, Clone)]
3712pub struct DungeonFloor {
3713 pub floor_number: i32,
3714 pub rooms: HashMap<u32, ExtDungeonRoom>,
3715 pub corridors: Vec<(u32, u32)>,
3716 pub entrance_room_id: u32,
3717 pub exit_room_id: u32,
3718 pub width: u32,
3719 pub height: u32,
3720 pub difficulty: f32,
3721}
3722
3723impl DungeonFloor {
3724 pub fn new(floor_number: i32, width: u32, height: u32) -> Self {
3725 DungeonFloor {
3726 floor_number, rooms: HashMap::new(),
3727 corridors: Vec::new(),
3728 entrance_room_id: 0, exit_room_id: 0,
3729 width, height, difficulty: 1.0,
3730 }
3731 }
3732
3733 pub fn add_room(&mut self, room: ExtDungeonRoom) {
3734 self.rooms.insert(room.id, room);
3735 }
3736
3737 pub fn connect_rooms(&mut self, a: u32, b: u32) {
3738 if let Some(room_a) = self.rooms.get_mut(&a) {
3739 room_a.connect(b);
3740 }
3741 if let Some(room_b) = self.rooms.get_mut(&b) {
3742 room_b.connect(a);
3743 }
3744 self.corridors.push((a, b));
3745 }
3746
3747 pub fn room_count(&self) -> usize { self.rooms.len() }
3748
3749 pub fn boss_rooms(&self) -> Vec<&ExtDungeonRoom> {
3750 self.rooms.values().filter(|r| r.boss_room).collect()
3751 }
3752
3753 pub fn secret_rooms(&self) -> Vec<&ExtDungeonRoom> {
3754 self.rooms.values().filter(|r| r.secret).collect()
3755 }
3756
3757 pub fn bfs_distance(&self, from: u32, to: u32) -> Option<u32> {
3758 let mut visited = HashSet::new();
3759 let mut queue = VecDeque::new();
3760 queue.push_back((from, 0));
3761 visited.insert(from);
3762 while let Some((curr, dist)) = queue.pop_front() {
3763 if curr == to { return Some(dist); }
3764 if let Some(room) = self.rooms.get(&curr) {
3765 for &next in &room.connections {
3766 if !visited.contains(&next) {
3767 visited.insert(next);
3768 queue.push_back((next, dist + 1));
3769 }
3770 }
3771 }
3772 }
3773 None
3774 }
3775}
3776
3777#[derive(Debug, Clone)]
3782pub struct ExtDungeonGenParams {
3783 pub width: u32,
3784 pub height: u32,
3785 pub min_rooms: u32,
3786 pub max_rooms: u32,
3787 pub min_room_size: u32,
3788 pub max_room_size: u32,
3789 pub corridor_width: u32,
3790 pub boss_room_probability: f32,
3791 pub secret_room_probability: f32,
3792 pub seed: u64,
3793}
3794
3795impl Default for ExtDungeonGenParams {
3796 fn default() -> Self {
3797 ExtDungeonGenParams {
3798 width: 100, height: 100,
3799 min_rooms: 8, max_rooms: 20,
3800 min_room_size: 5, max_room_size: 15,
3801 corridor_width: 2,
3802 boss_room_probability: 0.1,
3803 secret_room_probability: 0.15,
3804 seed: 12345,
3805 }
3806 }
3807}
3808
3809pub struct ExtDungeonGenerator {
3810 pub params: ExtDungeonGenParams,
3811 pub rng_state: u64,
3812}
3813
3814impl ExtDungeonGenerator {
3815 pub fn new(params: ExtDungeonGenParams) -> Self {
3816 let seed = params.seed;
3817 ExtDungeonGenerator { params, rng_state: seed }
3818 }
3819
3820 fn next_rand(&mut self) -> u64 {
3821 self.rng_state ^= self.rng_state << 13;
3822 self.rng_state ^= self.rng_state >> 7;
3823 self.rng_state ^= self.rng_state << 17;
3824 self.rng_state
3825 }
3826
3827 fn rand_range(&mut self, min: u32, max: u32) -> u32 {
3828 if min >= max { return min; }
3829 (self.next_rand() as u32 % (max - min)) + min
3830 }
3831
3832 fn rand_f32(&mut self) -> f32 {
3833 (self.next_rand() & 0xFFFFFF) as f32 / 0xFFFFFF as f32
3834 }
3835
3836 pub fn generate(&mut self, floor_number: i32) -> DungeonFloor {
3837 let mut floor = DungeonFloor::new(floor_number, self.params.width, self.params.height);
3838 let num_rooms = self.rand_range(self.params.min_rooms, self.params.max_rooms);
3839
3840 for i in 0..num_rooms {
3841 let w = self.rand_range(self.params.min_room_size, self.params.max_room_size);
3842 let h = self.rand_range(self.params.min_room_size, self.params.max_room_size);
3843 let x = self.rand_range(1, self.params.width.saturating_sub(w + 1));
3844 let y = self.rand_range(1, self.params.height.saturating_sub(h + 1));
3845 let theme_idx = (self.next_rand() % 10) as usize;
3846 let themes = [
3847 RoomTheme::Stone, RoomTheme::Wood, RoomTheme::Cave,
3848 RoomTheme::Crypt, RoomTheme::Library, RoomTheme::Throne,
3849 RoomTheme::Dungeon, RoomTheme::Sewers, RoomTheme::Ruins, RoomTheme::Temple,
3850 ];
3851 let mut room = ExtDungeonRoom::new(i, ExtRoomShape::Rectangular, themes[theme_idx].clone(), w, h);
3852 room.position = Vec2::new(x as f32, y as f32);
3853 room.boss_room = self.rand_f32() < self.params.boss_room_probability;
3854 room.secret = self.rand_f32() < self.params.secret_room_probability;
3855 floor.add_room(room);
3856 }
3857
3858 let room_ids: Vec<u32> = floor.rooms.keys().copied().collect();
3860 for i in 1..room_ids.len() {
3861 floor.connect_rooms(room_ids[i-1], room_ids[i]);
3862 }
3863 if !room_ids.is_empty() {
3864 floor.entrance_room_id = room_ids[0];
3865 floor.exit_room_id = *room_ids.last().unwrap();
3866 }
3867 floor
3868 }
3869}
3870
3871#[derive(Debug, Clone, PartialEq)]
3876pub enum ExtBiomeType {
3877 Plains,
3878 Forest,
3879 Desert,
3880 Tundra,
3881 Swamp,
3882 Mountains,
3883 Jungle,
3884 Coast,
3885 Ocean,
3886 Volcanic,
3887 Magical,
3888 Corrupted,
3889}
3890
3891#[derive(Debug, Clone)]
3892pub struct WorldRegion {
3893 pub id: u32,
3894 pub name: String,
3895 pub biome: ExtBiomeType,
3896 pub center: Vec2,
3897 pub radius: f32,
3898 pub elevation: f32,
3899 pub temperature: f32,
3900 pub rainfall: f32,
3901 pub danger_level: u32,
3902 pub discovered: bool,
3903 pub controlled_by_faction: Option<u32>,
3904 pub resources: HashMap<String, f32>,
3905 pub adjacent_region_ids: Vec<u32>,
3906 pub dungeons: Vec<u32>,
3907 pub settlements: Vec<u32>,
3908}
3909
3910impl WorldRegion {
3911 pub fn new(id: u32, name: &str, biome: ExtBiomeType) -> Self {
3912 WorldRegion {
3913 id, name: name.to_string(), biome,
3914 center: Vec2::ZERO, radius: 50.0,
3915 elevation: 0.0, temperature: 15.0, rainfall: 500.0,
3916 danger_level: 1, discovered: false,
3917 controlled_by_faction: None,
3918 resources: HashMap::new(),
3919 adjacent_region_ids: Vec::new(),
3920 dungeons: Vec::new(), settlements: Vec::new(),
3921 }
3922 }
3923
3924 pub fn is_habitable(&self) -> bool {
3925 self.temperature > -20.0 && self.temperature < 45.0
3926 && self.rainfall > 100.0
3927 && !matches!(self.biome, ExtBiomeType::Volcanic | ExtBiomeType::Corrupted)
3928 }
3929
3930 pub fn traversal_cost(&self) -> f32 {
3931 match self.biome {
3932 ExtBiomeType::Plains => 1.0,
3933 ExtBiomeType::Forest => 1.5,
3934 ExtBiomeType::Desert => 2.0,
3935 ExtBiomeType::Tundra => 2.5,
3936 ExtBiomeType::Swamp => 3.0,
3937 ExtBiomeType::Mountains => 4.0,
3938 ExtBiomeType::Jungle => 3.5,
3939 ExtBiomeType::Coast => 1.2,
3940 ExtBiomeType::Ocean => 5.0,
3941 ExtBiomeType::Volcanic => 6.0,
3942 ExtBiomeType::Magical => 1.8,
3943 ExtBiomeType::Corrupted => 3.0,
3944 }
3945 }
3946
3947 pub fn add_resource(&mut self, name: &str, amount: f32) {
3948 *self.resources.entry(name.to_string()).or_insert(0.0) += amount;
3949 }
3950}
3951
3952#[derive(Debug, Clone)]
3953pub struct ExtWorldMap {
3954 pub name: String,
3955 pub regions: HashMap<u32, WorldRegion>,
3956 pub width: f32,
3957 pub height: f32,
3958 pub sea_level: f32,
3959 pub calendar: String,
3960}
3961
3962impl ExtWorldMap {
3963 pub fn new(name: &str, w: f32, h: f32) -> Self {
3964 ExtWorldMap {
3965 name: name.to_string(),
3966 regions: HashMap::new(),
3967 width: w, height: h,
3968 sea_level: 0.0,
3969 calendar: "Imperial Calendar".to_string(),
3970 }
3971 }
3972
3973 pub fn add_region(&mut self, region: WorldRegion) {
3974 self.regions.insert(region.id, region);
3975 }
3976
3977 pub fn regions_near(&self, pos: Vec2, radius: f32) -> Vec<&WorldRegion> {
3978 self.regions.values()
3979 .filter(|r| (r.center - pos).length() <= radius)
3980 .collect()
3981 }
3982
3983 pub fn region_at(&self, pos: Vec2) -> Option<&WorldRegion> {
3984 self.regions.values()
3985 .filter(|r| (r.center - pos).length() <= r.radius)
3986 .min_by(|a, b| {
3987 (a.center - pos).length().partial_cmp(&(b.center - pos).length()).unwrap()
3988 })
3989 }
3990
3991 pub fn pathfind_regions(&self, from: u32, to: u32) -> Option<Vec<u32>> {
3992 if from == to { return Some(vec![from]); }
3993 let mut dist: HashMap<u32, f32> = HashMap::new();
3994 let mut prev: HashMap<u32, u32> = HashMap::new();
3995 let mut unvisited: HashSet<u32> = self.regions.keys().copied().collect();
3996 dist.insert(from, 0.0);
3997 for &id in &unvisited {
3998 if id != from { dist.insert(id, f32::INFINITY); }
3999 }
4000 while !unvisited.is_empty() {
4001 let curr = *unvisited.iter()
4002 .min_by(|&&a, &&b| dist[&a].partial_cmp(&dist[&b]).unwrap())?;
4003 if curr == to { break; }
4004 let curr_dist = dist[&curr];
4005 if curr_dist.is_infinite() { break; }
4006 unvisited.remove(&curr);
4007 if let Some(region) = self.regions.get(&curr) {
4008 for &adj in ®ion.adjacent_region_ids {
4009 if !unvisited.contains(&adj) { continue; }
4010 if let Some(adj_region) = self.regions.get(&adj) {
4011 let new_dist = curr_dist + adj_region.traversal_cost();
4012 if new_dist < *dist.get(&adj).unwrap_or(&f32::INFINITY) {
4013 dist.insert(adj, new_dist);
4014 prev.insert(adj, curr);
4015 }
4016 }
4017 }
4018 }
4019 }
4020 let mut path = Vec::new();
4021 let mut curr = to;
4022 while let Some(&p) = prev.get(&curr) {
4023 path.push(curr);
4024 curr = p;
4025 }
4026 path.push(from);
4027 path.reverse();
4028 if path[0] == from { Some(path) } else { None }
4029 }
4030}
4031
4032#[derive(Debug, Clone, PartialEq)]
4037pub enum FactionType {
4038 Kingdom,
4039 Empire,
4040 Republic,
4041 Tribe,
4042 Guild,
4043 Church,
4044 Cult,
4045 MercenaryBand,
4046 WizardOrder,
4047 Criminal,
4048}
4049
4050#[derive(Debug, Clone)]
4051pub struct Faction {
4052 pub id: u32,
4053 pub name: String,
4054 pub faction_type: FactionType,
4055 pub leader_name: String,
4056 pub influence: f32,
4057 pub military_strength: f32,
4058 pub wealth: f32,
4059 pub reputation: f32,
4060 pub controlled_regions: Vec<u32>,
4061 pub relations: HashMap<u32, f32>,
4062 pub description: String,
4063 pub color: Vec4,
4064}
4065
4066impl Faction {
4067 pub fn new(id: u32, name: &str, faction_type: FactionType) -> Self {
4068 Faction {
4069 id, name: name.to_string(), faction_type,
4070 leader_name: String::new(),
4071 influence: 50.0, military_strength: 50.0,
4072 wealth: 1000.0, reputation: 0.0,
4073 controlled_regions: Vec::new(),
4074 relations: HashMap::new(),
4075 description: String::new(),
4076 color: Vec4::new(0.5, 0.5, 0.5, 1.0),
4077 }
4078 }
4079
4080 pub fn relation_to(&self, other_id: u32) -> f32 {
4081 self.relations.get(&other_id).copied().unwrap_or(0.0)
4082 }
4083
4084 pub fn is_allied_with(&self, other_id: u32) -> bool {
4085 self.relation_to(other_id) >= 50.0
4086 }
4087
4088 pub fn is_enemy_of(&self, other_id: u32) -> bool {
4089 self.relation_to(other_id) <= -50.0
4090 }
4091
4092 pub fn set_relation(&mut self, other_id: u32, value: f32) {
4093 self.relations.insert(other_id, value.clamp(-100.0, 100.0));
4094 }
4095
4096 pub fn total_power(&self) -> f32 {
4097 self.influence * 0.3 + self.military_strength * 0.5 + self.wealth * 0.0001 * 0.2
4098 }
4099}
4100
4101#[derive(Debug, Clone, PartialEq)]
4106pub enum SettlementType {
4107 Hamlet,
4108 Village,
4109 Town,
4110 City,
4111 Metropolis,
4112 Fortress,
4113 Port,
4114 Capital,
4115 Ruin,
4116}
4117
4118#[derive(Debug, Clone)]
4119pub struct Settlement {
4120 pub id: u32,
4121 pub name: String,
4122 pub settlement_type: SettlementType,
4123 pub position: Vec2,
4124 pub population: u32,
4125 pub faction_id: Option<u32>,
4126 pub services: Vec<String>,
4127 pub prosperity: f32,
4128 pub defense_rating: f32,
4129 pub trade_goods: HashMap<String, f32>,
4130 pub quests_available: u32,
4131 pub inn_quality: u32,
4132}
4133
4134impl Settlement {
4135 pub fn new(id: u32, name: &str, st: SettlementType) -> Self {
4136 Settlement {
4137 id, name: name.to_string(), settlement_type: st,
4138 position: Vec2::ZERO, population: 100,
4139 faction_id: None,
4140 services: Vec::new(),
4141 prosperity: 50.0, defense_rating: 10.0,
4142 trade_goods: HashMap::new(),
4143 quests_available: 0, inn_quality: 2,
4144 }
4145 }
4146
4147 pub fn has_service(&self, service: &str) -> bool {
4148 self.services.iter().any(|s| s == service)
4149 }
4150
4151 pub fn is_major_city(&self) -> bool {
4152 matches!(self.settlement_type, SettlementType::City | SettlementType::Metropolis | SettlementType::Capital)
4153 }
4154
4155 pub fn add_service(&mut self, service: &str) {
4156 if !self.has_service(service) {
4157 self.services.push(service.to_string());
4158 }
4159 }
4160}
4161
4162#[derive(Debug, Clone, PartialEq)]
4167pub enum QuestType {
4168 MainStory,
4169 SideQuest,
4170 Bounty,
4171 Fetch,
4172 Escort,
4173 Explore,
4174 Defend,
4175 Assassinate,
4176 Investigate,
4177 Craft,
4178 Delivery,
4179}
4180
4181
4182#[derive(Debug, Clone)]
4183pub struct QuestObjective {
4184 pub id: u32,
4185 pub description: String,
4186 pub current: u32,
4187 pub required: u32,
4188 pub optional: bool,
4189 pub complete: bool,
4190}
4191
4192impl QuestObjective {
4193 pub fn progress_pct(&self) -> f32 {
4194 if self.required == 0 { return 1.0; }
4195 self.current as f32 / self.required as f32
4196 }
4197}
4198
4199#[derive(Debug, Clone)]
4200pub struct ExtQuestReward {
4201 pub experience: u32,
4202 pub gold: u32,
4203 pub items: Vec<String>,
4204 pub reputation_changes: HashMap<u32, f32>,
4205 pub unlock_ids: Vec<String>,
4206}
4207
4208#[derive(Debug, Clone)]
4209pub struct ExtQuest {
4210 pub id: u32,
4211 pub name: String,
4212 pub description: String,
4213 pub quest_type: QuestType,
4214 pub status: QuestStatus,
4215 pub level_requirement: u32,
4216 pub objectives: Vec<QuestObjective>,
4217 pub rewards: ExtQuestReward,
4218 pub giver_id: Option<u32>,
4219 pub location_hint: Option<Vec2>,
4220 pub time_limit_seconds: Option<f32>,
4221 pub prerequisite_quest_ids: Vec<u32>,
4222 pub tags: Vec<String>,
4223}
4224
4225impl ExtQuest {
4226 pub fn new(id: u32, name: &str, quest_type: QuestType) -> Self {
4227 ExtQuest {
4228 id, name: name.to_string(), description: String::new(),
4229 quest_type, status: QuestStatus::NotStarted,
4230 level_requirement: 1,
4231 objectives: Vec::new(),
4232 rewards: ExtQuestReward { experience: 0, gold: 0, items: Vec::new(), reputation_changes: HashMap::new(), unlock_ids: Vec::new() },
4233 giver_id: None, location_hint: None, time_limit_seconds: None,
4234 prerequisite_quest_ids: Vec::new(), tags: Vec::new(),
4235 }
4236 }
4237
4238 pub fn add_objective(&mut self, obj: QuestObjective) {
4239 self.objectives.push(obj);
4240 }
4241
4242 pub fn is_complete(&self) -> bool {
4243 self.objectives.iter()
4244 .filter(|o| !o.optional)
4245 .all(|o| o.complete)
4246 }
4247
4248 pub fn overall_progress(&self) -> f32 {
4249 let required: Vec<&QuestObjective> = self.objectives.iter().filter(|o| !o.optional).collect();
4250 if required.is_empty() { return 1.0; }
4251 required.iter().map(|o| o.progress_pct()).sum::<f32>() / required.len() as f32
4252 }
4253
4254 pub fn start(&mut self) {
4255 if self.status == QuestStatus::NotStarted {
4256 self.status = QuestStatus::Active;
4257 }
4258 }
4259
4260 pub fn complete_quest(&mut self) {
4261 self.status = QuestStatus::Completed;
4262 }
4263}
4264
4265#[derive(Debug, Clone, PartialEq)]
4270pub enum EncounterDifficulty {
4271 Trivial, Easy, Medium, Hard, Deadly, Legendary,
4272}
4273
4274#[derive(Debug, Clone)]
4275pub struct EncounterEnemy {
4276 pub monster_id: u32,
4277 pub count: u32,
4278 pub level_override: Option<u32>,
4279 pub loot_bonus: f32,
4280}
4281
4282#[derive(Debug, Clone)]
4283pub struct Encounter {
4284 pub id: u32,
4285 pub name: String,
4286 pub difficulty: EncounterDifficulty,
4287 pub enemies: Vec<EncounterEnemy>,
4288 pub location_id: u32,
4289 pub trigger_radius: f32,
4290 pub respawn_seconds: Option<f32>,
4291 pub is_scripted: bool,
4292 pub ambush: bool,
4293 pub xp_multiplier: f32,
4294}
4295
4296impl Encounter {
4297 pub fn new(id: u32, name: &str, difficulty: EncounterDifficulty, location_id: u32) -> Self {
4298 Encounter {
4299 id, name: name.to_string(), difficulty,
4300 enemies: Vec::new(), location_id,
4301 trigger_radius: 5.0,
4302 respawn_seconds: Some(300.0),
4303 is_scripted: false, ambush: false, xp_multiplier: 1.0,
4304 }
4305 }
4306
4307 pub fn total_enemy_count(&self) -> u32 {
4308 self.enemies.iter().map(|e| e.count).sum()
4309 }
4310
4311 pub fn threat_rating(&self) -> f32 {
4312 let base = match self.difficulty {
4313 EncounterDifficulty::Trivial => 1.0,
4314 EncounterDifficulty::Easy => 2.0,
4315 EncounterDifficulty::Medium => 4.0,
4316 EncounterDifficulty::Hard => 8.0,
4317 EncounterDifficulty::Deadly => 16.0,
4318 EncounterDifficulty::Legendary => 32.0,
4319 };
4320 base * self.total_enemy_count() as f32 * self.xp_multiplier
4321 }
4322}
4323
4324#[derive(Debug, Clone, PartialEq)]
4329pub enum MapToolMode {
4330 Select,
4331 PlaceRoom,
4332 PlaceCorridor,
4333 PlaceObject,
4334 Paint,
4335 Erase,
4336 FloodFill,
4337 MeasureDistance,
4338 PlaceSpawn,
4339 PlaceWaypoint,
4340 EditLight,
4341 EditTrigger,
4342}
4343
4344#[derive(Debug, Clone)]
4345pub struct MapEditorState {
4346 pub tool_mode: MapToolMode,
4347 pub selected_room_ids: HashSet<u32>,
4348 pub brush_size: f32,
4349 pub paint_tile_id: u32,
4350 pub grid_size: f32,
4351 pub snap_to_grid: bool,
4352 pub show_grid: bool,
4353 pub show_collision: bool,
4354 pub show_navmesh: bool,
4355 pub show_lighting: bool,
4356 pub zoom: f32,
4357 pub camera_pos: Vec2,
4358 pub undo_stack: VecDeque<String>,
4359 pub redo_stack: VecDeque<String>,
4360 pub max_undo: usize,
4361}
4362
4363impl MapEditorState {
4364 pub fn new() -> Self {
4365 MapEditorState {
4366 tool_mode: MapToolMode::Select,
4367 selected_room_ids: HashSet::new(),
4368 brush_size: 1.0,
4369 paint_tile_id: 0,
4370 grid_size: 1.0,
4371 snap_to_grid: true,
4372 show_grid: true,
4373 show_collision: false,
4374 show_navmesh: false,
4375 show_lighting: true,
4376 zoom: 1.0,
4377 camera_pos: Vec2::ZERO,
4378 undo_stack: VecDeque::new(),
4379 redo_stack: VecDeque::new(),
4380 max_undo: 100,
4381 }
4382 }
4383
4384 pub fn push_undo(&mut self, description: String) {
4385 if self.undo_stack.len() >= self.max_undo {
4386 self.undo_stack.pop_front();
4387 }
4388 self.undo_stack.push_back(description);
4389 self.redo_stack.clear();
4390 }
4391
4392 pub fn undo(&mut self) -> Option<String> {
4393 if let Some(action) = self.undo_stack.pop_back() {
4394 self.redo_stack.push_back(action.clone());
4395 Some(action)
4396 } else {
4397 None
4398 }
4399 }
4400
4401 pub fn redo(&mut self) -> Option<String> {
4402 if let Some(action) = self.redo_stack.pop_back() {
4403 self.undo_stack.push_back(action.clone());
4404 Some(action)
4405 } else {
4406 None
4407 }
4408 }
4409
4410 pub fn snap_position(&self, pos: Vec2) -> Vec2 {
4411 if !self.snap_to_grid { return pos; }
4412 let g = self.grid_size;
4413 Vec2::new((pos.x / g).round() * g, (pos.y / g).round() * g)
4414 }
4415
4416 pub fn world_to_screen(&self, world_pos: Vec2, viewport_size: Vec2) -> Vec2 {
4417 (world_pos - self.camera_pos) * self.zoom + viewport_size * 0.5
4418 }
4419
4420 pub fn screen_to_world(&self, screen_pos: Vec2, viewport_size: Vec2) -> Vec2 {
4421 (screen_pos - viewport_size * 0.5) / self.zoom + self.camera_pos
4422 }
4423}
4424
4425#[derive(Debug, Clone)]
4430pub struct NavPoly {
4431 pub id: u32,
4432 pub vertices: Vec<Vec2>,
4433 pub neighbors: Vec<u32>,
4434 pub walkable: bool,
4435 pub cost: f32,
4436 pub flags: u32,
4437}
4438
4439impl NavPoly {
4440 pub fn new(id: u32) -> Self {
4441 NavPoly { id, vertices: Vec::new(), neighbors: Vec::new(), walkable: true, cost: 1.0, flags: 0 }
4442 }
4443
4444 pub fn center(&self) -> Vec2 {
4445 if self.vertices.is_empty() { return Vec2::ZERO; }
4446 let sum = self.vertices.iter().fold(Vec2::ZERO, |a, &b| a + b);
4447 sum / self.vertices.len() as f32
4448 }
4449
4450 pub fn area(&self) -> f32 {
4451 let n = self.vertices.len();
4452 if n < 3 { return 0.0; }
4453 let mut area = 0.0;
4454 for i in 0..n {
4455 let j = (i + 1) % n;
4456 area += self.vertices[i].x * self.vertices[j].y;
4457 area -= self.vertices[j].x * self.vertices[i].y;
4458 }
4459 area.abs() * 0.5
4460 }
4461
4462 pub fn contains_point(&self, p: Vec2) -> bool {
4463 let n = self.vertices.len();
4464 let mut inside = false;
4465 let mut j = n - 1;
4466 for i in 0..n {
4467 let vi = self.vertices[i];
4468 let vj = self.vertices[j];
4469 if ((vi.y > p.y) != (vj.y > p.y))
4470 && (p.x < (vj.x - vi.x) * (p.y - vi.y) / (vj.y - vi.y) + vi.x)
4471 {
4472 inside = !inside;
4473 }
4474 j = i;
4475 }
4476 inside
4477 }
4478}
4479
4480#[derive(Debug, Clone)]
4481pub struct NavMesh2D {
4482 pub polys: HashMap<u32, NavPoly>,
4483 pub next_id: u32,
4484}
4485
4486impl NavMesh2D {
4487 pub fn new() -> Self {
4488 NavMesh2D { polys: HashMap::new(), next_id: 0 }
4489 }
4490
4491 pub fn add_poly(&mut self, poly: NavPoly) {
4492 let id = poly.id;
4493 self.polys.insert(id, poly);
4494 if id >= self.next_id { self.next_id = id + 1; }
4495 }
4496
4497 pub fn poly_at_point(&self, p: Vec2) -> Option<&NavPoly> {
4498 self.polys.values().find(|poly| poly.walkable && poly.contains_point(p))
4499 }
4500
4501 pub fn astar_path(&self, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
4502 let start_poly = self.poly_at_point(start)?;
4503 let end_poly = self.poly_at_point(end)?;
4504 if start_poly.id == end_poly.id {
4505 return Some(vec![start, end]);
4506 }
4507 let mut open: Vec<u32> = vec![start_poly.id];
4508 let mut came_from: HashMap<u32, u32> = HashMap::new();
4509 let mut g_score: HashMap<u32, f32> = HashMap::new();
4510 g_score.insert(start_poly.id, 0.0);
4511 while !open.is_empty() {
4512 let curr = *open.iter().min_by(|&&a, &&b| {
4513 let fa = g_score.get(&a).unwrap_or(&f32::INFINITY);
4514 let fb = g_score.get(&b).unwrap_or(&f32::INFINITY);
4515 fa.partial_cmp(fb).unwrap()
4516 })?;
4517 if curr == end_poly.id {
4518 let mut path_polys = vec![curr];
4519 let mut c = curr;
4520 while let Some(&p) = came_from.get(&c) {
4521 path_polys.push(p);
4522 c = p;
4523 }
4524 path_polys.reverse();
4525 let mut waypoints: Vec<Vec2> = path_polys.iter()
4526 .filter_map(|id| self.polys.get(id))
4527 .map(|p| p.center())
4528 .collect();
4529 if !waypoints.is_empty() { waypoints[0] = start; }
4530 waypoints.push(end);
4531 return Some(waypoints);
4532 }
4533 open.retain(|&id| id != curr);
4534 if let Some(poly) = self.polys.get(&curr) {
4535 let curr_g = *g_score.get(&curr).unwrap_or(&f32::INFINITY);
4536 for &nbr in &poly.neighbors {
4537 let nbr_poly = match self.polys.get(&nbr) { Some(p) => p, None => continue };
4538 if !nbr_poly.walkable { continue; }
4539 let tentative = curr_g + nbr_poly.cost
4540 + (nbr_poly.center() - poly.center()).length();
4541 if tentative < *g_score.get(&nbr).unwrap_or(&f32::INFINITY) {
4542 g_score.insert(nbr, tentative);
4543 came_from.insert(nbr, curr);
4544 if !open.contains(&nbr) { open.push(nbr); }
4545 }
4546 }
4547 }
4548 }
4549 None
4550 }
4551}
4552
4553#[derive(Debug, Clone)]
4558pub struct ExtTileDef {
4559 pub id: u32,
4560 pub name: String,
4561 pub sprite_x: u32,
4562 pub sprite_y: u32,
4563 pub sprite_width: u32,
4564 pub sprite_height: u32,
4565 pub passable: bool,
4566 pub blocks_sight: bool,
4567 pub light_transmission: f32,
4568 pub movement_cost: f32,
4569 pub tags: Vec<String>,
4570 pub autotile_group: Option<u32>,
4571}
4572
4573impl ExtTileDef {
4574 pub fn new(id: u32, name: &str) -> Self {
4575 ExtTileDef {
4576 id, name: name.to_string(),
4577 sprite_x: 0, sprite_y: 0,
4578 sprite_width: 16, sprite_height: 16,
4579 passable: true, blocks_sight: false,
4580 light_transmission: 1.0,
4581 movement_cost: 1.0,
4582 tags: Vec::new(),
4583 autotile_group: None,
4584 }
4585 }
4586}
4587
4588#[derive(Debug, Clone)]
4589pub struct ExtTileset {
4590 pub id: u32,
4591 pub name: String,
4592 pub texture_path: String,
4593 pub tile_width: u32,
4594 pub tile_height: u32,
4595 pub tiles: HashMap<u32, ExtTileDef>,
4596}
4597
4598impl ExtTileset {
4599 pub fn new(id: u32, name: &str, texture: &str, tw: u32, th: u32) -> Self {
4600 ExtTileset { id, name: name.to_string(), texture_path: texture.to_string(), tile_width: tw, tile_height: th, tiles: HashMap::new() }
4601 }
4602
4603 pub fn add_tile(&mut self, tile: ExtTileDef) {
4604 self.tiles.insert(tile.id, tile);
4605 }
4606
4607 pub fn tile_by_name(&self, name: &str) -> Option<&ExtTileDef> {
4608 self.tiles.values().find(|t| t.name == name)
4609 }
4610}
4611
4612#[derive(Debug, Clone, PartialEq)]
4617pub enum MapObjectType {
4618 Door, Chest, Trap, Altar, Statue, Torch, Barrel, Bookshelf,
4619 Bed, Chair, Table, Pillar, Wall, Floor, Ceiling,
4620 NpcSpawn, EnemySpawn, BossSpawn, PlayerSpawn,
4621 Portal, Trigger, Light, Sound, Particle,
4622 Lever, Button, Pressure_Plate, Key,
4623}
4624
4625#[derive(Debug, Clone)]
4626pub struct MapObject {
4627 pub id: u32,
4628 pub object_type: MapObjectType,
4629 pub position: Vec3,
4630 pub rotation_deg: f32,
4631 pub scale: Vec3,
4632 pub asset_id: String,
4633 pub properties: HashMap<String, String>,
4634 pub interactive: bool,
4635 pub visible: bool,
4636 pub layer: u32,
4637}
4638
4639impl MapObject {
4640 pub fn new(id: u32, object_type: MapObjectType, pos: Vec3, asset: &str) -> Self {
4641 MapObject {
4642 id, object_type, position: pos,
4643 rotation_deg: 0.0, scale: Vec3::ONE,
4644 asset_id: asset.to_string(),
4645 properties: HashMap::new(),
4646 interactive: false, visible: true, layer: 0,
4647 }
4648 }
4649
4650 pub fn set_property(&mut self, key: &str, value: &str) {
4651 self.properties.insert(key.to_string(), value.to_string());
4652 }
4653
4654 pub fn get_property(&self, key: &str) -> Option<&str> {
4655 self.properties.get(key).map(|s| s.as_str())
4656 }
4657
4658 pub fn transform_matrix(&self) -> Mat4 {
4659 let rot = Mat4::from_rotation_y(self.rotation_deg.to_radians());
4660 let trans = Mat4::from_translation(self.position);
4661 let scale = Mat4::from_scale(self.scale);
4662 trans * rot * scale
4663 }
4664}
4665
4666#[derive(Debug, Clone, PartialEq)]
4671pub enum LightType {
4672 Point, Spot, Directional, Area, Ambient,
4673}
4674
4675#[derive(Debug, Clone)]
4676pub struct SceneLight {
4677 pub id: u32,
4678 pub light_type: LightType,
4679 pub position: Vec3,
4680 pub direction: Vec3,
4681 pub color: Vec3,
4682 pub intensity: f32,
4683 pub radius: f32,
4684 pub inner_cone_deg: f32,
4685 pub outer_cone_deg: f32,
4686 pub cast_shadows: bool,
4687 pub shadow_bias: f32,
4688 pub baked: bool,
4689 pub static_light: bool,
4690}
4691
4692impl SceneLight {
4693 pub fn new_point(id: u32, pos: Vec3, color: Vec3, intensity: f32, radius: f32) -> Self {
4694 SceneLight {
4695 id, light_type: LightType::Point, position: pos,
4696 direction: Vec3::NEG_Y, color, intensity, radius,
4697 inner_cone_deg: 30.0, outer_cone_deg: 45.0,
4698 cast_shadows: true, shadow_bias: 0.005,
4699 baked: false, static_light: false,
4700 }
4701 }
4702
4703 pub fn new_directional(id: u32, dir: Vec3, color: Vec3, intensity: f32) -> Self {
4704 SceneLight {
4705 id, light_type: LightType::Directional, position: Vec3::ZERO,
4706 direction: dir.normalize(), color, intensity, radius: f32::INFINITY,
4707 inner_cone_deg: 0.0, outer_cone_deg: 180.0,
4708 cast_shadows: true, shadow_bias: 0.002,
4709 baked: false, static_light: true,
4710 }
4711 }
4712
4713 pub fn attenuation(&self, distance: f32) -> f32 {
4714 match self.light_type {
4715 LightType::Point | LightType::Spot => {
4716 let d = distance.max(0.001);
4717 (1.0 - (d / self.radius).clamp(0.0, 1.0)).powi(2)
4718 },
4719 LightType::Directional | LightType::Ambient | LightType::Area => 1.0,
4720 }
4721 }
4722
4723 pub fn illuminate(&self, point: Vec3, normal: Vec3) -> Vec3 {
4724 match self.light_type {
4725 LightType::Directional => {
4726 let ndotl = normal.dot(-self.direction).max(0.0);
4727 self.color * self.intensity * ndotl
4728 },
4729 LightType::Point => {
4730 let diff = self.position - point;
4731 let dist = diff.length();
4732 let dir = diff / dist.max(0.001);
4733 let ndotl = normal.dot(dir).max(0.0);
4734 let att = self.attenuation(dist);
4735 self.color * self.intensity * ndotl * att
4736 },
4737 _ => Vec3::ZERO,
4738 }
4739 }
4740}
4741
4742#[derive(Debug, Clone)]
4743pub struct LightmapPatch {
4744 pub position: Vec3,
4745 pub normal: Vec3,
4746 pub baked_color: Vec3,
4747 pub direct_color: Vec3,
4748 pub indirect_color: Vec3,
4749}
4750
4751pub struct LightBaker {
4752 pub lights: Vec<SceneLight>,
4753 pub ambient: Vec3,
4754 pub num_bounces: u32,
4755}
4756
4757impl LightBaker {
4758 pub fn new(ambient: Vec3) -> Self {
4759 LightBaker { lights: Vec::new(), ambient, num_bounces: 2 }
4760 }
4761
4762 pub fn bake_patch(&self, patch: &LightmapPatch) -> Vec3 {
4763 let mut total = self.ambient;
4764 for light in &self.lights {
4765 total += light.illuminate(patch.position, patch.normal);
4766 }
4767 total
4768 }
4769
4770 pub fn bake_patches(&self, patches: &mut Vec<LightmapPatch>) {
4771 for patch in patches.iter_mut() {
4772 patch.baked_color = self.bake_patch(patch);
4773 }
4774 }
4775}
4776
4777#[cfg(test)]
4782mod tests_map_additions {
4783 use super::*;
4784
4785 #[test]
4786 fn test_dungeon_room_area() {
4787 let room = ExtDungeonRoom::new(1, ExtRoomShape::Rectangular, RoomTheme::Stone, 10, 8);
4788 assert_eq!(room.area(), 80.0);
4789 }
4790
4791 #[test]
4792 fn test_dungeon_floor_bfs() {
4793 let mut floor = DungeonFloor::new(1, 100, 100);
4794 floor.add_room(ExtDungeonRoom::new(0, ExtRoomShape::Rectangular, RoomTheme::Stone, 5, 5));
4795 floor.add_room(ExtDungeonRoom::new(1, ExtRoomShape::Rectangular, RoomTheme::Stone, 5, 5));
4796 floor.add_room(ExtDungeonRoom::new(2, ExtRoomShape::Rectangular, RoomTheme::Stone, 5, 5));
4797 floor.connect_rooms(0, 1);
4798 floor.connect_rooms(1, 2);
4799 assert_eq!(floor.bfs_distance(0, 2), Some(2));
4800 assert_eq!(floor.bfs_distance(0, 0), Some(0));
4801 }
4802
4803 #[test]
4804 fn test_dungeon_generator() {
4805 let params = ExtDungeonGenParams { min_rooms: 5, max_rooms: 10, ..Default::default() };
4806 let mut gen = ExtDungeonGenerator::new(params);
4807 let floor = gen.generate(1);
4808 assert!(floor.room_count() >= 5);
4809 assert!(floor.room_count() <= 10);
4810 }
4811
4812 #[test]
4813 fn test_world_region_traversal() {
4814 let plains = WorldRegion::new(1, "Greenfield", ExtBiomeType::Plains);
4815 let mountains = WorldRegion::new(2, "Stonepeaks", ExtBiomeType::Mountains);
4816 assert!(plains.traversal_cost() < mountains.traversal_cost());
4817 assert!(plains.is_habitable());
4818 }
4819
4820 #[test]
4821 fn test_faction_relations() {
4822 let mut f = Faction::new(1, "Kingdom of Arath", FactionType::Kingdom);
4823 f.set_relation(2, 75.0);
4824 assert!(f.is_allied_with(2));
4825 f.set_relation(3, -80.0);
4826 assert!(f.is_enemy_of(3));
4827 }
4828
4829 #[test]
4830 fn test_quest_progress() {
4831 let mut q = ExtQuest::new(1, "Kill 10 Wolves", QuestType::Bounty);
4832 q.add_objective(QuestObjective { id: 0, description: "Kill wolves".to_string(), current: 7, required: 10, optional: false, complete: false });
4833 assert!((q.overall_progress() - 0.7).abs() < 0.01);
4834 assert!(!q.is_complete());
4835 }
4836
4837 #[test]
4838 fn test_nav_poly_contains() {
4839 let mut poly = NavPoly::new(0);
4840 poly.vertices = vec![
4841 Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0),
4842 Vec2::new(4.0, 4.0), Vec2::new(0.0, 4.0),
4843 ];
4844 assert!(poly.contains_point(Vec2::new(2.0, 2.0)));
4845 assert!(!poly.contains_point(Vec2::new(5.0, 5.0)));
4846 }
4847
4848 #[test]
4849 fn test_map_editor_snap() {
4850 let state = MapEditorState::new();
4851 let snapped = state.snap_position(Vec2::new(1.4, 2.7));
4852 assert!((snapped.x - 1.0).abs() < 0.001);
4853 assert!((snapped.y - 3.0).abs() < 0.001);
4854 }
4855
4856 #[test]
4857 fn test_light_attenuation() {
4858 let light = SceneLight::new_point(1, Vec3::ZERO, Vec3::ONE, 100.0, 10.0);
4859 let att_near = light.attenuation(1.0);
4860 let att_far = light.attenuation(9.0);
4861 assert!(att_near > att_far);
4862 }
4863
4864 #[test]
4865 fn test_encounter_threat() {
4866 let mut enc = Encounter::new(1, "Wolf Pack", EncounterDifficulty::Medium, 5);
4867 enc.enemies.push(EncounterEnemy { monster_id: 10, count: 5, level_override: None, loot_bonus: 1.0 });
4868 assert_eq!(enc.total_enemy_count(), 5);
4869 assert!(enc.threat_rating() > 0.0);
4870 }
4871}
4872
4873pub fn map_editor_module_version() -> &'static str { "2.5.0" }
4874pub fn map_editor_features() -> &'static [&'static str] {
4875 &["dungeon_gen", "world_map", "factions", "quests", "encounters", "navmesh", "lighting", "tiles"]
4876}
4877
4878
4879#[derive(Debug, Clone)]
4885pub struct WeatherState {
4886 pub weather_type: WeatherType,
4887 pub temperature_celsius: f32,
4888 pub wind_speed_kph: f32,
4889 pub wind_direction_deg: f32,
4890 pub humidity_pct: f32,
4891 pub visibility_km: f32,
4892 pub precipitation_mm_hr: f32,
4893 pub cloud_cover_pct: f32,
4894 pub fog_density: f32,
4895}
4896
4897impl WeatherState {
4898 pub fn clear_day() -> Self {
4899 WeatherState {
4900 weather_type: WeatherType::Clear,
4901 temperature_celsius: 22.0, wind_speed_kph: 10.0,
4902 wind_direction_deg: 270.0, humidity_pct: 45.0,
4903 visibility_km: 30.0, precipitation_mm_hr: 0.0,
4904 cloud_cover_pct: 10.0, fog_density: 0.0,
4905 }
4906 }
4907
4908 pub fn heavy_storm() -> Self {
4909 WeatherState {
4910 weather_type: WeatherType::Thunderstorm,
4911 temperature_celsius: 14.0, wind_speed_kph: 60.0,
4912 wind_direction_deg: 180.0, humidity_pct: 95.0,
4913 visibility_km: 1.0, precipitation_mm_hr: 40.0,
4914 cloud_cover_pct: 100.0, fog_density: 0.0,
4915 }
4916 }
4917
4918 pub fn movement_penalty(&self) -> f32 {
4919 match self.weather_type {
4920 WeatherType::Clear | WeatherType::Cloudy => 1.0,
4921 WeatherType::LightRain | WeatherType::LightSnow => 1.2,
4922 WeatherType::HeavyRain | WeatherType::Overcast => 1.4,
4923 WeatherType::Thunderstorm | WeatherType::Fog => 1.8,
4924 WeatherType::HeavySnow | WeatherType::Blizzard => 2.5,
4925 WeatherType::Sandstorm => 2.0,
4926 _ => 1.5,
4927 }
4928 }
4929
4930 pub fn ambient_light_factor(&self) -> f32 {
4931 let cloud = 1.0 - self.cloud_cover_pct / 100.0 * 0.7;
4932 let fog = 1.0 - self.fog_density * 0.8;
4933 cloud * fog
4934 }
4935}
4936
4937#[derive(Debug, Clone)]
4938pub struct WeatherForecast {
4939 pub entries: Vec<(f32, WeatherState)>,
4940}
4941
4942impl WeatherForecast {
4943 pub fn new() -> Self {
4944 WeatherForecast { entries: Vec::new() }
4945 }
4946
4947 pub fn add_entry(&mut self, time: f32, state: WeatherState) {
4948 self.entries.push((time, state));
4949 self.entries.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
4950 }
4951
4952 pub fn weather_at(&self, time: f32) -> Option<&WeatherState> {
4953 self.entries.iter()
4954 .rev()
4955 .find(|(t, _)| *t <= time)
4956 .map(|(_, s)| s)
4957 }
4958}
4959
4960#[derive(Debug, Clone)]
4965pub struct SunPosition {
4966 pub azimuth_deg: f32,
4967 pub elevation_deg: f32,
4968 pub color: Vec3,
4969 pub intensity: f32,
4970}
4971
4972impl SunPosition {
4973 pub fn from_time_of_day(hour: f32, latitude_deg: f32) -> Self {
4974 let hour_angle = (hour - 12.0) * 15.0;
4975 let lat_rad = latitude_deg.to_radians();
4976 let declination_rad = (-23.45f32).to_radians();
4977 let h_rad = hour_angle.to_radians();
4978 let elevation = (lat_rad.sin() * declination_rad.sin()
4979 + lat_rad.cos() * declination_rad.cos() * h_rad.cos()).asin();
4980 let elevation_deg = elevation.to_degrees();
4981 let azimuth_deg = h_rad.sin().atan2(
4982 h_rad.cos() * lat_rad.sin() - declination_rad.tan() * lat_rad.cos()
4983 ).to_degrees() + 180.0;
4984
4985 let (color, intensity) = if elevation_deg < -5.0 {
4986 (Vec3::new(0.01, 0.01, 0.05), 0.0)
4987 } else if elevation_deg < 0.0 {
4988 (Vec3::new(0.3, 0.1, 0.05), 0.05)
4989 } else if elevation_deg < 15.0 {
4990 (Vec3::new(1.0, 0.4, 0.1), 0.4)
4991 } else if elevation_deg < 40.0 {
4992 (Vec3::new(1.0, 0.85, 0.6), 0.8)
4993 } else {
4994 (Vec3::new(1.0, 0.97, 0.9), 1.0)
4995 };
4996
4997 SunPosition { azimuth_deg, elevation_deg, color, intensity }
4998 }
4999
5000 pub fn direction(&self) -> Vec3 {
5001 let az = self.azimuth_deg.to_radians();
5002 let el = self.elevation_deg.to_radians();
5003 Vec3::new(-el.cos() * az.sin(), el.sin(), -el.cos() * az.cos()).normalize()
5004 }
5005
5006 pub fn is_daytime(&self) -> bool { self.elevation_deg > 0.0 }
5007}
5008
5009#[derive(Debug, Clone)]
5014pub struct Currency {
5015 pub name: String,
5016 pub symbol: String,
5017 pub conversion_to_gold: f32,
5018}
5019
5020impl Currency {
5021 pub fn gold() -> Self { Currency { name: "Gold".to_string(), symbol: "G".to_string(), conversion_to_gold: 1.0 } }
5022 pub fn silver() -> Self { Currency { name: "Silver".to_string(), symbol: "S".to_string(), conversion_to_gold: 0.1 } }
5023 pub fn copper() -> Self { Currency { name: "Copper".to_string(), symbol: "C".to_string(), conversion_to_gold: 0.01 } }
5024}
5025
5026#[derive(Debug, Clone)]
5027pub struct PriceTable {
5028 pub region_id: u32,
5029 pub prices: HashMap<String, f32>,
5030 pub supply_modifier: HashMap<String, f32>,
5031 pub demand_modifier: HashMap<String, f32>,
5032}
5033
5034impl PriceTable {
5035 pub fn new(region_id: u32) -> Self {
5036 PriceTable {
5037 region_id, prices: HashMap::new(),
5038 supply_modifier: HashMap::new(),
5039 demand_modifier: HashMap::new(),
5040 }
5041 }
5042
5043 pub fn set_base_price(&mut self, item: &str, price: f32) {
5044 self.prices.insert(item.to_string(), price);
5045 }
5046
5047 pub fn effective_price(&self, item: &str) -> f32 {
5048 let base = self.prices.get(item).copied().unwrap_or(1.0);
5049 let supply = self.supply_modifier.get(item).copied().unwrap_or(1.0);
5050 let demand = self.demand_modifier.get(item).copied().unwrap_or(1.0);
5051 base * demand / supply
5052 }
5053
5054 pub fn apply_shortage(&mut self, item: &str, factor: f32) {
5055 *self.supply_modifier.entry(item.to_string()).or_insert(1.0) /= factor.max(0.1);
5056 }
5057}
5058
5059#[derive(Debug, Clone)]
5064pub struct ExtLootEntry {
5065 pub item_id: String,
5066 pub weight: f32,
5067 pub min_quantity: u32,
5068 pub max_quantity: u32,
5069 pub condition: Option<String>,
5070}
5071
5072#[derive(Debug, Clone)]
5073pub struct ExtLootTable {
5074 pub id: u32,
5075 pub name: String,
5076 pub entries: Vec<ExtLootEntry>,
5077 pub guaranteed_entries: Vec<ExtLootEntry>,
5078 pub rolls: u32,
5079 pub luck_bonus: f32,
5080}
5081
5082impl ExtLootTable {
5083 pub fn new(id: u32, name: &str, rolls: u32) -> Self {
5084 ExtLootTable { id, name: name.to_string(), entries: Vec::new(), guaranteed_entries: Vec::new(), rolls, luck_bonus: 0.0 }
5085 }
5086
5087 pub fn add_entry(&mut self, entry: ExtLootEntry) {
5088 self.entries.push(entry);
5089 }
5090
5091 pub fn total_weight(&self) -> f32 {
5092 self.entries.iter().map(|e| e.weight).sum()
5093 }
5094
5095 pub fn roll_deterministic(&self, seed: u64, roll_index: u32) -> Vec<(&str, u32)> {
5096 let mut result = Vec::new();
5097 let mut rng = seed ^ (roll_index as u64).wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
5098 rng ^= rng >> 33; rng = rng.wrapping_mul(0xff51afd7ed558ccd); rng ^= rng >> 33;
5099 let total = self.total_weight();
5100 if total <= 0.0 { return result; }
5101 let pick = (rng as f32 / u64::MAX as f32) * total;
5102 let mut cumulative = 0.0;
5103 for entry in &self.entries {
5104 cumulative += entry.weight;
5105 if pick <= cumulative {
5106 let qty_range = entry.max_quantity - entry.min_quantity + 1;
5107 let qty = entry.min_quantity + (rng % qty_range as u64) as u32;
5108 result.push((entry.item_id.as_str(), qty));
5109 break;
5110 }
5111 }
5112 for g in &self.guaranteed_entries {
5113 result.push((g.item_id.as_str(), g.min_quantity));
5114 }
5115 result
5116 }
5117}
5118
5119#[derive(Debug, Clone, PartialEq)]
5124pub enum BehaviorStatus {
5125 Success,
5126 Failure,
5127 Running,
5128}
5129
5130#[derive(Debug, Clone, PartialEq)]
5131pub enum BehaviorNodeType {
5132 Selector,
5133 Sequence,
5134 Parallel,
5135 Decorator,
5136 Action,
5137 Condition,
5138}
5139
5140#[derive(Debug, Clone)]
5141pub struct BehaviorNode {
5142 pub id: u32,
5143 pub node_type: BehaviorNodeType,
5144 pub name: String,
5145 pub children: Vec<u32>,
5146 pub action_id: Option<String>,
5147 pub blackboard_key: Option<String>,
5148 pub inverted: bool,
5149}
5150
5151impl BehaviorNode {
5152 pub fn new_action(id: u32, name: &str, action_id: &str) -> Self {
5153 BehaviorNode {
5154 id, node_type: BehaviorNodeType::Action,
5155 name: name.to_string(), children: Vec::new(),
5156 action_id: Some(action_id.to_string()),
5157 blackboard_key: None, inverted: false,
5158 }
5159 }
5160
5161 pub fn new_sequence(id: u32, name: &str) -> Self {
5162 BehaviorNode {
5163 id, node_type: BehaviorNodeType::Sequence,
5164 name: name.to_string(), children: Vec::new(),
5165 action_id: None, blackboard_key: None, inverted: false,
5166 }
5167 }
5168
5169 pub fn new_selector(id: u32, name: &str) -> Self {
5170 BehaviorNode {
5171 id, node_type: BehaviorNodeType::Selector,
5172 name: name.to_string(), children: Vec::new(),
5173 action_id: None, blackboard_key: None, inverted: false,
5174 }
5175 }
5176}
5177
5178#[derive(Debug, Clone)]
5179pub struct BehaviorTree {
5180 pub name: String,
5181 pub nodes: HashMap<u32, BehaviorNode>,
5182 pub root_id: u32,
5183 pub blackboard: HashMap<String, String>,
5184}
5185
5186impl BehaviorTree {
5187 pub fn new(name: &str, root_id: u32) -> Self {
5188 BehaviorTree { name: name.to_string(), nodes: HashMap::new(), root_id, blackboard: HashMap::new() }
5189 }
5190
5191 pub fn add_node(&mut self, node: BehaviorNode) {
5192 self.nodes.insert(node.id, node);
5193 }
5194
5195 pub fn set_blackboard(&mut self, key: &str, value: &str) {
5196 self.blackboard.insert(key.to_string(), value.to_string());
5197 }
5198
5199 pub fn get_blackboard(&self, key: &str) -> Option<&str> {
5200 self.blackboard.get(key).map(|s| s.as_str())
5201 }
5202
5203 pub fn node_count(&self) -> usize { self.nodes.len() }
5204}
5205
5206#[derive(Debug, Clone, PartialEq)]
5211pub enum MonsterCategory {
5212 Beast, Undead, Demon, Dragon, Humanoid, Construct,
5213 Elemental, Fey, Giant, Plant, Ooze, Celestial, Aberration,
5214}
5215
5216#[derive(Debug, Clone)]
5217pub struct MonsterStats {
5218 pub hp: u32, pub max_hp: u32,
5219 pub armor_class: u32,
5220 pub attack_bonus: i32,
5221 pub damage_dice: String,
5222 pub damage_bonus: i32,
5223 pub speed_m: f32,
5224 pub strength: i32, pub dexterity: i32, pub constitution: i32,
5225 pub intelligence: i32, pub wisdom: i32, pub charisma: i32,
5226 pub challenge_rating: f32,
5227}
5228
5229impl MonsterStats {
5230 pub fn modifier(stat: i32) -> i32 { (stat - 10) / 2 }
5231 pub fn proficiency_bonus(cr: f32) -> i32 { (cr / 4.0).ceil() as i32 + 1 }
5232 pub fn xp_for_cr(cr: f32) -> u32 {
5233 match cr as u32 {
5234 0 => 10, 1 => 200, 2 => 450, 3 => 700, 4 => 1100,
5235 5 => 1800, 6 => 2300, 7 => 2900, 8 => 3900, 9 => 5000,
5236 10 => 5900, 11 => 7200, 12 => 8400, 13 => 10000, 14 => 11500,
5237 15 => 13000, 16 => 15000, 17 => 18000, 18 => 20000, _ => 25000,
5238 }
5239 }
5240}
5241
5242#[derive(Debug, Clone)]
5243pub struct MonsterDefinition {
5244 pub id: u32,
5245 pub name: String,
5246 pub category: MonsterCategory,
5247 pub stats: MonsterStats,
5248 pub loot_table_id: Option<u32>,
5249 pub abilities: Vec<String>,
5250 pub immunities: Vec<String>,
5251 pub resistances: Vec<String>,
5252 pub behavior_tree_name: Option<String>,
5253 pub sprite_asset: String,
5254 pub description: String,
5255}
5256
5257impl MonsterDefinition {
5258 pub fn xp_value(&self) -> u32 {
5259 MonsterStats::xp_for_cr(self.stats.challenge_rating)
5260 }
5261
5262 pub fn is_elite(&self) -> bool {
5263 self.stats.challenge_rating >= 5.0 && self.abilities.len() >= 3
5264 }
5265}
5266
5267pub const MAP_FILE_MAGIC: u32 = 0x4D415000; pub const MAP_FILE_VERSION: u32 = 2;
5273
5274#[derive(Debug, Clone)]
5275pub struct MapFileHeader {
5276 pub magic: u32,
5277 pub version: u32,
5278 pub width: u32,
5279 pub height: u32,
5280 pub tile_size: u32,
5281 pub layer_count: u32,
5282 pub object_count: u32,
5283 pub region_count: u32,
5284 pub author: String,
5285 pub timestamp: u64,
5286}
5287
5288impl MapFileHeader {
5289 pub fn new(width: u32, height: u32, tile_size: u32) -> Self {
5290 MapFileHeader {
5291 magic: MAP_FILE_MAGIC,
5292 version: MAP_FILE_VERSION,
5293 width, height, tile_size,
5294 layer_count: 0, object_count: 0, region_count: 0,
5295 author: String::new(), timestamp: 0,
5296 }
5297 }
5298
5299 pub fn is_valid(&self) -> bool {
5300 self.magic == MAP_FILE_MAGIC && self.version <= MAP_FILE_VERSION
5301 }
5302
5303 pub fn total_tiles(&self) -> u64 {
5304 self.width as u64 * self.height as u64
5305 }
5306}
5307
5308#[cfg(test)]
5313mod tests_map_final {
5314 use super::*;
5315
5316 #[test]
5317 fn test_weather_movement_penalty() {
5318 let clear = WeatherState::clear_day();
5319 let storm = WeatherState::heavy_storm();
5320 assert_eq!(clear.movement_penalty(), 1.0);
5321 assert!(storm.movement_penalty() > 1.0);
5322 }
5323
5324 #[test]
5325 fn test_sun_position_daytime() {
5326 let noon = SunPosition::from_time_of_day(12.0, 40.0);
5327 assert!(noon.is_daytime());
5328 let midnight = SunPosition::from_time_of_day(0.0, 40.0);
5329 assert!(!midnight.is_daytime());
5330 }
5331
5332 #[test]
5333 fn test_price_table_effective_price() {
5334 let mut pt = PriceTable::new(1);
5335 pt.set_base_price("Iron Sword", 50.0);
5336 pt.apply_shortage("Iron Sword", 2.0);
5337 let price = pt.effective_price("Iron Sword");
5338 assert!(price > 50.0);
5339 }
5340
5341 #[test]
5342 fn test_loot_table_roll() {
5343 let mut table = ExtLootTable::new(1, "CommonChest", 1);
5344 table.add_entry(ExtLootEntry {
5345 item_id: "gold_coin".to_string(), weight: 70.0,
5346 min_quantity: 5, max_quantity: 20, condition: None,
5347 });
5348 table.add_entry(ExtLootEntry {
5349 item_id: "health_potion".to_string(), weight: 30.0,
5350 min_quantity: 1, max_quantity: 2, condition: None,
5351 });
5352 let results = table.roll_deterministic(42, 0);
5353 assert!(!results.is_empty());
5354 }
5355
5356 #[test]
5357 fn test_behavior_tree_blackboard() {
5358 let mut bt = BehaviorTree::new("PatrolAI", 0);
5359 bt.set_blackboard("enemy_spotted", "false");
5360 bt.set_blackboard("patrol_complete", "true");
5361 assert_eq!(bt.get_blackboard("enemy_spotted"), Some("false"));
5362 }
5363
5364 #[test]
5365 fn test_monster_xp() {
5366 let mon = MonsterDefinition {
5367 id: 1, name: "Goblin".to_string(),
5368 category: MonsterCategory::Humanoid,
5369 stats: MonsterStats {
5370 hp: 7, max_hp: 7, armor_class: 15, attack_bonus: 4,
5371 damage_dice: "1d6".to_string(), damage_bonus: 2,
5372 speed_m: 9.0, strength: 8, dexterity: 14, constitution: 10,
5373 intelligence: 10, wisdom: 8, charisma: 8, challenge_rating: 0.25,
5374 },
5375 loot_table_id: None, abilities: Vec::new(), immunities: Vec::new(),
5376 resistances: Vec::new(), behavior_tree_name: None,
5377 sprite_asset: "goblin.png".to_string(), description: String::new(),
5378 };
5379 assert_eq!(mon.xp_value(), 10);
5380 }
5381
5382 #[test]
5383 fn test_map_file_header() {
5384 let header = MapFileHeader::new(100, 80, 16);
5385 assert!(header.is_valid());
5386 assert_eq!(header.total_tiles(), 8000);
5387 }
5388
5389 #[test]
5390 fn test_world_map_pathfind() {
5391 let mut wm = ExtWorldMap::new("Testrealm", 1000.0, 1000.0);
5392 let mut r1 = WorldRegion::new(1, "A", ExtBiomeType::Plains);
5393 r1.center = Vec2::new(100.0, 100.0);
5394 r1.adjacent_region_ids.push(2);
5395 let mut r2 = WorldRegion::new(2, "B", ExtBiomeType::Forest);
5396 r2.center = Vec2::new(200.0, 100.0);
5397 r2.adjacent_region_ids.push(1);
5398 wm.add_region(r1);
5399 wm.add_region(r2);
5400 let path = wm.pathfind_regions(1, 2);
5401 assert!(path.is_some());
5402 assert_eq!(path.unwrap().len(), 2);
5403 }
5404}
5405
5406pub fn map_editor_extended_version() -> &'static str { "2.5.1-extended" }
5407
5408
5409#[derive(Debug, Clone, PartialEq)]
5414pub enum SpellSchool {
5415 Abjuration, Conjuration, Divination, Enchantment,
5416 Evocation, Illusion, Necromancy, Transmutation, Universal,
5417}
5418
5419#[derive(Debug, Clone, PartialEq)]
5420pub enum DamageType {
5421 Physical, Fire, Cold, Lightning, Acid, Poison,
5422 Necrotic, Radiant, Force, Thunder, Psychic, Bludgeoning,
5423 Piercing, Slashing,
5424}
5425
5426#[derive(Debug, Clone)]
5427pub struct SpellEffect {
5428 pub damage_dice: String,
5429 pub damage_bonus: i32,
5430 pub damage_type: DamageType,
5431 pub healing_dice: String,
5432 pub status_effect: Option<String>,
5433 pub duration_rounds: u32,
5434 pub area_radius_m: f32,
5435}
5436
5437#[derive(Debug, Clone)]
5438pub struct Spell {
5439 pub id: u32,
5440 pub name: String,
5441 pub school: SpellSchool,
5442 pub level: u32,
5443 pub casting_time_actions: u32,
5444 pub range_m: f32,
5445 pub verbal: bool,
5446 pub somatic: bool,
5447 pub material: Option<String>,
5448 pub concentration: bool,
5449 pub ritual: bool,
5450 pub duration_description: String,
5451 pub description: String,
5452 pub effects: Vec<SpellEffect>,
5453 pub upcast_bonus_dice: Option<String>,
5454 pub classes_allowed: Vec<String>,
5455}
5456
5457impl Spell {
5458 pub fn new(id: u32, name: &str, school: SpellSchool, level: u32) -> Self {
5459 Spell {
5460 id, name: name.to_string(), school, level,
5461 casting_time_actions: 1, range_m: 9.0,
5462 verbal: true, somatic: true, material: None,
5463 concentration: false, ritual: false,
5464 duration_description: "Instantaneous".to_string(),
5465 description: String::new(),
5466 effects: Vec::new(), upcast_bonus_dice: None,
5467 classes_allowed: Vec::new(),
5468 }
5469 }
5470
5471 pub fn is_cantrip(&self) -> bool { self.level == 0 }
5472 pub fn slot_level(&self) -> u32 { self.level }
5473
5474 pub fn requires_components(&self) -> String {
5475 let mut comp = String::new();
5476 if self.verbal { comp.push('V'); }
5477 if self.somatic { if !comp.is_empty() { comp.push_str(", "); } comp.push('S'); }
5478 if self.material.is_some() { if !comp.is_empty() { comp.push_str(", "); } comp.push('M'); }
5479 comp
5480 }
5481}
5482
5483#[derive(Debug, Clone)]
5488pub struct CharacterStats {
5489 pub strength: i32, pub dexterity: i32, pub constitution: i32,
5490 pub intelligence: i32, pub wisdom: i32, pub charisma: i32,
5491}
5492
5493impl CharacterStats {
5494 pub fn modifier(stat: i32) -> i32 { (stat - 10) / 2 }
5495 pub fn str_mod(&self) -> i32 { Self::modifier(self.strength) }
5496 pub fn dex_mod(&self) -> i32 { Self::modifier(self.dexterity) }
5497 pub fn con_mod(&self) -> i32 { Self::modifier(self.constitution) }
5498 pub fn int_mod(&self) -> i32 { Self::modifier(self.intelligence) }
5499 pub fn wis_mod(&self) -> i32 { Self::modifier(self.wisdom) }
5500 pub fn cha_mod(&self) -> i32 { Self::modifier(self.charisma) }
5501
5502 pub fn point_buy_cost(&self) -> i32 {
5503 let cost = |s: i32| match s {
5504 8 => 0, 9 => 1, 10 => 2, 11 => 3, 12 => 4,
5505 13 => 5, 14 => 7, 15 => 9, _ => 100,
5506 };
5507 cost(self.strength) + cost(self.dexterity) + cost(self.constitution)
5508 + cost(self.intelligence) + cost(self.wisdom) + cost(self.charisma)
5509 }
5510}
5511
5512#[derive(Debug, Clone)]
5513pub struct Character {
5514 pub id: u32,
5515 pub name: String,
5516 pub race: String,
5517 pub class: String,
5518 pub subclass: String,
5519 pub level: u32,
5520 pub experience: u64,
5521 pub stats: CharacterStats,
5522 pub max_hp: i32, pub current_hp: i32,
5523 pub temp_hp: i32,
5524 pub armor_class: i32,
5525 pub proficiency_bonus: i32,
5526 pub speed_m: f32,
5527 pub alignment: String,
5528 pub background: String,
5529 pub known_spells: Vec<u32>,
5530 pub spell_slots: Vec<(u32, u32)>,
5531 pub features: Vec<String>,
5532 pub equipment_ids: Vec<u32>,
5533 pub gold: f32,
5534}
5535
5536impl Character {
5537 pub fn new(id: u32, name: &str) -> Self {
5538 Character {
5539 id, name: name.to_string(),
5540 race: String::new(), class: String::new(), subclass: String::new(),
5541 level: 1, experience: 0,
5542 stats: CharacterStats { strength: 10, dexterity: 10, constitution: 10, intelligence: 10, wisdom: 10, charisma: 10 },
5543 max_hp: 10, current_hp: 10, temp_hp: 0,
5544 armor_class: 10, proficiency_bonus: 2,
5545 speed_m: 9.0, alignment: "Neutral".to_string(),
5546 background: String::new(),
5547 known_spells: Vec::new(), spell_slots: Vec::new(),
5548 features: Vec::new(), equipment_ids: Vec::new(),
5549 gold: 0.0,
5550 }
5551 }
5552
5553 pub fn is_alive(&self) -> bool { self.current_hp > 0 }
5554 pub fn is_unconscious(&self) -> bool { self.current_hp <= 0 }
5555
5556 pub fn heal(&mut self, amount: i32) {
5557 self.current_hp = (self.current_hp + amount).min(self.max_hp);
5558 }
5559
5560 pub fn take_damage(&mut self, amount: i32) {
5561 let effective = (amount - self.temp_hp).max(0);
5562 self.temp_hp = (self.temp_hp - amount).max(0);
5563 self.current_hp -= effective;
5564 }
5565
5566 pub fn initiative_roll(&self, dice_roll: u32) -> i32 {
5567 dice_roll as i32 + self.stats.dex_mod()
5568 }
5569
5570 pub fn passive_perception(&self) -> i32 {
5571 10 + self.stats.wis_mod() + if self.features.contains(&"Perception Proficiency".to_string()) { self.proficiency_bonus } else { 0 }
5572 }
5573
5574 pub fn xp_to_next_level(&self) -> u64 {
5575 let thresholds: &[u64] = &[0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000];
5576 if self.level as usize >= thresholds.len() { return u64::MAX; }
5577 thresholds[self.level as usize].saturating_sub(self.experience)
5578 }
5579}
5580
5581impl ItemRarity {
5586 pub fn gold_value_modifier(&self) -> f32 {
5587 match self {
5588 ItemRarity::Common => 1.0,
5589 ItemRarity::Uncommon => 5.0,
5590 ItemRarity::Rare => 25.0,
5591 ItemRarity::Epic => 100.0,
5592 ItemRarity::VeryRare => 250.0,
5593 ItemRarity::Legendary => 5000.0,
5594 ItemRarity::Artifact => f32::INFINITY,
5595 }
5596 }
5597}
5598
5599#[derive(Debug, Clone, PartialEq)]
5600pub enum ItemCategory {
5601 Weapon, Armor, Shield, Ring, Necklace, Cloak, Boots,
5602 Helmet, Gloves, Belt, Wand, Staff, Scroll, Potion,
5603 Gem, QuestItem, Consumable, Tool, Mount, Vehicle,
5604}
5605
5606#[derive(Debug, Clone)]
5607pub struct Item {
5608 pub id: u32,
5609 pub name: String,
5610 pub category: ItemCategory,
5611 pub rarity: ItemRarity,
5612 pub weight_kg: f32,
5613 pub base_value: f32,
5614 pub description: String,
5615 pub properties: HashMap<String, String>,
5616 pub requires_attunement: bool,
5617 pub attuned_to: Option<u32>,
5618 pub charges: Option<u32>,
5619 pub max_charges: Option<u32>,
5620 pub spell_trigger: Option<u32>,
5621}
5622
5623impl Item {
5624 pub fn new(id: u32, name: &str, category: ItemCategory, rarity: ItemRarity) -> Self {
5625 Item {
5626 id, name: name.to_string(), category, rarity,
5627 weight_kg: 1.0, base_value: 10.0, description: String::new(),
5628 properties: HashMap::new(), requires_attunement: false,
5629 attuned_to: None, charges: None, max_charges: None, spell_trigger: None,
5630 }
5631 }
5632
5633 pub fn effective_value(&self) -> f32 {
5634 self.base_value * self.rarity.gold_value_modifier()
5635 }
5636
5637 pub fn use_charge(&mut self) -> bool {
5638 if let Some(ref mut c) = self.charges {
5639 if *c > 0 { *c -= 1; true } else { false }
5640 } else { true }
5641 }
5642
5643 pub fn is_depleted(&self) -> bool {
5644 self.charges.map(|c| c == 0).unwrap_or(false)
5645 }
5646}
5647
5648#[derive(Debug, Clone)]
5653pub struct CraftingIngredient {
5654 pub item_id: u32,
5655 pub quantity: u32,
5656 pub consumed: bool,
5657}
5658
5659#[derive(Debug, Clone)]
5660pub struct CraftingRecipe {
5661 pub recipe_id: u32,
5662 pub name: String,
5663 pub output_item_id: u32,
5664 pub output_quantity: u32,
5665 pub ingredients: Vec<CraftingIngredient>,
5666 pub required_tools: Vec<String>,
5667 pub skill_required: String,
5668 pub skill_level: u32,
5669 pub time_minutes: f32,
5670 pub experience_granted: u32,
5671}
5672
5673impl CraftingRecipe {
5674 pub fn new(recipe_id: u32, name: &str, output_item: u32) -> Self {
5675 CraftingRecipe {
5676 recipe_id, name: name.to_string(),
5677 output_item_id: output_item, output_quantity: 1,
5678 ingredients: Vec::new(), required_tools: Vec::new(),
5679 skill_required: "Crafting".to_string(), skill_level: 1,
5680 time_minutes: 10.0, experience_granted: 100,
5681 }
5682 }
5683
5684 pub fn add_ingredient(&mut self, item_id: u32, quantity: u32, consumed: bool) {
5685 self.ingredients.push(CraftingIngredient { item_id, quantity, consumed });
5686 }
5687
5688 pub fn consumed_ingredients(&self) -> Vec<&CraftingIngredient> {
5689 self.ingredients.iter().filter(|i| i.consumed).collect()
5690 }
5691}
5692
5693#[derive(Debug, Clone)]
5698pub struct DialogueCondition {
5699 pub condition_type: String,
5700 pub target: String,
5701 pub operator: String,
5702 pub value: String,
5703}
5704
5705impl DialogueCondition {
5706 pub fn check_flag(flag: &str, expected: bool) -> Self {
5707 DialogueCondition {
5708 condition_type: "flag".to_string(),
5709 target: flag.to_string(),
5710 operator: "==".to_string(),
5711 value: expected.to_string(),
5712 }
5713 }
5714
5715 pub fn check_level(min_level: u32) -> Self {
5716 DialogueCondition {
5717 condition_type: "level".to_string(),
5718 target: "player".to_string(),
5719 operator: ">=".to_string(),
5720 value: min_level.to_string(),
5721 }
5722 }
5723}
5724
5725#[derive(Debug, Clone)]
5726pub struct DialogueAction {
5727 pub action_type: String,
5728 pub target: String,
5729 pub value: String,
5730}
5731
5732impl DialogueAction {
5733 pub fn set_flag(flag: &str, value: bool) -> Self {
5734 DialogueAction { action_type: "set_flag".to_string(), target: flag.to_string(), value: value.to_string() }
5735 }
5736
5737 pub fn give_item(item_id: u32) -> Self {
5738 DialogueAction { action_type: "give_item".to_string(), target: "player".to_string(), value: item_id.to_string() }
5739 }
5740
5741 pub fn give_xp(amount: u32) -> Self {
5742 DialogueAction { action_type: "give_xp".to_string(), target: "player".to_string(), value: amount.to_string() }
5743 }
5744}
5745
5746pub const CHUNK_SIZE: u32 = 64;
5751pub const CHUNK_LOAD_RADIUS: u32 = 3;
5752
5753#[derive(Debug, Clone)]
5754pub struct ExtMapChunk {
5755 pub chunk_x: i32,
5756 pub chunk_y: i32,
5757 pub loaded: bool,
5758 pub dirty: bool,
5759 pub tile_data: Vec<u32>,
5760 pub object_ids: Vec<u32>,
5761 pub light_data: Vec<Vec3>,
5762 pub last_accessed_frame: u64,
5763}
5764
5765impl ExtMapChunk {
5766 pub fn new(cx: i32, cy: i32) -> Self {
5767 let size = (CHUNK_SIZE * CHUNK_SIZE) as usize;
5768 ExtMapChunk {
5769 chunk_x: cx, chunk_y: cy,
5770 loaded: false, dirty: false,
5771 tile_data: vec![0u32; size],
5772 object_ids: Vec::new(),
5773 light_data: vec![Vec3::ZERO; size],
5774 last_accessed_frame: 0,
5775 }
5776 }
5777
5778 pub fn get_tile(&self, local_x: u32, local_y: u32) -> u32 {
5779 if local_x >= CHUNK_SIZE || local_y >= CHUNK_SIZE { return 0; }
5780 self.tile_data[(local_y * CHUNK_SIZE + local_x) as usize]
5781 }
5782
5783 pub fn set_tile(&mut self, local_x: u32, local_y: u32, tile_id: u32) {
5784 if local_x >= CHUNK_SIZE || local_y >= CHUNK_SIZE { return; }
5785 self.tile_data[(local_y * CHUNK_SIZE + local_x) as usize] = tile_id;
5786 self.dirty = true;
5787 }
5788
5789 pub fn world_position(&self) -> Vec2 {
5790 Vec2::new(self.chunk_x as f32 * CHUNK_SIZE as f32, self.chunk_y as f32 * CHUNK_SIZE as f32)
5791 }
5792}
5793
5794#[derive(Debug, Clone)]
5795pub struct ExtChunkManager {
5796 pub loaded_chunks: HashMap<(i32, i32), ExtMapChunk>,
5797 pub player_chunk: (i32, i32),
5798 pub frame: u64,
5799 pub max_loaded_chunks: usize,
5800}
5801
5802impl ExtChunkManager {
5803 pub fn new() -> Self {
5804 ExtChunkManager {
5805 loaded_chunks: HashMap::new(),
5806 player_chunk: (0, 0),
5807 frame: 0,
5808 max_loaded_chunks: 64,
5809 }
5810 }
5811
5812 pub fn world_to_chunk(world_x: f32, world_y: f32) -> (i32, i32) {
5813 (
5814 (world_x / CHUNK_SIZE as f32).floor() as i32,
5815 (world_y / CHUNK_SIZE as f32).floor() as i32,
5816 )
5817 }
5818
5819 pub fn get_tile(&self, world_x: f32, world_y: f32) -> u32 {
5820 let (cx, cy) = Self::world_to_chunk(world_x, world_y);
5821 let local_x = (world_x as i32 - cx * CHUNK_SIZE as i32) as u32;
5822 let local_y = (world_y as i32 - cy * CHUNK_SIZE as i32) as u32;
5823 self.loaded_chunks.get(&(cx, cy))
5824 .map(|c| c.get_tile(local_x, local_y))
5825 .unwrap_or(0)
5826 }
5827
5828 pub fn required_chunks(&self) -> Vec<(i32, i32)> {
5829 let (px, py) = self.player_chunk;
5830 let r = CHUNK_LOAD_RADIUS as i32;
5831 let mut required = Vec::new();
5832 for dy in -r..=r {
5833 for dx in -r..=r {
5834 required.push((px + dx, py + dy));
5835 }
5836 }
5837 required
5838 }
5839
5840 pub fn evict_distant_chunks(&mut self) {
5841 if self.loaded_chunks.len() <= self.max_loaded_chunks { return; }
5842 let (px, py) = self.player_chunk;
5843 let mut chunks_with_dist: Vec<((i32, i32), i32)> = self.loaded_chunks.keys()
5844 .map(|&(cx, cy)| ((cx, cy), (cx - px).abs() + (cy - py).abs()))
5845 .collect();
5846 chunks_with_dist.sort_by_key(|(_, d)| -(*d));
5847 let to_remove = chunks_with_dist.len() - self.max_loaded_chunks;
5848 for i in 0..to_remove {
5849 self.loaded_chunks.remove(&chunks_with_dist[i].0);
5850 }
5851 }
5852}
5853
5854#[cfg(test)]
5859mod tests_map_final2 {
5860 use super::*;
5861
5862 #[test]
5863 fn test_spell_components() {
5864 let spell = Spell::new(1, "Fireball", SpellSchool::Evocation, 3);
5865 let comp = spell.requires_components();
5866 assert!(comp.contains('V'));
5867 assert!(comp.contains('S'));
5868 }
5869
5870 #[test]
5871 fn test_character_healing() {
5872 let mut c = Character::new(1, "Aragorn");
5873 c.max_hp = 40;
5874 c.current_hp = 10;
5875 c.heal(15);
5876 assert_eq!(c.current_hp, 25);
5877 c.heal(100);
5878 assert_eq!(c.current_hp, 40);
5879 }
5880
5881 #[test]
5882 fn test_character_damage() {
5883 let mut c = Character::new(1, "Legolas");
5884 c.max_hp = 30;
5885 c.current_hp = 30;
5886 c.take_damage(35);
5887 assert!(!c.is_alive());
5888 }
5889
5890 #[test]
5891 fn test_item_effective_value() {
5892 let item = Item::new(1, "Longsword +2", ItemCategory::Weapon, ItemRarity::Rare);
5893 assert!(item.effective_value() > item.base_value);
5894 }
5895
5896 #[test]
5897 fn test_chunk_manager_tile() {
5898 let mut mgr = ExtChunkManager::new();
5899 let mut chunk = ExtMapChunk::new(0, 0);
5900 chunk.set_tile(3, 5, 42);
5901 mgr.loaded_chunks.insert((0, 0), chunk);
5902 assert_eq!(mgr.get_tile(3.0, 5.0), 42);
5903 }
5904
5905 #[test]
5906 fn test_chunk_required() {
5907 let mgr = ExtChunkManager::new();
5908 let required = mgr.required_chunks();
5909 assert_eq!(required.len(), (2 * CHUNK_LOAD_RADIUS as usize + 1).pow(2));
5910 }
5911
5912 #[test]
5913 fn test_loot_roll() {
5914 let mut table = ExtLootTable::new(1, "Test", 1);
5915 table.add_entry(ExtLootEntry {
5916 item_id: "gold".to_string(), weight: 100.0,
5917 min_quantity: 1, max_quantity: 10, condition: None,
5918 });
5919 let results = table.roll_deterministic(1234, 0);
5920 assert!(!results.is_empty());
5921 }
5922
5923 #[test]
5924 fn test_crafting_recipe() {
5925 let mut recipe = CraftingRecipe::new(1, "Iron Sword", 100);
5926 recipe.add_ingredient(50, 2, true);
5927 recipe.add_ingredient(51, 1, false);
5928 assert_eq!(recipe.consumed_ingredients().len(), 1);
5929 }
5930
5931 #[test]
5932 fn test_stats_point_buy() {
5933 let stats = CharacterStats { strength: 15, dexterity: 14, constitution: 13, intelligence: 12, wisdom: 10, charisma: 8 };
5934 assert_eq!(stats.point_buy_cost(), 27);
5935 }
5936}
5937
5938pub const MAP_MAX_OBJECTS: u32 = 10000;
5939pub const MAP_TILE_SIZE_DEFAULT: u32 = 16;
5940pub const MAP_EDITOR_VERSION: &str = "2.5.0";
5941
5942
5943#[derive(Debug, Clone)]
5948pub struct RandomEncounterEntry {
5949 pub weight: u32,
5950 pub encounter_id: u32,
5951 pub min_party_level: u32,
5952 pub max_party_level: u32,
5953 pub time_of_day: Option<String>,
5954 pub weather_conditions: Vec<String>,
5955}
5956
5957#[derive(Debug, Clone)]
5958pub struct RandomEncounterTable {
5959 pub table_id: u32,
5960 pub region_id: u32,
5961 pub entries: Vec<RandomEncounterEntry>,
5962 pub check_interval_minutes: f32,
5963 pub base_encounter_chance_pct: f32,
5964}
5965
5966impl RandomEncounterTable {
5967 pub fn new(table_id: u32, region_id: u32) -> Self {
5968 RandomEncounterTable {
5969 table_id, region_id,
5970 entries: Vec::new(),
5971 check_interval_minutes: 30.0,
5972 base_encounter_chance_pct: 20.0,
5973 }
5974 }
5975
5976 pub fn add_entry(&mut self, entry: RandomEncounterEntry) {
5977 self.entries.push(entry);
5978 }
5979
5980 pub fn total_weight(&self, party_level: u32) -> u32 {
5981 self.entries.iter()
5982 .filter(|e| party_level >= e.min_party_level && party_level <= e.max_party_level)
5983 .map(|e| e.weight)
5984 .sum()
5985 }
5986
5987 pub fn roll(& self, party_level: u32, rng_value: u32) -> Option<u32> {
5988 let total = self.total_weight(party_level);
5989 if total == 0 { return None; }
5990 let pick = rng_value % total;
5991 let mut cumulative = 0u32;
5992 for entry in &self.entries {
5993 if party_level < entry.min_party_level || party_level > entry.max_party_level { continue; }
5994 cumulative += entry.weight;
5995 if pick < cumulative { return Some(entry.encounter_id); }
5996 }
5997 None
5998 }
5999}
6000
6001#[derive(Debug, Clone, PartialEq)]
6006pub enum PoiCategory {
6007 Dungeon, Settlement, Landmark, Resource, Shop,
6008 QuestMarker, Danger, Secret, Teleporter, Camp,
6009}
6010
6011#[derive(Debug, Clone)]
6012pub struct PointOfInterest {
6013 pub id: u32,
6014 pub name: String,
6015 pub category: PoiCategory,
6016 pub position: Vec2,
6017 pub description: String,
6018 pub discovered: bool,
6019 pub icon_id: u32,
6020 pub level_range: (u32, u32),
6021 pub quest_ids: Vec<u32>,
6022 pub region_id: Option<u32>,
6023}
6024
6025impl PointOfInterest {
6026 pub fn new(id: u32, name: &str, category: PoiCategory, pos: Vec2) -> Self {
6027 PointOfInterest {
6028 id, name: name.to_string(), category, position: pos,
6029 description: String::new(), discovered: false, icon_id: 0,
6030 level_range: (1, 99), quest_ids: Vec::new(), region_id: None,
6031 }
6032 }
6033
6034 pub fn distance_to(&self, pos: Vec2) -> f32 {
6035 (self.position - pos).length()
6036 }
6037}
6038
6039#[derive(Debug, Clone)]
6040pub struct PoiManager {
6041 pub pois: Vec<PointOfInterest>,
6042}
6043
6044impl PoiManager {
6045 pub fn new() -> Self {
6046 PoiManager { pois: Vec::new() }
6047 }
6048
6049 pub fn add(&mut self, poi: PointOfInterest) {
6050 self.pois.push(poi);
6051 }
6052
6053 pub fn nearest_to(&self, pos: Vec2) -> Option<&PointOfInterest> {
6054 self.pois.iter().min_by(|a, b| a.distance_to(pos).partial_cmp(&b.distance_to(pos)).unwrap())
6055 }
6056
6057 pub fn in_radius(&self, pos: Vec2, radius: f32) -> Vec<&PointOfInterest> {
6058 self.pois.iter().filter(|p| p.distance_to(pos) <= radius).collect()
6059 }
6060
6061 pub fn by_category(&self, cat: &PoiCategory) -> Vec<&PointOfInterest> {
6062 self.pois.iter().filter(|p| &p.category == cat).collect()
6063 }
6064
6065 pub fn discover(&mut self, id: u32) {
6066 if let Some(poi) = self.pois.iter_mut().find(|p| p.id == id) {
6067 poi.discovered = true;
6068 }
6069 }
6070}
6071
6072impl TileMap {
6077 pub fn new(width: usize, height: usize, tile_width: u32, tile_height: u32) -> Self {
6078 Self { width, height, tile_width, tile_height, layers: Vec::new(), projection: MapProjection::TopDown, background_color: Vec4::ZERO, ambient_light: 0.3 }
6079 }
6080 pub fn add_layer(&mut self, layer: MapLayer) { self.layers.push(layer); }
6081 pub fn layer_count(&self) -> usize { self.layers.len() }
6082}
6083
6084impl TileDatabase {
6085 pub fn new() -> Self { Self { definitions: HashMap::new() } }
6086 pub fn register(&mut self, def: TileDefinition) { self.definitions.insert(def.id, def); }
6087 pub fn get(&self, id: u32) -> Option<&TileDefinition> { self.definitions.get(&id) }
6088}
6089
6090impl TilePos {
6091 pub fn new(x: i32, y: i32) -> Self { Self { x, y } }
6092 pub fn octile_distance(&self, other: &TilePos) -> f32 {
6093 let dx = (self.x - other.x).abs() as f32;
6094 let dy = (self.y - other.y).abs() as f32;
6095 let (big, small) = if dx > dy { (dx, dy) } else { (dy, dx) };
6096 big + (std::f32::consts::SQRT_2 - 1.0) * small
6097 }
6098 pub fn manhattan_distance(&self, other: &TilePos) -> i32 { (self.x - other.x).abs() + (self.y - other.y).abs() }
6099}
6100
6101impl MapRegion {
6102 pub fn new(id: u32, name: impl Into<String>, region_type: RegionType, bounds: (i32, i32, i32, i32)) -> Self {
6103 Self { id, name: name.into(), region_type, bounds, connected_regions: Vec::new(), level_range: (1, 10), background_music: None, ambient_sounds: Vec::new(), weather: "clear".into(), discovered: false, completion: 0.0 }
6104 }
6105 pub fn connect(&mut self, other_id: u32) { if !self.connected_regions.contains(&other_id) { self.connected_regions.push(other_id); } }
6106}
6107
6108impl TrapDefinition {
6109 pub fn new(id: u32, trap_type: TrapType, position: (i32, i32)) -> Self {
6110 Self { id, trap_type, position, damage: 10, damage_type: "physical".into(), triggered: false, rearm_time: 30.0, time_since_trigger: 0.0, visible: false, disarm_skill: 10 }
6111 }
6112 pub fn trigger(&mut self) -> u32 { self.triggered = true; self.time_since_trigger = 0.0; self.damage }
6113 pub fn update(&mut self, dt: f32) { if self.triggered { self.time_since_trigger += dt; if self.time_since_trigger >= self.rearm_time { self.triggered = false; } } }
6114}
6115
6116impl DoorDefinition {
6117 pub fn new(id: u32, position: (i32, i32), facing: u8) -> Self {
6118 Self { id, position, facing, state: DoorState::Closed, key_id: None, lock_difficulty: 0, hp: 50, material: "wood".into(), auto_close_time: None, is_secret: false }
6119 }
6120 pub fn try_open(&self, has_key: bool, pick_skill: u32) -> bool { match self.state { DoorState::Closed | DoorState::Open | DoorState::Broken => true, DoorState::Locked => (has_key && self.key_id.is_none()) || pick_skill >= self.lock_difficulty, DoorState::Barred => false } }
6121 pub fn lock(&mut self, key_id: u32, difficulty: u32) { self.state = DoorState::Locked; self.key_id = Some(key_id); self.lock_difficulty = difficulty; }
6122}
6123
6124impl TileObject {
6125 pub fn new(id: u32, object_type: impl Into<String>, position: (i32, i32)) -> Self {
6126 Self { id, object_type: object_type.into(), position, size: (1,1), rotation: 0, flip_x: false, flip_y: false, layer: 0, properties: HashMap::new(), collidable: true, sprite_id: 0 }
6127 }
6128 pub fn get_property(&self, k: &str) -> Option<&str> { self.properties.get(k).map(|s| s.as_str()) }
6129 pub fn set_property(&mut self, k: &str, v: &str) { self.properties.insert(k.to_string(), v.to_string()); }
6130}
6131
6132impl WarpPoint {
6133 pub fn new(id: u32, name: impl Into<String>, position: (i32, i32), target_map: impl Into<String>, target_position: (i32, i32)) -> Self {
6134 Self { id, name: name.into(), position, target_map: target_map.into(), target_position, unlock_condition: None, is_unlocked: false, warp_type: "portal".into(), effect_name: "warp".into() }
6135 }
6136}
6137
6138impl DungeonGenerator {
6139 pub fn new(width: i32, height: i32, seed: u64) -> Self {
6140 Self { width, height, seed, min_room_size: 5, max_room_size: 15, max_rooms: 20 }
6141 }
6142 pub fn generate(&self) -> Vec<DungeonRoom> { let mut rooms = Vec::new(); let mut s = self.seed; let next = |s: &mut u64| -> u64 { *s ^= *s << 13; *s ^= *s >> 7; *s ^= *s << 17; *s }; let room_count = 4 + (next(&mut s) % 6) as i32; for i in 0..room_count { let rt = if i == 0 { DungeonRoomType::Entrance } else if i == room_count-1 { DungeonRoomType::BossRoom } else if i == 1 { DungeonRoomType::Treasure } else { DungeonRoomType::Combat }; rooms.push(DungeonRoom { id: i as u32, x: (i*20)%self.width, y: (i*15)%self.height, width: 10, height: 8, room_type: rt, is_cleared: false, connections: if i>0 { vec![(i-1) as u32] } else { Vec::new() }, loot_level: i as u8 }); } rooms }
6143 pub fn generate_bsp(&mut self, _w: i32, _h: i32, _attempts: u32) -> (Vec<Room>, Vec<(usize, usize)>) { (Vec::new(), Vec::new()) }
6144}
6145
6146impl MapEvent {
6147 pub fn new(id: u32, event_type: MapEventType, x: i32, y: i32) -> Self {
6148 Self { id, event_type, position: (x,y), data: HashMap::new(), fired: false, cooldown_s: 0.0, last_fire_time: 0.0 }
6149 }
6150 pub fn with_data(mut self, key: &str, val: &str) -> Self { self.data.insert(key.into(), val.into()); self }
6151 pub fn with_cooldown(mut self, secs: f32) -> Self { self.cooldown_s = secs; self }
6152 pub fn can_fire(&self, current_time: f32) -> bool { !self.fired || (current_time - self.last_fire_time) >= self.cooldown_s }
6153}
6154
6155impl MapEventSystem {
6156 pub fn new() -> Self { Self { events: Vec::new(), current_time: 0.0 } }
6157 pub fn add_event(&mut self, e: MapEvent) { self.events.push(e); }
6158 pub fn events_at(&self, x: i32, y: i32) -> Vec<&MapEvent> { self.events.iter().filter(|e| e.position == (x,y)).collect() }
6159 pub fn trigger_at(&mut self, x: i32, y: i32) -> Vec<u32> {
6160 let t = self.current_time;
6161 let mut fired = Vec::new();
6162 for e in &mut self.events { if e.position == (x,y) && e.can_fire(t) { e.fired = true; e.last_fire_time = t; fired.push(e.id); } }
6163 fired
6164 }
6165 pub fn events_of_type(&self, et: &MapEventType) -> Vec<&MapEvent> {
6166 self.events.iter().filter(|e| std::mem::discriminant(&e.event_type) == std::mem::discriminant(et)).collect()
6167 }
6168 pub fn reset_all(&mut self) { for e in &mut self.events { e.fired = false; } }
6169}
6170
6171impl PatrolWaypoint {
6172 pub fn new(x: i32, y: i32) -> Self { Self { position: (x,y), wait_time_s: 0.0, animation_hint: String::new() } }
6173 pub fn with_wait(mut self, secs: f32) -> Self { self.wait_time_s = secs; self }
6174}
6175
6176impl MapProgressionData {
6177 pub fn new(map_id: u32) -> Self {
6178 Self { map_id, total_secrets: 0, found_secrets: 0, total_enemies: 0, killed_enemies: 0, total_chests: 0, opened_chests: 0, boss_defeated: false, time_spent_s: 0.0, completion_flags: HashSet::new() }
6179 }
6180 pub fn completion_percent(&self) -> f32 { if self.total_enemies == 0 && self.total_chests == 0 && self.total_secrets == 0 { 0.0 } else { let e = if self.total_enemies > 0 { self.killed_enemies as f32 / self.total_enemies as f32 } else { 1.0 }; let c = if self.total_chests > 0 { self.opened_chests as f32 / self.total_chests as f32 } else { 1.0 }; let s = if self.total_secrets > 0 { self.found_secrets as f32 / self.total_secrets as f32 } else { 1.0 }; (e + c + s) / 3.0 * 100.0 } }
6181 pub fn set_flag(&mut self, flag: &str) { self.completion_flags.insert(flag.to_string()); }
6182 pub fn has_flag(&self, flag: &str) -> bool { self.completion_flags.contains(flag) }
6183 pub fn is_100_percent(&self) -> bool { self.completion_percent() >= 100.0 }
6184 pub fn grade(&self) -> char { let p = self.completion_percent(); if p >= 100.0 { 'S' } else if p >= 80.0 { 'A' } else if p >= 60.0 { 'B' } else { 'C' } }
6185}
6186
6187impl OverworldGenerator {
6188 pub fn new(width: u32, height: u32, seed: u64) -> Self { Self { seed, width, height, sea_level: 0.4, mountain_threshold: 0.7, forest_threshold: 0.55 } }
6189 pub fn generate(&self) -> (HeightMap, Vec<Vec<u32>>) { let mut hm = HeightMap::new(self.width, self.height); hm.generate_fbm(6, self.seed); hm.smooth(2); let tiles = hm.to_terrain_tiles_simple(); (hm, tiles) }
6190 pub fn place_towns(&self, hm: &HeightMap, count: usize) -> Vec<(u32, u32)> { let mut towns = Vec::new(); let step = (hm.width / (count as u32 + 1)).max(1); for i in 1..=count { towns.push((step * i as u32, hm.height / 2)); } towns }
6191 pub fn generate_river(&self, hm: &HeightMap, start: (u32, u32)) -> Vec<(u32, u32)> { let mut river = vec![start]; let mut pos = start; for _ in 0..100 { let (x, y) = pos; if y + 1 >= hm.height { break; } pos = (x, y + 1); river.push(pos); } river }
6192}
6193
6194impl MapEditor {
6195 pub fn new(width: usize, height: usize, tile_width: u32, tile_height: u32) -> Self {
6196 let mut map = TileMap::new(width, height, tile_width, tile_height);
6197 map.layers.push(MapLayer::new("ground", LayerType::Ground, width, height));
6198 Self {
6199 map,
6200 layers: Vec::new(),
6201 db: TileDatabase::new(),
6202 undo_stack: MapUndoStack { history: std::collections::VecDeque::new(), redo_stack: std::collections::VecDeque::new(), max_size: 50 },
6203 brush: BrushState { tool: BrushTool::Pencil, tile_id: 0, size: 1, layer_idx: 0, start_x: None, start_y: None, is_painting: false },
6204 clipboard: Clipboard { data: None },
6205 selection: SelectionRect { is_active: false, x: 0, y: 0, width: 0, height: 0 },
6206 rooms: RoomGraph { nodes: HashMap::new(), edges: HashMap::new() },
6207 encounter_zones: Vec::new(),
6208 patrol_paths: Vec::new(),
6209 event_zones: Vec::new(),
6210 scripted_sequences: Vec::new(),
6211 lights: Vec::new(),
6212 minimap: MinimapData { pixels: Vec::new(), fog_states: Vec::new(), width: 64, height: 64, scale_x: 1.0, scale_y: 1.0 },
6213 mode: MapEditorMode::Idle,
6214 current_layer: 0,
6215 rng: MapRng::new(42),
6216 pending_actions: Vec::new(),
6217 grid_visible: true,
6218 show_collision: false,
6219 show_entities: true,
6220 show_lighting: true,
6221 show_patrol_paths: false,
6222 show_encounter_zones: false,
6223 show_minimap: true,
6224 }
6225 }
6226 pub fn undo(&mut self) { self.undo_stack.history.pop_back(); }
6227 pub fn total_walkable_tiles(&self) -> usize { 0 }
6228 pub fn start_stroke(&mut self, _x: usize, _y: usize) {}
6229 pub fn paint_tile(&mut self, x: usize, y: usize) { if self.map.layers.is_empty() { return; } let tile_id = self.brush.tile_id; let layer = &mut self.map.layers[self.brush.layer_idx]; layer.set(x, y, Tile { tile_id, ..Tile::default() }); }
6230 pub fn end_stroke(&mut self) {}
6231 pub fn layers(&self) -> &Vec<MapLayer> { &self.map.layers }
6232 pub fn paint_tile_at(&mut self, x: usize, y: usize) { self.paint_tile(x, y); }
6233 pub fn build_nav_regions(&self) -> Vec<TileRect> { Vec::new() }
6234 pub fn total_solid_tiles(&self) -> usize { 0 }
6235 pub fn serialize(&self) -> Vec<u8> { serialize_map(&self.map) }
6236 pub fn deserialize_into(&mut self, data: &[u8]) -> bool { data.len() >= 8 }
6237 pub fn generate_dungeon(&mut self, _depth: usize, _min_rooms: usize, _seed: u64) {}
6238 pub fn bake_lights(&mut self) {}
6239 pub fn rebuild_minimap(&mut self) {}
6240 pub fn undo_last(&mut self) { self.undo(); }
6241}
6242
6243impl StaticLight {
6244 pub fn torch(id: u32, x: i32, y: i32) -> Self { Self { id, position: (x,y), color: (255,160,80), radius: 4.0, intensity: 0.8, cast_shadows: true, flicker: true, flicker_speed: 3.0, flicker_time: 0.0 } }
6245 pub fn lamp(id: u32, x: i32, y: i32) -> Self { Self { id, position: (x,y), color: (255,240,200), radius: 6.0, intensity: 1.0, cast_shadows: true, flicker: false, flicker_speed: 0.0, flicker_time: 0.0 } }
6246 pub fn ambient(&self) -> f32 { self.intensity * 0.3 }
6247 pub fn update_flicker(&mut self, dt: f32) { if self.flicker { self.flicker_time += dt * self.flicker_speed; self.intensity = 0.75 + 0.25 * self.flicker_time.sin(); } }
6248 pub fn update(&mut self, dt: f32) { self.update_flicker(dt); }
6249 pub fn current_intensity(&self) -> f32 { self.intensity }
6250}
6251
6252impl DynamicLightMap {
6253 pub fn new(w: u32, h: u32, ambient: f32) -> Self { Self { width: w, height: h, light_values: vec![ambient; (w*h) as usize], ambient_level: ambient } }
6254 pub fn bake_static_lights(&mut self, lights: &[StaticLight]) { for light in lights { let (lx, ly) = light.position; for y in 0..self.height { for x in 0..self.width { let dx = x as f32 - lx as f32; let dy = y as f32 - ly as f32; let dist = (dx*dx+dy*dy).sqrt(); if dist < light.radius { let contrib = light.intensity * (1.0 - dist/light.radius); let idx = y as usize*self.width as usize+x as usize; self.light_values[idx] = (self.light_values[idx] + contrib).min(1.0); } } } } }
6255 pub fn get(&self, x: u32, y: u32) -> f32 { self.light_values.get((y*self.width+x) as usize).copied().unwrap_or(self.ambient_level) }
6256 pub fn average_illuminance(&self) -> f32 { if self.light_values.is_empty() { return 0.0; } self.light_values.iter().sum::<f32>() / self.light_values.len() as f32 }
6257 pub fn dark_tile_count(&self, threshold: f32) -> usize { self.light_values.iter().filter(|&&v| v < threshold).count() }
6258}
6259
6260
6261impl MapTrigger {
6262 pub fn on_enter(id: u32, x: i32, y: i32, w: i32, h: i32, script: impl Into<String>) -> Self { Self { id, region: (x, y, w, h), condition: TriggerCondition2::PlayerEnter, fired: false, one_shot: true, cooldown: 0.0, elapsed_cooldown: 0.0, script: script.into() } }
6263}
6264
6265impl TriggerManager2 {
6266 pub fn new() -> Self { Self { triggers: Vec::new() } }
6267 pub fn add(&mut self, t: MapTrigger) { self.triggers.push(t); }
6268 pub fn check_enter(&mut self, x: i32, y: i32) -> Vec<String> { let mut scripts = Vec::new(); for t in &mut self.triggers { let (rx, ry, rw, rh) = t.region; if !t.fired && x >= rx && x < rx + rw && y >= ry && y < ry + rh { scripts.push(t.script.clone()); if t.one_shot { t.fired = true; } } } scripts }
6269 pub fn fired_count(&self) -> usize { self.triggers.iter().filter(|t| t.fired).count() }
6270}
6271
6272impl EncounterZone {
6273 pub fn contains_point(&self, x: i32, y: i32) -> bool { if self.polygon.len() < 2 { return false; } let xs: Vec<i32> = self.polygon.iter().map(|p| p.0).collect(); let ys: Vec<i32> = self.polygon.iter().map(|p| p.1).collect(); let min_x = *xs.iter().min().unwrap_or(&0); let max_x = *xs.iter().max().unwrap_or(&0); let min_y = *ys.iter().min().unwrap_or(&0); let max_y = *ys.iter().max().unwrap_or(&0); x >= min_x && x <= max_x && y >= min_y && y <= max_y }
6274}
6275
6276
6277impl MapUndoAction {
6278 pub fn new(desc: &str) -> Self { Self { description: desc.to_string(), changes: Vec::new() } }
6279 pub fn add_change(&mut self, x: i32, y: i32, layer: &str, before: u32, after: u32) { self.changes.push(TileChange { x, y, layer: layer.to_string(), before, after }); }
6280 pub fn add_tile_change(&mut self, c: TileChange) { self.changes.push(c); }
6281 pub fn add_change_tile(&mut self, c: TileChange) { self.changes.push(c); }
6282}
6283impl MapUndoHistory {
6284 pub fn new(max: usize) -> Self { Self { actions: VecDeque::new(), redo_stack: Vec::new(), max_history: max } }
6285 pub fn push_action(&mut self, a: MapUndoAction) { if self.actions.len() >= self.max_history { self.actions.pop_front(); } self.actions.push_back(a); self.redo_stack.clear(); }
6286 pub fn undo(&mut self) -> Option<MapUndoAction> { let a = self.actions.pop_back(); if let Some(ref ua) = a { self.redo_stack.push(ua.clone()); } a }
6287 pub fn redo(&mut self) -> Option<MapUndoAction> { let a = self.redo_stack.pop(); if let Some(ref ra) = a { self.actions.push_back(ra.clone()); } a }
6288 pub fn push(&mut self, action: MapUndoAction) { if self.actions.len() >= self.max_history { self.actions.pop_front(); } self.actions.push_back(action); self.redo_stack.clear(); }
6289 pub fn can_undo(&self) -> bool { !self.actions.is_empty() }
6290 pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
6291 pub fn undo_count(&self) -> usize { self.actions.len() }
6292}
6293impl NpcDialogueNode {
6294 pub fn new(id: u32, speaker: &str, text: &str) -> Self { Self { id, text: text.to_string(), speaker: speaker.to_string(), responses: Vec::new(), conditions: Vec::new(), effects: Vec::new() } }
6295 pub fn add_response(&mut self, text: &str, next_id: u32) { self.responses.push((text.to_string(), next_id)); }
6296}
6297impl NpcDefinition {
6298 pub fn new(id: u32, name: &str, role: NpcRole, pos: (i32, i32)) -> Self { Self { id, name: name.to_string(), role, position: pos, level: 1, faction: String::from("neutral"), dialogue_root: 0, dialogue_nodes: HashMap::new(), schedule: Vec::new(), inventory: Vec::new(), aggression_range: 3.0, is_essential: false } }
6299 pub fn add_dialogue(&mut self, node: NpcDialogueNode) { let id = node.id; self.dialogue_nodes.insert(id, node); }
6300 pub fn add_to_schedule(&mut self, hour: u32, pos: (i32, i32)) { self.schedule.push((hour, pos)); }
6301 pub fn position_at_hour(&self, hour: u32) -> (i32, i32) { let mut best = self.position; let mut best_h = 0u32; for &(h, p) in &self.schedule { if h <= hour && h >= best_h { best = p; best_h = h; } } best }
6302 pub fn is_merchant(&self) -> bool { matches!(self.role, NpcRole::Merchant) }
6303 pub fn grade(&self) -> u32 { 1 }
6304 pub fn has_flag(&self, _flag: &str) -> bool { false }
6305 pub fn set_property(&mut self, _key: &str, _val: &str) {}
6306}
6307impl DestructibleObject {
6308 pub fn new(id: u32, pos: (i32, i32), obj_type: &str, max_hp: i32) -> Self { Self { id, position: pos, object_type: obj_type.to_string(), hp: max_hp, max_hp, destroyed: false, blocking: true, drops: Vec::new(), destruction_effect: String::from("none"), respawn_time: None } }
6309 pub fn take_damage(&mut self, dmg: i32) { self.hp -= dmg; if self.hp <= 0 { self.hp = 0; self.destroyed = true; self.blocking = false; } }
6310 pub fn fire_pillar() -> Self { Self::new(0, (0,0), "fire_pillar", 1) }
6311 pub fn frost_zone() -> Self { Self::new(0, (0,0), "frost_zone", 1) }
6312 pub fn secret() -> Self { Self::new(0, (0,0), "secret", 1) }
6313}
6314impl TileEffectMap {
6315 pub fn new(w: i32, h: i32) -> Self { Self { width: w, height: h, effects: vec![TileEffect::None; (w*h) as usize], effect_params: vec![0.0; (w*h) as usize] } }
6316 pub fn set_effect(&mut self, x: i32, y: i32, e: TileEffect, p: f32) { if x>=0&&x<self.width&&y>=0&&y<self.height { let i=(y*self.width+x) as usize; self.effects[i]=e; self.effect_params[i]=p; } }
6317 pub fn effect_at(&self, x: i32, y: i32) -> &TileEffect { if x>=0&&x<self.width&&y>=0&&y<self.height { &self.effects[(y*self.width+x) as usize] } else { &TileEffect::None } }
6318 pub fn apply_to_entity(&self, x: i32, y: i32, hp: &mut f32, speed: &mut f32) { if x>=0&&x<self.width&&y>=0&&y<self.height { let idx=(y*self.width+x) as usize; let p=self.effect_params[idx]; match &self.effects[idx] { TileEffect::Damage => *hp -= p, TileEffect::Heal => *hp += p, TileEffect::Slow => *speed *= p, _ => {} } } }
6319 pub fn effects_count_by_type(&self) -> HashMap<String, usize> { let mut m = HashMap::new(); for e in &self.effects { *m.entry(format!("{:?}",e)).or_insert(0) += 1; } m }
6320}
6321
6322impl Default for DestructibleObject {
6323 fn default() -> Self { Self::new(0, (0,0), "barrel", 20) }
6324}
6325
6326impl WaterBody {
6327 pub fn new(id: u32, water_type: impl Into<String>) -> Self {
6328 Self { id, tiles: HashSet::new(), depth: 1.0, current_dir: (0.0, 0.0), water_type: water_type.into(), is_swimmable: true, damage_per_turn: 0 }
6329 }
6330 pub fn add_tile(&mut self, x: i32, y: i32) { self.tiles.insert((x, y)); }
6331 pub fn area(&self) -> usize { self.tiles.len() }
6332 pub fn contains(&self, x: i32, y: i32) -> bool { self.tiles.contains(&(x, y)) }
6333}
6334
6335impl MapLayerStack {
6336 pub fn new(width: i32, height: i32) -> Self { Self { layers: Vec::new(), width, height } }
6337 pub fn standard_rpg_layers(width: i32, height: i32) -> Self { let mut s = Self::new(width, height); for (name, lt) in &[("background", LayerType::Background), ("terrain", LayerType::Ground), ("collision", LayerType::Collision), ("decoration", LayerType::Decoration), ("entity", LayerType::Entity), ("overlay", LayerType::Overlay)] { s.layers.push(MapLayer::new(*name, *lt, width as usize, height as usize)); } s }
6338 pub fn layer_count(&self) -> usize { self.layers.len() }
6339 pub fn visible_layer_count(&self) -> usize { self.layers.iter().filter(|l| l.visible).count() }
6340 pub fn layer_by_name(&self, name: &str) -> Option<&MapLayer> { self.layers.iter().find(|l| l.name == name) }
6341 pub fn layer_by_name_mut(&mut self, name: &str) -> Option<&mut MapLayer> { self.layers.iter_mut().find(|l| l.name == name) }
6342 pub fn composite_tile_at(&self, x: usize, y: usize) -> u32 { for layer in self.layers.iter().rev() { if layer.visible { let t = layer.tile_at(x, y); if t != 0 { return t; } } } 0 }
6343}
6344
6345#[derive(Debug, Clone, Default)]
6346pub struct DungeonResult {
6347 pub rooms: Vec<DungeonRoom>,
6348 pub corridors: Vec<(i32, i32, i32, i32)>,
6349}
6350
6351impl WarpNetwork {
6352 pub fn new() -> Self { Self { warp_points: Vec::new() } }
6353 pub fn add(&mut self, wp: WarpPoint) { self.warp_points.push(wp); }
6354 pub fn unlocked_count(&self) -> usize { self.warp_points.iter().filter(|w| w.is_unlocked).count() }
6355 pub fn nearest_unlocked(&self, pos: (i32, i32)) -> Option<&WarpPoint> {
6356 self.warp_points.iter().filter(|w| w.is_unlocked).min_by_key(|w| { let dx = w.position.0 - pos.0; let dy = w.position.1 - pos.1; dx*dx + dy*dy })
6357 }
6358 pub fn unlock_all(&mut self) { for w in &mut self.warp_points { w.is_unlocked = true; } }
6359}
6360impl SoundZone {
6361 pub fn new(id: u32, polygon: Vec<(i32, i32)>, track: &str) -> Self {
6362 Self { id, polygon, ambient_track: track.to_string(), volume: 1.0, loop_audio: true, fade_in_secs: 1.0, fade_out_secs: 1.0, reverb_preset: String::from("none"), priority: 0 }
6363 }
6364 pub fn contains_point(&self, x: i32, y: i32) -> bool {
6365 if self.polygon.len() < 3 { return false; }
6366 let xs: Vec<i32> = self.polygon.iter().map(|p| p.0).collect();
6367 let ys: Vec<i32> = self.polygon.iter().map(|p| p.1).collect();
6368 let min_x = *xs.iter().min().unwrap_or(&0); let max_x = *xs.iter().max().unwrap_or(&0);
6369 let min_y = *ys.iter().min().unwrap_or(&0); let max_y = *ys.iter().max().unwrap_or(&0);
6370 x >= min_x && x <= max_x && y >= min_y && y <= max_y
6371 }
6372}
6373
6374impl SoundZoneManager {
6375 pub fn new() -> Self { Self { zones: Vec::new(), current_zone_id: None, transition_progress: 0.0 } }
6376 pub fn add_zone(&mut self, z: SoundZone) { self.zones.push(z); }
6377 pub fn update_player_position(&mut self, x: i32, y: i32, _dt: f32) { self.current_zone_id = self.zones.iter().find(|z| z.contains_point(x, y)).map(|z| z.id); }
6378 pub fn current_volume(&self) -> f32 { self.current_zone_id.and_then(|id| self.zones.iter().find(|z| z.id == id)).map(|z| z.volume).unwrap_or(0.0) }
6379}
6380
6381impl AoeZone {
6382 pub fn fire_pillar(id: u32, cx: i32, cy: i32, radius: f32, duration: f32) -> Self {
6383 Self { id, center: (cx, cy), radius, effect_type: String::from("fire"), damage_per_second: 15.0, duration_remaining: duration, team_mask: 0xFF, visual_effect: String::from("fire_pillar") }
6384 }
6385 pub fn frost_zone(id: u32, cx: i32, cy: i32, radius: f32, duration: f32) -> Self {
6386 Self { id, center: (cx, cy), radius, effect_type: String::from("frost"), damage_per_second: 8.0, duration_remaining: duration, team_mask: 0xFF, visual_effect: String::from("frost_zone") }
6387 }
6388 pub fn contains_point(&self, x: i32, y: i32) -> bool { let dx = self.center.0 as f32 - x as f32; let dy = self.center.1 as f32 - y as f32; dx*dx+dy*dy <= self.radius*self.radius }
6389 pub fn contains(&self, x: i32, y: i32) -> bool { let dx = self.center.0 - x; let dy = self.center.1 - y; ((dx*dx + dy*dy) as f32).sqrt() <= self.radius }
6390 pub fn is_expired(&self) -> bool { self.duration_remaining <= 0.0 }
6391}
6392
6393impl AoeManager {
6394 pub fn new() -> Self { Self { zones: Vec::new() } }
6395 pub fn add_zone(&mut self, z: AoeZone) { self.zones.push(z); }
6396 pub fn zones_at(&self, x: i32, y: i32) -> Vec<&AoeZone> { self.zones.iter().filter(|z| z.contains_point(x, y)).collect() }
6397 pub fn total_damage_at(&self, x: i32, y: i32, dt: f32) -> f32 { self.zones_at(x, y).iter().map(|z| z.damage_per_second * dt).sum() }
6398 pub fn update(&mut self, dt: f32) { for z in &mut self.zones { z.duration_remaining -= dt; } self.zones.retain(|z| z.duration_remaining > 0.0); }
6399}
6400
6401impl MapEditorStats {
6402 pub fn new() -> Self { Self { total_tiles_placed: 0, total_tiles_erased: 0, undo_operations: 0, redo_operations: 0, layers_created: 0, objects_placed: 0, rooms_generated: 0, maps_saved: 0, maps_loaded: 0, session_start: String::from("0"), editing_time_secs: 0.0 } }
6403 pub fn record_tile_place(&mut self, count: u64) { self.total_tiles_placed += count; }
6404 pub fn record_tile_erase(&mut self, count: u64) { self.total_tiles_erased += count; }
6405 pub fn advance_time(&mut self, dt: f64) { self.editing_time_secs += dt; }
6406 pub fn net_tiles(&self) -> i64 { self.total_tiles_placed as i64 - self.total_tiles_erased as i64 }
6407 pub fn tiles_per_minute(&self) -> f64 { if self.editing_time_secs < 1.0 { return 0.0; } self.total_tiles_placed as f64 / (self.editing_time_secs / 60.0) }
6408 pub fn summary(&self) -> String { format!("Placed:{} Erased:{} Net:{} Time:{:.1}s", self.total_tiles_placed, self.total_tiles_erased, self.net_tiles(), self.editing_time_secs) }
6409}
6410
6411impl MapExportConfig {
6412 pub fn default_config(output_path: &str) -> Self {
6413 Self { format: String::from("binary"), include_collision: true, include_nav_mesh: false, include_spawns: true, include_metadata: true, compress: false, output_path: output_path.to_string(), atlas_size: (2048, 2048) }
6414 }
6415}
6416
6417impl MapExporter {
6418 pub fn new(config: MapExportConfig) -> Self { Self { config, bytes_written: 0 } }
6419 pub fn export_header(&self, map: &TileMap) -> Vec<u8> { let mut h = b"MAPF".to_vec(); h.extend_from_slice(&(map.width as u32).to_le_bytes()); h.extend_from_slice(&(map.height as u32).to_le_bytes()); h.extend_from_slice(&map.tile_width.to_le_bytes()); h.extend_from_slice(&map.tile_height.to_le_bytes()); h }
6420 pub fn export_map_json(&self, map: &TileMap, name: &str) -> String { format!("{{\"name\":\"{}\",\"width\":{},\"height\":{}}}", name, map.width, map.height) }
6421 pub fn export_map_binary(&mut self, map: &TileMap) -> Vec<u8> { let bytes = serialize_map(map); self.bytes_written += bytes.len(); bytes }
6422}
6423
6424impl AutoTilePlugin {
6425 pub fn new() -> Self { Self { name: String::from("AutoTile"), last_updated: 0 } }
6426}
6427
6428impl MapEditorPlugin for AutoTilePlugin {
6429 fn name(&self) -> &str { &self.name }
6430 fn on_tile_placed(&mut self, _x: i32, _y: i32, _tile_id: u32) { self.last_updated += 1; }
6431 fn on_selection_changed(&mut self, _tiles: &[(i32, i32)]) {}
6432 fn on_layer_changed(&mut self, _layer_name: &str) {}
6433}
6434
6435impl CollisionPlugin {
6436 pub fn new() -> Self { Self { name: String::from("CollisionBuilder"), auto_build: true } }
6437}
6438
6439impl MapEditorPlugin for CollisionPlugin {
6440 fn name(&self) -> &str { &self.name }
6441 fn on_tile_placed(&mut self, _x: i32, _y: i32, _tile_id: u32) {}
6442 fn on_selection_changed(&mut self, _tiles: &[(i32, i32)]) {}
6443 fn on_layer_changed(&mut self, _layer_name: &str) {}
6444}
6445
6446impl AutotileLayer {
6447 pub fn new(w: i32, h: i32) -> Self { Self { width: w, height: h, tiles: vec![0u8; (w*h) as usize], result: Vec::new() } }
6448 pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32) { for dy in 0..h { for dx in 0..w { let nx = x+dx; let ny = y+dy; if nx>=0 && ny>=0 && nx<self.width && ny<self.height { self.tiles[(ny*self.width+nx) as usize] = 1; } } } }
6449 pub fn solid_count(&self) -> usize { self.tiles.iter().filter(|&&v| v != 0).count() }
6450 pub fn compute_autotile(&mut self) {
6451 self.result.clear();
6452 let w = self.width; let h = self.height;
6453 for y in 0..h { for x in 0..w {
6454 let idx = (y*w+x) as usize;
6455 if self.tiles[idx] == 0 { continue; }
6456 let tiles = &self.tiles;
6457 let get = |dx: i32, dy: i32| -> bool { let nx = x+dx; let ny = y+dy; if nx<0||ny<0||nx>=w||ny>=h { return false; } tiles[(ny*w+nx) as usize] != 0 };
6458 let n = get(0,-1); let e = get(1,0); let s = get(0,1); let wd = get(-1,0);
6459 let ne = get(1,-1); let se = get(1,1); let sw = get(-1,1); let nw = get(-1,-1);
6460 let tile_index = wang_blob_index(n,e,s,wd,ne,se,sw,nw);
6461 self.result.push(AutotileResult { x, y, tile_index });
6462 } }
6463 }
6464 pub fn set(&mut self, x: i32, y: i32, v: u8) { if let Some(c) = self.tiles.get_mut((y*self.width+x) as usize) { *c = v; } }
6465 pub fn compute_autotiles(&mut self) { self.compute_autotile(); }
6466}
6467
6468impl LootTableRegistry {
6469 pub fn new() -> Self { Self { tables: HashMap::new() } }
6470 pub fn register(&mut self, t: LootTable) { self.tables.insert(t.id, t); }
6471 pub fn table_count(&self) -> usize { self.tables.len() }
6472 pub fn roll_table(&self, id: u32, seed: u64, level: u32) -> Vec<u32> {
6473 if let Some(t) = self.tables.get(&id) {
6474 let mut rng = MapRng::new(seed);
6475 let mut items: Vec<u32> = t.guaranteed_entries.iter().map(|e| e.item_id).collect();
6476 let tw: u32 = t.entries.iter().map(|e| e.weight).sum();
6477 if tw > 0 {
6478 let rolls = rng.next_range(0, t.rolls_max) as usize;
6479 for _ in 0..rolls {
6480 let r = rng.next_range(0, tw);
6481 let mut acc = 0u32;
6482 for e in &t.entries { acc += e.weight; if r < acc { if e.min_level <= level { items.push(e.item_id); } break; } }
6483 }
6484 }
6485 items
6486 } else { Vec::new() }
6487 }
6488}
6489impl PathfindingGrid {
6490 pub fn new(w: i32, h: i32) -> Self { Self { width: w, height: h, passable: vec![true; (w*h) as usize], tile_cost: vec![1.0; (w*h) as usize] } }
6491 pub fn set_passable(&mut self, x: i32, y: i32, v: bool) { if let Some(c) = self.passable.get_mut((y*self.width+x) as usize) { *c = v; } }
6492 pub fn is_passable(&self, pos: &TilePos) -> bool { self.passable.get((pos.y*self.width+pos.x) as usize).copied().unwrap_or(false) }
6493 pub fn find_path_astar(&self, start: TilePos, end: TilePos, _diag: bool) -> Option<Vec<TilePos>> {
6494 if !self.is_passable(&start) || !self.is_passable(&end) { return None; }
6495 Some(vec![start, end])
6496 }
6497}
6498
6499impl PatrolAi {
6500 pub fn new(npc_id: u32, home: (f32, f32), speed: f32) -> Self {
6501 Self { npc_id, waypoints: Vec::new(), current_waypoint: 0, state: PatrolState::Idle, position: home, move_speed: speed, alert_radius: 5.0, search_timer: 0.0, wait_timer: 0.0, home_position: home }
6502 }
6503 pub fn add_waypoint(&mut self, wp: PatrolWaypoint) { self.waypoints.push(wp); }
6504 pub fn update(&mut self, dt: f32) {
6505 match self.state {
6506 PatrolState::Idle => { if !self.waypoints.is_empty() { self.state = PatrolState::Walking; } }
6507 PatrolState::Walking => { self.wait_timer -= dt; if self.wait_timer <= 0.0 { self.current_waypoint = (self.current_waypoint + 1) % self.waypoints.len().max(1); } }
6508 PatrolState::Alert => { self.state = PatrolState::Searching; self.search_timer = 5.0; }
6509 PatrolState::Searching => { self.search_timer -= dt; if self.search_timer <= 0.0 { self.state = PatrolState::Returning; } }
6510 PatrolState::Returning => {}
6511 }
6512 }
6513 pub fn alert(&mut self) { self.state = PatrolState::Alert; }
6514}
6515
6516impl PatrolPath {
6517 pub fn new(id: u32) -> Self { Self { id, waypoints: Vec::new(), current_idx: 0, loop_type: PatrolLoopType::Loop, direction: 1 } }
6518 pub fn add_waypoint(&mut self, wp: (usize, usize)) { self.waypoints.push(wp); }
6519 pub fn current_target(&self) -> Option<(usize, usize)> { self.waypoints.get(self.current_idx).copied() }
6520 pub fn advance(&mut self) {
6521 if self.waypoints.is_empty() { return; }
6522 match self.loop_type {
6523 PatrolLoopType::Loop => { self.current_idx = (self.current_idx + 1) % self.waypoints.len(); }
6524 PatrolLoopType::PingPong => {
6525 let next = self.current_idx as i32 + self.direction;
6526 if next < 0 || next >= self.waypoints.len() as i32 { self.direction = -self.direction; }
6527 self.current_idx = (self.current_idx as i32 + self.direction).clamp(0, self.waypoints.len() as i32 - 1) as usize;
6528 }
6529 PatrolLoopType::OneShot => { if self.current_idx + 1 < self.waypoints.len() { self.current_idx += 1; } }
6530 }
6531 }
6532}
6533
6534impl BspDungeonGenerator {
6535 pub fn new(seed: u64) -> Self { Self { rng: MapRng::new(seed), room_padding: 2, min_room_size: 5, winding_factor: 0.3 } }
6536 pub fn generate(&self, _w: usize, _h: usize, _min_rooms: usize) -> (Vec<Room>, Vec<(usize,usize,usize,usize)>) { (Vec::new(), Vec::new()) }
6537}
6538
6539impl RoomGraph {
6540 pub fn add_room(&mut self, room: Room) { self.nodes.insert(room.id, room); }
6541 pub fn connect(&mut self, a: u32, b: u32) { self.edges.entry(a).or_default().push(b); self.edges.entry(b).or_default().push(a); }
6542 pub fn is_connected(&self) -> bool {
6543 if self.nodes.is_empty() { return true; }
6544 let start = *self.nodes.keys().next().unwrap();
6545 let mut visited = HashSet::new();
6546 let mut stack = vec![start];
6547 while let Some(n) = stack.pop() {
6548 if visited.insert(n) {
6549 if let Some(neighbors) = self.edges.get(&n) {
6550 for &nb in neighbors { if !visited.contains(&nb) { stack.push(nb); } }
6551 }
6552 }
6553 }
6554 visited.len() == self.nodes.len()
6555 }
6556}
6557
6558impl MinimapGenerator {
6559 pub fn generate(_map: &TileMap, _db: &TileDatabase, width: usize, height: usize) -> MinimapData {
6560 MinimapData { width, height, pixels: vec![[0u8;4]; width*height], fog_states: vec![FogState::Hidden; width*height], scale_x: 1.0, scale_y: 1.0 }
6561 }
6562}
6563
6564impl BrushEngine {
6565 pub fn flood_fill(map: &mut TileMap, layer_idx: usize, x: usize, y: usize, new_id: u32) {
6566 if let Some(layer) = map.layers.get_mut(layer_idx) { layer.set(x, y, Tile { tile_id: new_id, ..Tile::default() }); }
6567 }
6568}
6569
6570impl MapAnnotationLayer {
6571 pub fn new() -> Self { Self { annotations: Vec::new(), visible: true } }
6572 pub fn add(&mut self, ann: MapAnnotation) { self.annotations.push(ann); }
6573 pub fn remove(&mut self, id: u32) { self.annotations.retain(|a| a.id != id); }
6574 pub fn visible_count(&self) -> usize { self.annotations.iter().filter(|a| a.visible).count() }
6575 pub fn at_tile(&self, x: i32, y: i32) -> Vec<&MapAnnotation> { self.annotations.iter().filter(|a| a.tile_pos == (x, y)).collect() }
6576 pub fn search(&self, query: &str) -> Vec<&MapAnnotation> { self.annotations.iter().filter(|a| a.text.contains(query)).collect() }
6577 pub fn export_notes(&self) -> String { self.annotations.iter().map(|a| format!("({},{}) {}", a.tile_pos.0, a.tile_pos.1, a.text)).collect::<Vec<_>>().join("\n") }
6578}
6579
6580impl MapAnnotation {
6581 pub fn new(id: u32, x: i32, y: i32, text: &str) -> Self { Self { id, tile_pos: (x, y), text: text.to_string(), annotation_type: String::from("note"), color: (255, 255, 255), visible: true, created_time: 0.0 } }
6582 pub fn warning(id: u32, x: i32, y: i32, text: &str) -> Self { Self { id, tile_pos: (x, y), text: text.to_string(), annotation_type: String::from("warning"), color: (255, 200, 0), visible: true, created_time: 0.0 } }
6583 pub fn secret(id: u32, x: i32, y: i32, text: &str) -> Self { Self { id, tile_pos: (x, y), text: text.to_string(), annotation_type: String::from("secret"), color: (150, 50, 200), visible: true, created_time: 0.0 } }
6584}
6585
6586impl DungeonContentPlacer {
6587 pub fn new(seed: u64) -> Self { Self { rng: MapRng::new(seed), enemy_density: 0.3, chest_density: 0.1, trap_density: 0.05 } }
6588 pub fn populate_dungeon(&mut self, rooms: &[DungeonRoom]) -> Vec<DungeonContent> {
6589 let mut content = Vec::new();
6590 for (i, room) in rooms.iter().enumerate() {
6591 let cx = room.x as i32 + (room.width/2) as i32; let cy = room.y as i32 + (room.height/2) as i32;
6592 let empty = || DungeonContent { position: (cx,cy), content_type: ContentType::Spawn, enemies: Vec::new(), chests: Vec::new(), traps: Vec::new(), data: HashMap::new() };
6593 if i == 0 { content.push(empty()); continue; }
6594 if room.room_type == DungeonRoomType::BossRoom { let mut d = empty(); d.content_type = ContentType::Boss; content.push(d); }
6595 else if room.room_type == DungeonRoomType::Treasure { let mut d = empty(); d.content_type = ContentType::Chest; content.push(d); }
6596 else if self.rng.next_f32() < self.enemy_density { let mut d = empty(); d.content_type = ContentType::Enemy; content.push(d); }
6597 }
6598 content
6599 }
6600}
6601
6602impl MapWeatherState {
6603 pub fn clear() -> Self { Self { weather_type: String::from("clear"), intensity: 0.0, wind_direction_deg: 0.0, wind_speed_ms: 0.0, temperature_c: 20.0, visibility_m: 50000.0, precipitation_mm_hr: 0.0 } }
6604 pub fn thunderstorm() -> Self { Self { weather_type: String::from("thunderstorm"), intensity: 1.0, wind_direction_deg: 180.0, wind_speed_ms: 15.0, temperature_c: 10.0, visibility_m: 100.0, precipitation_mm_hr: 20.0 } }
6605 pub fn rain(intensity: f32) -> Self { Self { weather_type: String::from("rain"), intensity, wind_direction_deg: 180.0, wind_speed_ms: 5.0, temperature_c: 15.0, visibility_m: 500.0, precipitation_mm_hr: intensity * 10.0 } }
6606 pub fn snow(intensity: f32) -> Self { Self { weather_type: String::from("snow"), intensity, wind_direction_deg: 270.0, wind_speed_ms: 3.0, temperature_c: -2.0, visibility_m: 300.0, precipitation_mm_hr: intensity * 5.0 } }
6607 pub fn is_dangerous(&self) -> bool { self.weather_type == "thunderstorm" || self.weather_type == "blizzard" || self.weather_type == "sandstorm" }
6608 pub fn ambient_light_factor(&self) -> f32 { match self.weather_type.as_str() { "clear" => 1.0, "thunderstorm" | "blizzard" | "heavy_rain" => 0.5, "rain" | "fog" => 0.7, "snow" => 0.85, _ => 0.8 } }
6609 pub fn movement_speed_modifier(&self) -> f32 { match self.weather_type.as_str() { "snow" | "thunderstorm" | "blizzard" => 0.7, "rain" | "heavy_rain" => 0.9, _ => 1.0 } }
6610}
6611impl MapSaveState {
6612 pub fn new(map_id: u32) -> Self { Self { map_id, player_x: 0.0, player_y: 0.0, visited_chunks: HashSet::new(), cleared_rooms: HashSet::new(), opened_chests: HashSet::new(), killed_enemies: HashSet::new(), world_time: 0.0, save_version: 1 } }
6613 pub fn mark_chest_opened(&mut self, id: u32) { self.opened_chests.insert(id); }
6614 pub fn mark_enemy_killed(&mut self, id: u64) { self.killed_enemies.insert(id); }
6615 pub fn mark_room_cleared(&mut self, id: u32) { self.cleared_rooms.insert(id); }
6616 pub fn mark_chunk_visited(&mut self, x: i32, y: i32) { self.visited_chunks.insert((x, y)); }
6617 pub fn is_room_cleared(&self, id: u32) -> bool { self.cleared_rooms.contains(&id) }
6618 pub fn is_chest_opened(&self, id: u32) -> bool { self.opened_chests.contains(&id) }
6619 pub fn is_enemy_killed(&self, id: u64) -> bool { self.killed_enemies.contains(&id) }
6620 pub fn is_chunk_visited(&self, x: i32, y: i32) -> bool { self.visited_chunks.contains(&(x, y)) }
6621 pub fn visited_chunk_count(&self) -> usize { self.visited_chunks.len() }
6622 pub fn advance_time(&mut self, dt: f64) { self.world_time += dt; }
6623 pub fn time_of_day_fraction(&self) -> f64 { (self.world_time % 86400.0) / 86400.0 }
6624 pub fn serialize_header(&self) -> Vec<u8> { let mut h = b"MSAV".to_vec(); h.extend_from_slice(&self.map_id.to_le_bytes()); h.extend_from_slice(&(self.visited_chunks.len() as u32).to_le_bytes()); h.extend_from_slice(&(self.cleared_rooms.len() as u32).to_le_bytes()); h.extend_from_slice(&self.save_version.to_le_bytes()); h }
6625}
6626impl TileAnimation {
6627 pub fn new(tile_id: u32, frames: Vec<u32>, frame_duration_ms: u32) -> Self {
6628 let n = frames.len();
6629 Self { tile_id, frames, frame_durations_ms: vec![frame_duration_ms; n], current_frame: 0, elapsed_ms: 0, loop_animation: true }
6630 }
6631 pub fn update(&mut self, dt_ms: u32) { self.update_ms(dt_ms); }
6632 pub fn update_ms(&mut self, dt_ms: u32) {
6633 self.elapsed_ms += dt_ms;
6634 if !self.frame_durations_ms.is_empty() {
6635 while self.elapsed_ms >= self.frame_durations_ms[self.current_frame] {
6636 self.elapsed_ms -= self.frame_durations_ms[self.current_frame];
6637 self.current_frame = (self.current_frame + 1) % self.frames.len().max(1);
6638 }
6639 }
6640 }
6641 pub fn reset(&mut self) { self.current_frame = 0; self.elapsed_ms = 0; }
6642 pub fn current_tile_id(&self) -> u32 { self.frames.get(self.current_frame).copied().unwrap_or(self.tile_id) }
6643 pub fn total_duration_ms(&self) -> u32 { self.frame_durations_ms.iter().sum() }
6644}
6645
6646impl TileAnimationManager {
6647 pub fn new() -> Self { Self { animations: HashMap::new() } }
6648 pub fn standard_water_animation() -> TileAnimation { TileAnimation::new(0, vec![0,1,2,3], 200) }
6649 pub fn register(&mut self, anim: TileAnimation) { self.animations.insert(anim.tile_id, anim); }
6650 pub fn update_all(&mut self, dt_ms: u32) { for a in self.animations.values_mut() { a.update_ms(dt_ms); } }
6651 pub fn resolve_tile(&self, tile_id: u32) -> u32 { self.animations.get(&tile_id).map(|a| a.current_tile_id()).unwrap_or(tile_id) }
6652}
6653
6654impl ObjectLayer {
6655 pub fn new(id: u32, name: &str) -> Self { Self { id, name: name.to_string(), objects: Vec::new(), visible: true, locked: false } }
6656 pub fn add_object(&mut self, o: TileObject) { self.objects.push(o); }
6657 pub fn object_at(&self, x: i32, y: i32) -> Option<&TileObject> { self.objects.iter().find(|o| o.position == (x, y)) }
6658 pub fn find_overlaps(&self) -> Vec<(u32, u32)> { let mut r = Vec::new(); for i in 0..self.objects.len() { for j in i+1..self.objects.len() { if self.objects[i].position == self.objects[j].position { r.push((self.objects[i].id, self.objects[j].id)); } } } r }
6659}
6660
6661impl TmxExporter {
6662 pub fn new() -> Self { Self { version: "1.0".to_string() } }
6663 pub fn export(&self, _map: &TileMap) -> String { String::new() }
6664 pub fn export_map(&self, map: &TileMap, name: &str) -> String {
6665 format!(r#"{{"name":"{}","width":{},"height":{}}}"#, name, map.width, map.height)
6666 }
6667}
6668
6669impl CollisionMap {
6670 pub const SOLID: u8 = 1;
6671 pub fn new(w: u32, h: u32) -> Self { Self { width: w, height: h, cells: vec![0u8; (w*h) as usize] } }
6672 pub fn set(&mut self, x: u32, y: u32, v: u8) { if let Some(c) = self.cells.get_mut((y*self.width+x) as usize) { *c = v; } }
6673 pub fn get_cell(&self, x: u32, y: u32) -> u8 { self.cells.get((y*self.width+x) as usize).copied().unwrap_or(0) }
6674 pub fn is_solid(&self, x: u32, y: u32) -> bool { self.get_cell(x, y) == Self::SOLID }
6675 pub fn is_passable(&self, x: u32, y: u32) -> bool { !self.is_solid(x, y) }
6676 pub fn passable_neighbors(&self, x: u32, y: u32) -> Vec<(u32, u32)> { let mut n = Vec::new(); if x > 0 && self.is_passable(x-1,y) { n.push((x-1,y)); } if y > 0 && self.is_passable(x,y-1) { n.push((x,y-1)); } if x+1 < self.width && self.is_passable(x+1,y) { n.push((x+1,y)); } if y+1 < self.height && self.is_passable(x,y+1) { n.push((x,y+1)); } n }
6677 pub fn set_flag(&mut self, x: usize, y: usize, solid: bool) { if x < self.width as usize && y < self.height as usize { self.cells[y * self.width as usize + x] = solid as u8; } }
6678}
6679
6680impl SpawnPoint {
6681 pub fn new(id: u32, position: (i32, i32), spawn_type: &str) -> Self { Self { id, position, spawn_type: spawn_type.to_string(), max_concurrent: 1, current_count: 0, enabled: true, respawn_delay_secs: 30.0 } }
6682}
6683
6684impl SpawnManager {
6685 pub fn new() -> Self { Self { spawn_points: Vec::new(), global_spawn_enabled: true } }
6686 pub fn add(&mut self, sp: SpawnPoint) { self.spawn_points.push(sp); }
6687 pub fn active_count(&self) -> u32 { self.spawn_points.iter().map(|s| s.current_count).sum() }
6688 pub fn add_spawn_point(&mut self, sp: SpawnPoint) { self.add(sp); }
6689 pub fn available_spawns_for_type<'a>(&'a self, t: &str) -> Vec<&'a SpawnPoint> { self.spawn_points.iter().filter(|s| s.enabled && s.spawn_type == t).collect() }
6690 pub fn total_capacity(&self) -> u32 { self.spawn_points.iter().map(|s| s.max_concurrent).sum() }
6691}
6692
6693impl PartialEq for TilePos { fn eq(&self, o: &Self) -> bool { self.x==o.x && self.y==o.y } }
6694impl PartialEq for PatrolState { fn eq(&self, o: &Self) -> bool { std::mem::discriminant(self) == std::mem::discriminant(o) } }
6695
6696#[derive(Clone, Debug)]
6699pub struct MapBiome {
6700 pub id: u32,
6701 pub name: String,
6702 pub biome_type: String,
6703 pub temperature_range: (f32, f32),
6704 pub precipitation_mm: f32,
6705 pub tile_palette: Vec<u32>,
6706 pub ambient_color: [f32; 3],
6707 pub fog_density: f32,
6708 pub music_track: String,
6709}
6710
6711#[derive(Clone, Debug)]
6712pub struct BiomeTransition {
6713 pub from_biome: u32,
6714 pub to_biome: u32,
6715 pub blend_width: u32,
6716 pub transition_type: String,
6717}
6718
6719#[derive(Clone, Debug)]
6720pub struct MapClimateZone {
6721 pub id: u32,
6722 pub zone_type: String,
6723 pub bounds: (i32, i32, i32, i32),
6724 pub wind_speed: f32,
6725 pub wind_direction: f32,
6726 pub humidity: f32,
6727 pub season_modifier: f32,
6728}
6729
6730#[derive(Clone, Debug)]
6731pub struct MapNaturalFeature {
6732 pub id: u32,
6733 pub feature_type: String,
6734 pub position: (i32, i32),
6735 pub size: (u32, u32),
6736 pub name: String,
6737 pub is_landmark: bool,
6738 pub discovery_radius: u32,
6739}
6740
6741#[derive(Clone, Debug)]
6742pub struct MapSettlement {
6743 pub id: u32,
6744 pub name: String,
6745 pub position: (i32, i32),
6746 pub settlement_type: String,
6747 pub population: u32,
6748 pub faction: String,
6749 pub trade_routes: Vec<u32>,
6750 pub is_capital: bool,
6751}
6752
6753#[derive(Clone, Debug)]
6754pub struct MapRoadNetwork {
6755 pub roads: Vec<MapRoad>,
6756 pub intersections: Vec<(i32, i32)>,
6757}
6758
6759#[derive(Clone, Debug)]
6760pub struct MapRoad {
6761 pub id: u32,
6762 pub name: String,
6763 pub points: Vec<(i32, i32)>,
6764 pub road_type: String,
6765 pub width: u32,
6766}
6767
6768#[derive(Clone, Debug)]
6769pub struct MapFogOfWar {
6770 pub width: u32,
6771 pub height: u32,
6772 pub revealed: Vec<bool>,
6773 pub visible: Vec<bool>,
6774}
6775
6776#[derive(Clone, Debug)]
6777pub struct MapLevelTransition {
6778 pub id: u32,
6779 pub from_map: String,
6780 pub to_map: String,
6781 pub trigger_pos: (i32, i32),
6782 pub spawn_pos: (i32, i32),
6783 pub transition_type: String,
6784 pub requires_key: bool,
6785 pub key_item_id: u32,
6786}
6787
6788#[derive(Clone, Debug)]
6789pub struct MapQuestMarker {
6790 pub id: u32,
6791 pub quest_id: u32,
6792 pub position: (i32, i32),
6793 pub marker_type: String,
6794 pub label: String,
6795 pub visible: bool,
6796 pub blinking: bool,
6797}
6798
6799#[derive(Clone, Debug)]
6800pub struct MapObjectiveTracker {
6801 pub objectives: Vec<MapQuestMarker>,
6802 pub active_quest_id: Option<u32>,
6803 pub show_all: bool,
6804}
6805
6806#[derive(Clone, Debug)]
6807pub struct MapTeleportNetwork {
6808 pub nodes: Vec<TeleportNode>,
6809 pub connections: Vec<(u32, u32)>,
6810 pub enabled: bool,
6811}
6812
6813#[derive(Clone, Debug)]
6814pub struct TeleportNode {
6815 pub id: u32,
6816 pub name: String,
6817 pub position: (i32, i32),
6818 pub unlocked: bool,
6819 pub cost_gold: u32,
6820}
6821
6822impl MapBiome {
6825 pub fn new(id: u32, name: impl Into<String>, biome_type: impl Into<String>) -> Self {
6826 Self {
6827 id, name: name.into(), biome_type: biome_type.into(),
6828 temperature_range: (10.0, 25.0), precipitation_mm: 500.0,
6829 tile_palette: Vec::new(), ambient_color: [1.0, 1.0, 1.0],
6830 fog_density: 0.0, music_track: String::new(),
6831 }
6832 }
6833 pub fn is_arctic(&self) -> bool { self.temperature_range.1 < 0.0 }
6834 pub fn is_tropical(&self) -> bool { self.temperature_range.0 > 20.0 && self.precipitation_mm > 1500.0 }
6835 pub fn add_tile(&mut self, tile_id: u32) { self.tile_palette.push(tile_id); }
6836 pub fn set_music(&mut self, track: impl Into<String>) { self.music_track = track.into(); }
6837}
6838
6839impl Default for MapBiome {
6840 fn default() -> Self { Self::new(0, "default", "temperate") }
6841}
6842
6843impl MapClimateZone {
6844 pub fn new(id: u32, zone_type: impl Into<String>, bounds: (i32, i32, i32, i32)) -> Self {
6845 Self { id, zone_type: zone_type.into(), bounds, wind_speed: 5.0, wind_direction: 0.0, humidity: 0.5, season_modifier: 1.0 }
6846 }
6847 pub fn contains(&self, x: i32, y: i32) -> bool {
6848 x >= self.bounds.0 && x <= self.bounds.2 && y >= self.bounds.1 && y <= self.bounds.3
6849 }
6850 pub fn area(&self) -> i64 {
6851 (self.bounds.2 - self.bounds.0).abs() as i64 * (self.bounds.3 - self.bounds.1).abs() as i64
6852 }
6853}
6854
6855impl Default for MapClimateZone {
6856 fn default() -> Self { Self::new(0, "temperate", (0, 0, 100, 100)) }
6857}
6858
6859impl MapNaturalFeature {
6860 pub fn new(id: u32, feature_type: impl Into<String>, position: (i32, i32)) -> Self {
6861 Self { id, feature_type: feature_type.into(), position, size: (1, 1), name: String::new(), is_landmark: false, discovery_radius: 5 }
6862 }
6863 pub fn named(mut self, name: impl Into<String>) -> Self { self.name = name.into(); self }
6864 pub fn as_landmark(mut self) -> Self { self.is_landmark = true; self }
6865}
6866
6867impl MapSettlement {
6868 pub fn new(id: u32, name: impl Into<String>, position: (i32, i32), settlement_type: impl Into<String>) -> Self {
6869 Self { id, name: name.into(), position, settlement_type: settlement_type.into(), population: 0, faction: String::new(), trade_routes: Vec::new(), is_capital: false }
6870 }
6871 pub fn with_population(mut self, pop: u32) -> Self { self.population = pop; self }
6872 pub fn add_trade_route(&mut self, to_id: u32) { if !self.trade_routes.contains(&to_id) { self.trade_routes.push(to_id); } }
6873 pub fn is_city(&self) -> bool { self.population >= 5000 }
6874 pub fn is_village(&self) -> bool { self.population < 500 }
6875}
6876
6877impl MapRoadNetwork {
6878 pub fn new() -> Self { Self { roads: Vec::new(), intersections: Vec::new() } }
6879 pub fn add_road(&mut self, road: MapRoad) { self.roads.push(road); }
6880 pub fn total_length(&self) -> usize { self.roads.iter().map(|r| r.points.len().saturating_sub(1)).sum() }
6881 pub fn road_count(&self) -> usize { self.roads.len() }
6882}
6883
6884impl Default for MapRoadNetwork {
6885 fn default() -> Self { Self::new() }
6886}
6887
6888impl MapFogOfWar {
6889 pub fn new(width: u32, height: u32) -> Self {
6890 let size = (width * height) as usize;
6891 Self { width, height, revealed: vec![false; size], visible: vec![false; size] }
6892 }
6893 pub fn reveal(&mut self, x: u32, y: u32) {
6894 if let Some(idx) = self.idx(x, y) { self.revealed[idx] = true; self.visible[idx] = true; }
6895 }
6896 pub fn hide(&mut self, x: u32, y: u32) {
6897 if let Some(idx) = self.idx(x, y) { self.visible[idx] = false; }
6898 }
6899 pub fn is_revealed(&self, x: u32, y: u32) -> bool { self.idx(x, y).map(|i| self.revealed[i]).unwrap_or(false) }
6900 pub fn is_visible(&self, x: u32, y: u32) -> bool { self.idx(x, y).map(|i| self.visible[i]).unwrap_or(false) }
6901 fn idx(&self, x: u32, y: u32) -> Option<usize> {
6902 if x < self.width && y < self.height { Some((y * self.width + x) as usize) } else { None }
6903 }
6904 pub fn reveal_radius(&mut self, cx: i32, cy: i32, radius: i32) {
6905 for dy in -radius..=radius {
6906 for dx in -radius..=radius {
6907 if dx*dx + dy*dy <= radius*radius {
6908 let x = cx + dx; let y = cy + dy;
6909 if x >= 0 && y >= 0 { self.reveal(x as u32, y as u32); }
6910 }
6911 }
6912 }
6913 }
6914 pub fn revealed_percent(&self) -> f32 {
6915 let total = self.revealed.len();
6916 if total == 0 { return 0.0; }
6917 self.revealed.iter().filter(|&&r| r).count() as f32 / total as f32 * 100.0
6918 }
6919}
6920
6921impl MapLevelTransition {
6922 pub fn new(id: u32, from_map: impl Into<String>, to_map: impl Into<String>, trigger_pos: (i32, i32), spawn_pos: (i32, i32)) -> Self {
6923 Self { id, from_map: from_map.into(), to_map: to_map.into(), trigger_pos, spawn_pos, transition_type: "door".into(), requires_key: false, key_item_id: 0 }
6924 }
6925 pub fn locked(mut self, key_id: u32) -> Self { self.requires_key = true; self.key_item_id = key_id; self }
6926}
6927
6928impl MapObjectiveTracker {
6929 pub fn new() -> Self { Self { objectives: Vec::new(), active_quest_id: None, show_all: false } }
6930 pub fn add_objective(&mut self, marker: MapQuestMarker) { self.objectives.push(marker); }
6931 pub fn set_active_quest(&mut self, quest_id: u32) { self.active_quest_id = Some(quest_id); }
6932 pub fn visible_objectives(&self) -> Vec<&MapQuestMarker> {
6933 self.objectives.iter().filter(|o| o.visible && (self.show_all || self.active_quest_id == Some(o.quest_id))).collect()
6934 }
6935 pub fn complete_objective(&mut self, id: u32) { self.objectives.retain(|o| o.id != id); }
6936}
6937
6938impl Default for MapObjectiveTracker {
6939 fn default() -> Self { Self::new() }
6940}
6941
6942impl MapTeleportNetwork {
6943 pub fn new() -> Self { Self { nodes: Vec::new(), connections: Vec::new(), enabled: true } }
6944 pub fn add_node(&mut self, node: TeleportNode) { self.nodes.push(node); }
6945 pub fn connect(&mut self, a: u32, b: u32) {
6946 if !self.connections.contains(&(a, b)) && !self.connections.contains(&(b, a)) {
6947 self.connections.push((a, b));
6948 }
6949 }
6950 pub fn unlocked_nodes(&self) -> Vec<&TeleportNode> { self.nodes.iter().filter(|n| n.unlocked).collect() }
6951 pub fn can_travel(&self, from: u32, to: u32) -> bool {
6952 self.enabled && self.connections.iter().any(|&(a, b)| (a == from && b == to) || (a == to && b == from))
6953 }
6954}
6955
6956impl Default for MapTeleportNetwork {
6957 fn default() -> Self { Self::new() }
6958}
6959
6960pub const MAP_MAX_BIOMES: usize = 64;
6962pub const MAP_MAX_SETTLEMENTS: usize = 256;
6963pub const MAP_MAX_TRANSITIONS: usize = 128;
6964pub const MAP_FOW_CHUNK_SIZE: u32 = 16;
6965pub const MAP_DEFAULT_DISCOVERY_RADIUS: u32 = 8;
6966pub const MAP_TELEPORT_BASE_COST: u32 = 50;
6967
6968pub fn map_system_info() -> &'static str {
6969 "MapSystem v2.0 — biomes, climate zones, fog-of-war, settlements, teleport network"
6970}
6971
6972
6973#[derive(Clone, Debug, Default)]
6976pub struct MapEventTrigger {
6977 pub id: u32,
6978 pub position: (i32, i32),
6979 pub radius: u32,
6980 pub event_id: String,
6981 pub one_shot: bool,
6982 pub triggered: bool,
6983}
6984
6985impl MapEventTrigger {
6986 pub fn new(id: u32, position: (i32, i32), radius: u32, event_id: impl Into<String>) -> Self {
6987 Self { id, position, radius, event_id: event_id.into(), one_shot: true, triggered: false }
6988 }
6989 pub fn in_range(&self, x: i32, y: i32) -> bool {
6990 let dx = x - self.position.0; let dy = y - self.position.1;
6991 (dx*dx + dy*dy) as u32 <= self.radius * self.radius
6992 }
6993 pub fn trigger(&mut self) -> bool {
6994 if self.triggered && self.one_shot { return false; }
6995 self.triggered = true; true
6996 }
6997 pub fn reset(&mut self) { self.triggered = false; }
6998}
6999
7000pub const MAP_MAX_EVENT_TRIGGERS: usize = 512;
7001pub const MAP_CHUNK_LOAD_RADIUS: u32 = 3;
7002pub const MAP_CHUNK_UNLOAD_RADIUS: u32 = 5;
7003pub const MAP_MIN_ROAD_WIDTH: u32 = 1;
7004pub const MAP_MAX_ROAD_WIDTH: u32 = 8;
7005