//! Village map: grid, obstacles, named places, pathfinding, building meshes. use bevy::prelude::*; use std::collections::{HashMap, VecDeque}; pub const GRID: i32 = 64; #[derive(Resource)] pub struct WorldMap { /// true = blocked (buildings, props) pub blocked: Vec, /// lowercase place name -> cell pub pois: HashMap, } impl WorldMap { pub fn idx(cell: IVec2) -> usize { (cell.y * GRID + cell.x) as usize } pub fn inside(cell: IVec2) -> bool { cell.x >= 0 && cell.y >= 0 && cell.x < GRID && cell.y < GRID } pub fn walkable(&self, cell: IVec2) -> bool { Self::inside(cell) && !self.blocked[Self::idx(cell)] } pub fn place(&self, name: &str) -> Option { self.pois.get(&name.to_lowercase()).copied() } /// 4-directional BFS. If `to` is blocked, targets its nearest walkable neighbor. pub fn path(&self, from: IVec2, to: IVec2) -> Option> { let goal = if self.walkable(to) { to } else { self.nearest_walkable(to)? }; if from == goal { return Some(vec![]); } let mut queue = VecDeque::from([from]); let mut parent: HashMap = HashMap::new(); parent.insert(from, from); let dirs = [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y]; while let Some(cur) = queue.pop_front() { for d in dirs { let next = cur + d; if !self.walkable(next) || parent.contains_key(&next) { continue; } parent.insert(next, cur); if next == goal { let mut path = vec![next]; let mut c = next; while c != from { c = parent[&c]; path.push(c); } path.pop(); // drop start cell path.reverse(); return Some(path); } queue.push_back(next); } } None } fn nearest_walkable(&self, cell: IVec2) -> Option { let mut queue = VecDeque::from([cell]); let mut seen = HashMap::from([(cell, ())]); while let Some(c) = queue.pop_front() { if self.walkable(c) { return Some(c); } for d in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] { let n = c + d; if Self::inside(n) && !seen.contains_key(&n) { seen.insert(n, ()); queue.push_back(n); } } } None } pub fn random_walkable(&self) -> IVec2 { // Deterministic-ish wander using a cheap LCG seeded by position hash is // overkill; callers pass their own randomness. Fallback: scan. for i in 0..self.blocked.len() { if !self.blocked[i] && i % 7 == 3 { return IVec2::new((i % GRID as usize) as i32, (i / GRID as usize) as i32); } } IVec2::new(GRID / 2, GRID / 2) } } fn cell_to_world(cell: IVec2) -> Vec3 { Vec3::new(cell.x as f32 + 0.5, 0.0, cell.y as f32 + 0.5) } /// Marker for the main isometric camera (pan/zoom/pick target). #[derive(Component)] pub struct VillageCamera; pub fn spawn_village( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let mut blocked = vec![false; (GRID * GRID) as usize]; let mut pois: HashMap = HashMap::new(); let ground = meshes.add(Cuboid::from_size(Vec3::new(GRID as f32, 0.2, GRID as f32))); commands.spawn(( Mesh3d(ground), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.36, 0.47, 0.28), perceptual_roughness: 1.0, ..default() })), Transform::from_xyz(GRID as f32 / 2.0, -0.1, GRID as f32 / 2.0), )); // Plaza (walkable, center) + well in its middle (blocked cell, edge used) let center = IVec2::new(GRID / 2, GRID / 2); let plaza_cells = [ center, center + IVec2::X, center + IVec2::NEG_X, center + IVec2::Y, center + IVec2::NEG_Y, center + IVec2::new(1, 1), center + IVec2::new(-1, -1), center + IVec2::new(1, -1), center + IVec2::new(-1, 1), ]; for c in plaza_cells { set_floor(&mut blocked, c, Color::srgb(0.55, 0.5, 0.42), &mut commands, &mut meshes, &mut materials, 0.03); } pois.insert("plaza".into(), center); let well = center + IVec2::new(0, -2); building(&mut commands, &mut meshes, &mut materials, well, 1.0, 1.4, Color::srgb(0.45, 0.45, 0.5)); set_blocked(&mut blocked, well); pois.insert("well".into(), well); // Named buildings around the plaza let named: &[(&str, IVec2, Vec3, Color)] = &[ ("market", center + IVec2::new(6, 0), Vec3::new(4.0, 2.0, 3.0), Color::srgb(0.72, 0.55, 0.3)), ("tavern", center + IVec2::new(-6, 1), Vec3::new(3.0, 2.4, 3.0), Color::srgb(0.6, 0.35, 0.25)), ("workshop", center + IVec2::new(1, 6), Vec3::new(3.0, 1.8, 2.0), Color::srgb(0.5, 0.42, 0.32)), ("weavery", center + IVec2::new(-2, -7), Vec3::new(3.0, 1.8, 2.0), Color::srgb(0.55, 0.45, 0.55)), ("scriptorium", center + IVec2::new(7, -6), Vec3::new(2.5, 2.6, 2.5), Color::srgb(0.4, 0.4, 0.6)), ("granary", center + IVec2::new(-7, -5), Vec3::new(3.5, 2.2, 2.5), Color::srgb(0.65, 0.6, 0.4)), ]; for (name, cell, size, color) in named { building(&mut commands, &mut meshes, &mut materials, *cell, size.x, size.y, *color); stamp_blocked(&mut blocked, *cell, *size); pois.insert((*name).into(), *cell + IVec2::new(0, size.z as i32 / 2)); // door side } // Houses: two rings of cottages (cells stay walkable around them) let mut homes: Vec = Vec::new(); let mut house_idx = 0usize; for radius in [16i32, 26] { for (dx, dy) in [(1, 0), (0, 1), (-1, 0), (0, -1), (1, 1), (-1, -1), (1, -1), (-1, 1)] { let base = center + IVec2::new(dx * radius, dy * radius); for step in -2..=2 { let c = base + IVec2::new(dy * step * 4, dx * step * 4); if !WorldMap::inside(c) || blocked[WorldMap::idx(c)] { continue; } building(&mut commands, &mut meshes, &mut materials, c, 2.0, 1.6, Color::srgb(0.7, 0.62, 0.5)); set_blocked(&mut blocked, c); homes.push(c); house_idx += 1; if house_idx >= 34 { break; } } if house_idx >= 34 { break; } } if house_idx >= 34 { break; } } // Homes are per-person, addressed by index in the prompt ("your home: (x,y)") for (i, c) in homes.iter().enumerate() { pois.insert(format!("home_{i}"), *c); } // Fields: large walkable work areas at the map edge let field_centers = [ IVec2::new(6, GRID / 2), IVec2::new(6, GRID / 2 + 8), IVec2::new(GRID - 7, GRID / 2), IVec2::new(GRID - 7, GRID / 2 + 8), IVec2::new(GRID / 2, GRID - 7), IVec2::new(GRID / 2 + 8, GRID - 7), ]; for (i, fc) in field_centers.iter().enumerate() { for y in 0..3 { for x in 0..3 { let c = fc + IVec2::new(x - 1, y - 1); if WorldMap::inside(c) { set_floor( &mut blocked, c, Color::srgb(0.5, 0.42 + (i as f32) * 0.01, 0.2), &mut commands, &mut meshes, &mut materials, 0.04, ); } } } pois.insert(format!("field_{i}"), *fc); pois.insert("field".into(), *fc); // generic alias } // Roads: cross through the plaza (visual only) for axis in 0..2 { for i in 0..GRID { let c = if axis == 0 { IVec2::new(i, GRID / 2) } else { IVec2::new(GRID / 2, i) }; if blocked[WorldMap::idx(c)] { continue; } let tile = meshes.add(Cuboid::from_size(Vec3::new(0.9, 0.05, 0.9))); commands.spawn(( Mesh3d(tile), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.48, 0.44, 0.36), perceptual_roughness: 1.0, ..default() })), Transform::from_translation(cell_to_world(c) + Vec3::new(0.0, 0.03, 0.0)), )); } } // Lights + iso camera commands.spawn(( DirectionalLight { illuminance: 12_000.0, shadow_maps_enabled: false, ..default() }, Transform::from_xyz(40.0, 80.0, 20.0).looking_at(Vec3::new(32.0, 0.0, 32.0), Vec3::Y), )); commands.spawn(( Camera3d::default(), VillageCamera, Projection::Orthographic(OrthographicProjection { scale: 0.11, ..OrthographicProjection::default_3d() }), Transform::from_xyz(96.0, 96.0, 96.0).looking_at(Vec3::new(32.0, 0.0, 32.0), Vec3::Y), )); commands.insert_resource(WorldMap { blocked, pois }); } fn set_blocked(blocked: &mut [bool], cell: IVec2) { if WorldMap::inside(cell) { blocked[WorldMap::idx(cell)] = true; } } fn stamp_blocked(blocked: &mut [bool], cell: IVec2, size: Vec3) { let w = size.x as i32; let h = size.z as i32; for dy in 0..h.max(1) { for dx in 0..w.max(1) { set_blocked(blocked, cell + IVec2::new(dx - w / 2, dy - h / 2)); } } } fn building( commands: &mut Commands, meshes: &mut Assets, materials: &mut Assets, cell: IVec2, w: f32, h: f32, color: Color, ) { let mesh = meshes.add(Cuboid::from_size(Vec3::new(w, h, 2.0))); let pos = cell_to_world(cell); commands.spawn(( Mesh3d(mesh), MeshMaterial3d(materials.add(StandardMaterial { base_color: color, perceptual_roughness: 0.9, ..default() })), Transform::from_translation(pos + Vec3::new(0.0, h / 2.0, 0.0)), )); // pitched-roof suggestion: a smaller cube on top let roof = meshes.add(Cuboid::from_size(Vec3::new(w * 0.8, 0.6, 1.6))); commands.spawn(( Mesh3d(roof), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.4, 0.26, 0.22), perceptual_roughness: 1.0, ..default() })), Transform::from_translation(pos + Vec3::new(0.0, h + 0.3, 0.0)), )); } fn set_floor( _blocked: &mut [bool], cell: IVec2, color: Color, commands: &mut Commands, meshes: &mut Assets, materials: &mut Assets, height: f32, ) { if !WorldMap::inside(cell) { return; } let mesh = meshes.add(Cuboid::from_size(Vec3::new(0.96, height * 2.0, 0.96))); commands.spawn(( Mesh3d(mesh), MeshMaterial3d(materials.add(StandardMaterial { base_color: color, perceptual_roughness: 1.0, ..default() })), Transform::from_translation(cell_to_world(cell) + Vec3::new(0.0, height, 0.0)), )); }