Skip to main content

proof_engine/editor/
map_editor.rs

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
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10const 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// ============================================================
28// TILE DEFINITIONS
29// ============================================================
30
31#[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
142// 8-bit neighbor mask → 47-tile frame
143pub fn blob_8bit_to_47(mask: u8) -> u8 {
144    // Canonical 47-tile blob: use lookup table
145    // mask bits: NW=0, N=1, NE=2, W=3, E=4, SW=5, S=6, SE=7
146    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        // Suppress diagonals if adjacent cardinals absent
163        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),  // x, y, w, h
240    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// ============================================================
308// MINIMAP
309// ============================================================
310
311#[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
355// ============================================================
356// JUMP POINT SEARCH
357// ============================================================
358
359pub 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// ============================================================
383// MAP EVENTS
384// ============================================================
385
386#[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// ============================================================
494// COPY/PASTE
495// ============================================================
496
497#[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// ============================================================
677// MAP STATISTICS
678// ============================================================
679
680#[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// ============================================================
725// TILESET MANAGEMENT
726// ============================================================
727
728#[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
809// ============================================================
810// MAP TESTS
811// ============================================================
812
813pub fn run_all_map_tests() -> bool {
814    // Test tilemap creation
815    {
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    // Test paint and undo
822    {
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    // Test flood fill
835    {
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    // Test A*
843    {
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    // Test BSP
850    {
851        let mut gen = BspDungeonGenerator::new(42);
852        let (rooms, corridors) = gen.generate(64, 64, 4);
853        assert!(!rooms.is_empty());
854    }
855    // Test minimap
856    {
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    // Test serialization round-trip
864    {
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    // Test patrol path
874    {
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))); // looped
886    }
887    // Test encounter zone containment
888    {
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
906// ============================================================
907// TILEMAP QUERY UTILITIES
908// ============================================================
909
910pub 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
996// ============================================================
997// FULL SAMPLE DUNGEON BUILDER
998// ============================================================
999
1000pub 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    // Place lights in rooms
1005    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    // Add encounter zones per room
1019    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// ============================================================
1054// MAP REGION SYSTEM
1055// ============================================================
1056
1057#[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), // x, y, w, h
1066    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    // World map test
1287    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    // Treasure test
1300    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    // NPC test
1311    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    // Quest test
1324    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    // Trap test
1339    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    // Door test
1347    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    // Destructible test
1353    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    // Fog of war test
1361    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    // Level transition test
1367    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)); // won't matter since no req set
1370
1371    // Height map test
1372    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    // Overworld generator test
1379    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    // Dungeon theme test
1388    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    // Water body test
1394    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// ============================================================
1402// TILE ANIMATION SYSTEM
1403// ============================================================
1404
1405#[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    // Tile animation
1470    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    // Object layer
1482    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    // TMX export
1492    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    // Collision map
1498    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    // Spawn manager
1506    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    // Chunk streaming
1515    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    // Only chunks with recent access remain
1521    let _ = csm.get_tile_at(5, 5);
1522
1523    // World map and quests
1524    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    // Treasure
1535    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    // Door
1544    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    // Trap
1551    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    // Fog of war
1558    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    // Level transition
1566    let lt = LevelTransition::new(1, (0, 10), "next_level", (1, 1));
1567    assert!(lt.can_use(|_| true, |_| true));
1568
1569    // Height map full test
1570    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    // Overworld
1581    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// ============================================================
1592// MAP DIALOGUE TRIGGER SYSTEM
1593// ============================================================
1594
1595#[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    // Trigger system
1725    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()); // one_shot
1733    assert_eq!(tm.fired_count(), 1);
1734
1735    // Warp network
1736    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    // Sound zone
1748    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    // AoE manager
1758    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); // frost expired
1766    aoe.update(1.0);
1767    assert!(aoe.zones.is_empty());
1768
1769    // Static lights + light map
1770    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    // Stats tracker
1781    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    // Map exporter
1791    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    // Plugin system
1804    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    // Full dungeon generation + stats tracking
1813    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    // Dynamic light map with flickering torch
1819    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    // Fog complete workflow
1825    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    // Overworld with roads connecting towns
1832    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    // All rivers should start at town positions
1840    for (i, river) in rivers.iter().enumerate() { assert!(!river.is_empty(), "River {} is empty", i); }
1841}
1842
1843// ============================================================
1844// SECTION: Dungeon Room Placement System
1845// ============================================================
1846
1847#[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, // minimap pixels per world tile
1923}
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    // At least some rooms should be connected
1969    let connected = rooms.iter().filter(|r| !r.connections.is_empty() || r.room_type == DungeonRoomType::Entrance).count();
1970    assert!(connected > 0);
1971    // Boss room should exist in larger dungeons
1972    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    // All rooms should have non-zero area
1977    for room in &rooms { assert!(room.area() > 0); }
1978}
1979
1980pub fn run_pathfinding_tests() {
1981    let mut grid = PathfindingGrid::new(20, 20);
1982    // Block a wall
1983    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    // Verify path avoids blocked tiles
1991    for pos in &p { assert!(grid.is_passable(pos)); }
1992
1993    // Octile distance
1994    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    // After search timer expires
2043    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
2074// ============================================================
2075// SECTION: Map Tile Autotiling
2076// ============================================================
2077
2078/// Wang tile autotiling — 16-tile blob tileset index
2079pub 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    // Corners only count if both adjacent cardinals are also solid
2086    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
2093/// Returns a descriptive name for a Wang blob tile index
2094pub 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>,  // 0=empty, 1=solid
2115    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>, // damage amount, heal amount, etc.
2152}
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    // Center tile should be full interior (255)
2182    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    // Edge tile (top-left corner)
2186    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()); // guaranteed drop
2199    let boss_loot = registry.roll_table(2, 99999, 10);
2200    assert!(!boss_loot.is_empty());
2201    // Boss table weight
2202    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    // Objects layer is above terrain, check terrain tile if no object
2240    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
2273// ============================================================
2274// SECTION: Module Constants
2275// ============================================================
2276
2277pub 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; // "MAPE"
2295pub 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// ============================================================
2313// SECTION: Procedural Dungeon Content Placement
2314// ============================================================
2315
2316#[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    // Should have a spawn point
2377    let has_spawn = content.iter().any(|c| c.content_type == ContentType::Spawn);
2378    assert!(has_spawn || rooms[0].room_type == DungeonRoomType::Entrance);
2379    // Boss room if enough rooms
2380    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); // 4+4+4+4+4
2434}
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// ============================================================
2447// SECTION: Map Editor Selection & Clipboard
2448// ============================================================
2449
2450#[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>>,  // [row][col]
2461    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)>, // (x, y, old_tile, new_tile)
2481}
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    // Flip
2584    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// ============================================================
2638// SECTION: Tile Variant & Biome System
2639// ============================================================
2640
2641#[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// ============================================================
2745// SECTION: Encounter Zone System
2746// ============================================================
2747
2748#[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), // x1, y1, x2, y2
2776    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// ============================================================
2809// SECTION: Map Region Connection & Graph
2810// ============================================================
2811
2812#[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 &region.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// ============================================================
2880// SECTION: Dungeon Room System
2881// ============================================================
2882
2883#[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// ============================================================
2959// SECTION: A* Pathfinding
2960// ============================================================
2961
2962#[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// ============================================================
3019// SECTION: Map Event System
3020// ============================================================
3021
3022#[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// ============================================================
3058// SECTION: Minimap
3059// ============================================================
3060
3061#[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// ============================================================
3083// SECTION: NPC Patrol
3084// ============================================================
3085
3086#[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// ============================================================
3138// SECTION: Map Annotation Layer
3139// ============================================================
3140
3141#[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
3167// ============================================================
3168// SECTION: Tile Autotiling
3169// ============================================================
3170
3171pub 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// ============================================================
3197// SECTION: Loot Tables
3198// ============================================================
3199
3200#[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// ============================================================
3256// SECTION: Tile Effect Map
3257// ============================================================
3258
3259#[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// ============================================================
3279// SECTION: Map Layer Stack
3280// ============================================================
3281
3282#[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// ============================================================
3336// SECTION: Map Progression
3337// ============================================================
3338
3339#[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
3362// ============================================================
3363// SECTION: Integration Tests
3364// ============================================================
3365
3366pub 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
3517// Impl blocks for structs that need Default + new + methods
3518impl 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// ============================================================
3616// DUNGEON ROOM GENERATOR
3617// ============================================================
3618
3619#[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// ============================================================
3778// PROCEDURAL DUNGEON GENERATOR
3779// ============================================================
3780
3781#[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        // Connect rooms in a simple chain
3859        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// ============================================================
3872// WORLD MAP REGION SYSTEM
3873// ============================================================
3874
3875#[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 &region.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// ============================================================
4033// FACTION AND POLITICS SYSTEM
4034// ============================================================
4035
4036#[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// ============================================================
4102// SETTLEMENT / CITY SYSTEM
4103// ============================================================
4104
4105#[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// ============================================================
4163// QUEST SYSTEM
4164// ============================================================
4165
4166#[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// ============================================================
4266// ENCOUNTER / COMBAT SYSTEM
4267// ============================================================
4268
4269#[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// ============================================================
4325// MAP EDITOR TOOLS
4326// ============================================================
4327
4328#[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// ============================================================
4426// NAVMESH GENERATION (SIMPLIFIED 2D)
4427// ============================================================
4428
4429#[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// ============================================================
4554// TILE PALETTE AND TILESET
4555// ============================================================
4556
4557#[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// ============================================================
4613// MAP OBJECT PLACEMENT
4614// ============================================================
4615
4616#[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// ============================================================
4667// LIGHT BAKING / RADIOSITY (SIMPLIFIED)
4668// ============================================================
4669
4670#[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// ============================================================
4778// TEST FUNCTIONS
4779// ============================================================
4780
4781#[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// ============================================================
4880// WEATHER AND ENVIRONMENT SYSTEM
4881// ============================================================
4882
4883
4884#[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// ============================================================
4961// TIME OF DAY SYSTEM
4962// ============================================================
4963
4964#[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// ============================================================
5010// GAME ECONOMY SYSTEM
5011// ============================================================
5012
5013#[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// ============================================================
5060// LOOT TABLE SYSTEM
5061// ============================================================
5062
5063#[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// ============================================================
5120// NPC AI BEHAVIOR TREE
5121// ============================================================
5122
5123#[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// ============================================================
5207// MONSTER / ENEMY DATABASE
5208// ============================================================
5209
5210#[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
5267// ============================================================
5268// MAP IMPORT / EXPORT
5269// ============================================================
5270
5271pub const MAP_FILE_MAGIC: u32 = 0x4D415000; // "MAP\0"
5272pub 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// ============================================================
5309// ADDITIONAL TEST FUNCTIONS
5310// ============================================================
5311
5312#[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// ============================================================
5410// SPELL / ABILITY SYSTEM
5411// ============================================================
5412
5413#[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// ============================================================
5484// GAME CHARACTER STATS
5485// ============================================================
5486
5487#[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
5581// ============================================================
5582// ITEM / EQUIPMENT SYSTEM
5583// ============================================================
5584
5585impl 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// ============================================================
5649// CRAFTING SYSTEM
5650// ============================================================
5651
5652#[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// ============================================================
5694// DIALOGUE SCRIPTING ENGINE
5695// ============================================================
5696
5697#[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
5746// ============================================================
5747// MAP CHUNK STREAMING
5748// ============================================================
5749
5750pub 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// ============================================================
5855// ADDITIONAL TEST FUNCTIONS
5856// ============================================================
5857
5858#[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// ============================================================
5944// RANDOM ENCOUNTER TABLE SYSTEM
5945// ============================================================
5946
5947#[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// ============================================================
6002// POINT OF INTEREST SYSTEM
6003// ============================================================
6004
6005#[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
6072// ============================================================
6073// PUZZLE / TRAP SYSTEM
6074// ============================================================
6075
6076impl 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// ── Map Biome & Climate Extensions ──────────────────────────────────────────
6697
6698#[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
6822// ── Impl blocks ──────────────────────────────────────────────────────────────
6823
6824impl 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
6960// ── Map system constants ─────────────────────────────────────────────────────
6961pub 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// ── Additional map utility types ─────────────────────────────────────────────
6974
6975#[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