/// Test builders for creating Bevy World and game state /// /// Provides fluent API for constructing test environments with minimal boilerplate. use borders_core::prelude::*; pub struct TestWorld { map_width: u16, map_height: u16, players: Vec<(NationId, f32, Option)>, // (id, troops, name) territories: Vec<(NationId, Vec)>, terrain: Option, spawn_phase_active: bool, rng_seed: u64, } impl TestWorld { /// Create a new test world builder with default settings pub fn new() -> Self { Self { map_width: 100, map_height: 100, players: Vec::new(), territories: Vec::new(), terrain: None, spawn_phase_active: false, rng_seed: 0xDEADBEEF } } /// Set the map size pub fn with_map_size(mut self, width: u16, height: u16) -> Self { self.map_width = width; self.map_height = height; self } /// Add a player with specified troops pub fn with_player(mut self, id: NationId, troops: f32) -> Self { self.players.push((id, troops, None)); self } /// Add a named player pub fn with_named_player(mut self, id: NationId, troops: f32, name: String) -> Self { self.players.push((id, troops, Some(name))); self } /// Set territory ownership for a player pub fn with_territory(mut self, id: NationId, tiles: &[U16Vec2]) -> Self { self.territories.push((id, tiles.to_vec())); self } /// Provide custom terrain data pub fn with_terrain(mut self, terrain: TerrainData) -> Self { self.terrain = Some(terrain); self } /// Activate spawn phase pub fn with_spawn_phase(mut self) -> Self { self.spawn_phase_active = true; self } /// Set RNG seed for deterministic tests pub fn with_rng_seed(mut self, seed: u64) -> Self { self.rng_seed = seed; self } /// Build the World with all configured state pub fn build(self) -> World { let mut world = World::new(); // Initialize Time resources (required by many systems) world.insert_resource(Time::default()); world.insert_resource(FixedTime::from_seconds(0.1)); // Generate or use provided terrain let terrain = self.terrain.unwrap_or_else(|| MapBuilder::new(self.map_width, self.map_height).all_conquerable().build()); // Initialize TerritoryManager let mut territory_manager = TerritoryManager::new(self.map_width, self.map_height); let conquerable_tiles: Vec = { let mut tiles = Vec::with_capacity((self.map_width as usize) * (self.map_height as usize)); for y in 0..self.map_height { for x in 0..self.map_width { tiles.push(terrain.is_conquerable(U16Vec2::new(x, y))); } } tiles }; territory_manager.reset(self.map_width, self.map_height, &conquerable_tiles); // Apply territory ownership for (nation_id, tiles) in &self.territories { for &tile in tiles { territory_manager.conquer(tile, *nation_id); } } // Create player entities let mut entity_map = PlayerEntityMap::default(); for (i, (nation_id, troops, name)) in self.players.iter().enumerate() { let player_name = name.clone().unwrap_or_else(|| format!("Player {}", nation_id.get())); let color = HSLColor::new( (i as f32 * 137.5) % 360.0, // golden angle distribution 0.6, 0.5, ); let entity = world.spawn((*nation_id, PlayerName(player_name), PlayerColor(color), BorderTiles::default(), Troops(*troops), TerritorySize(0), borders_core::game::ships::ShipCount::default())).id(); entity_map.0.insert(*nation_id, entity); } // Insert core resources world.insert_resource(entity_map); world.insert_resource(territory_manager); world.insert_resource(ActiveAttacks::new()); world.insert_resource(terrain); world.insert_resource(DeterministicRng::new(self.rng_seed)); world.insert_resource(BorderCache::default()); world.insert_resource(AttackControls::default()); world.insert_resource(ClientPlayerId(NationId::ZERO)); world.insert_resource(HumanPlayerCount(1)); world.insert_resource(LocalPlayerContext::new(NationId::ZERO)); // Optional spawn phase if self.spawn_phase_active { world.insert_resource(SpawnPhase { active: true }); world.insert_resource(SpawnTimeout::new(30.0)); } // Compute coastal tiles let map_size = U16Vec2::new(self.map_width, self.map_height); let coastal_tiles = CoastalTiles::compute(world.resource::(), map_size); world.insert_resource(coastal_tiles); world } } impl Default for TestWorld { fn default() -> Self { Self::new() } } /// Builder for programmatic terrain generation /// /// # Example /// ``` /// let terrain = MapBuilder::new(100, 100) /// .all_conquerable() /// .build(); /// ``` pub struct MapBuilder { width: u16, height: u16, terrain_data: Vec, tile_types: Vec, } impl MapBuilder { /// Create a new map builder pub fn new(width: u16, height: u16) -> Self { let size = (width as usize) * (height as usize); Self { width, height, terrain_data: vec![0; size], tile_types: Vec::new() } } /// Make all tiles land and conquerable pub fn all_conquerable(mut self) -> Self { let size = (self.width as usize) * (self.height as usize); self.terrain_data = vec![0x80; size]; // bit 7 = land/conquerable self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }]; self } /// Create islands: center 50x50 land, rest water pub fn islands(mut self) -> Self { self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }]; let center_x = self.width / 2; let center_y = self.height / 2; let island_size = 25u16; for y in 0..self.height { for x in 0..self.width { let idx = (y as usize) * (self.width as usize) + (x as usize); let in_island = x.abs_diff(center_x) < island_size && y.abs_diff(center_y) < island_size; self.terrain_data[idx] = if in_island { 0x80 } else { 0x00 }; } } self } /// Create continents: alternating vertical strips of land/water pub fn continents(mut self) -> Self { self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }]; for y in 0..self.height { for x in 0..self.width { let idx = (y as usize) * (self.width as usize) + (x as usize); // 30-tile wide strips let is_land = (x / 30) % 2 == 0; self.terrain_data[idx] = if is_land { 0x80 } else { 0x00 }; } } self } /// Set specific tiles as land pub fn with_land(mut self, tiles: &[U16Vec2]) -> Self { for &tile in tiles { let idx = (tile.y as usize) * (self.width as usize) + (tile.x as usize); if idx < self.terrain_data.len() { self.terrain_data[idx] = 0x80; } } self } /// Build the TerrainData pub fn build(self) -> TerrainData { let tiles: Vec = self.terrain_data.iter().map(|&byte| if byte & 0x80 != 0 { 1 } else { 0 }).collect(); let num_land_tiles = tiles.iter().filter(|&&t| t == 1).count(); let terrain_tile_map = TileMap::from_vec(self.width, self.height, self.terrain_data); TerrainData { _manifest: MapManifest { map: MapMetadata { size: U16Vec2::new(self.width, self.height), num_land_tiles }, name: "Test Map".to_string(), nations: Vec::new() }, terrain_data: terrain_tile_map, tiles, tile_types: self.tile_types } } }