//! World setup: Tiled map, spawning buildings and items, and tile occupancy. use std::{collections::BTreeMap, sync::Arc}; use bevy::prelude::*; use bevy_replicon::prelude::*; use game_core::{ GameData, components::{ Building, BuildingState, Character, GridPos, GroundItem, ItemStack, Machine, WorldMap, }, map::MapLayout, }; use crate::{ServerSettings, persistence}; /// The loaded map, used for collision and spawn points. #[derive(Resource, Clone)] pub struct WorldLayout(pub Arc); #[derive(Resource, Clone, Copy)] pub struct SpawnPoint(pub GridPos); pub fn setup_world(mut commands: Commands, settings: Res, data: Res) { let assets = game_core::assets_dir(); let layout = MapLayout::load(&assets, &settings.map).unwrap_or_else(|e| panic!("{e}")); let spawn = layout .objects_of_class("spawn") .next() .map(|o| o.pos) .unwrap_or_else(|| { warn!("map has no `spawn` object, using the map center"); GridPos::new(layout.width as i32 / 2, layout.height as i32 / 2) }); commands.insert_resource(SpawnPoint(spawn)); commands.spawn(( Replicated, WorldMap { path: settings.map.clone(), }, )); match persistence::load(settings.save_path.as_deref()) { Some(save) => { info!( "restoring persistent world: {} buildings, {} items, {} characters", save.buildings.len(), save.items.len(), save.characters.len() ); if save.map != settings.map { warn!( "save was made on map `{}`, now running `{}`", save.map, settings.map ); } persistence::restore(&mut commands, &data, save); } None => { info!("creating a new world from `{}`", settings.map); spawn_map_objects(&mut commands, &data, &layout); } } commands.insert_resource(WorldLayout(Arc::new(layout))); } fn spawn_map_objects(commands: &mut Commands, data: &GameData, layout: &MapLayout) { for obj in &layout.objects { let proto = obj.properties.get("proto"); match (obj.class.as_str(), proto) { ("spawn", _) => {} ("building", Some(proto)) => { // Integer custom properties override the building's script variables. let vars = obj .properties .iter() .filter_map(|(k, v)| Some((k.clone(), v.parse::().ok()?))) .collect(); if spawn_building(commands, data, proto, obj.pos, vars).is_none() { warn!("map object {}: unknown building `{proto}`", obj.id); } } ("item", Some(proto)) => { let count = obj .properties .get("count") .and_then(|c| c.parse().ok()) .unwrap_or(1); if data.items.contains_key(proto) { spawn_ground_item(commands, ItemStack::new(proto, count), obj.pos); } else { warn!("map object {}: unknown item `{proto}`", obj.id); } } (class, _) => warn!( "map object {} (`{}`): unsupported class `{class}` or missing `proto` property", obj.id, obj.name ), } } } /// Spawns a building with its default state. `vars` override the prototype's variables. pub fn spawn_building( commands: &mut Commands, data: &GameData, proto: &str, pos: GridPos, vars: BTreeMap, ) -> Option { let def = data.buildings.get(proto)?; let mut state = BuildingState { solid: def.solid, variant: None, health: def.max_health, vars: def.vars.clone(), }; state.vars.extend(vars); let machine = data.machines.get(proto).map(Machine::new); Some(spawn_building_with_state( commands, proto, pos, state, machine, )) } pub fn spawn_building_with_state( commands: &mut Commands, proto: &str, pos: GridPos, state: BuildingState, machine: Option, ) -> Entity { let mut entity = commands.spawn(( Replicated, Building { proto: proto.to_owned(), }, pos, state, )); if let Some(machine) = machine { entity.insert(machine); } entity.id() } pub fn spawn_ground_item(commands: &mut Commands, stack: ItemStack, pos: GridPos) -> Entity { commands.spawn((Replicated, GroundItem(stack), pos)).id() } /// Whether a character can not enter `pos`. pub fn is_blocked<'a>( pos: GridPos, layout: &MapLayout, buildings: impl IntoIterator, ) -> bool { layout.is_solid(pos) || buildings.into_iter().any(|(p, s)| *p == pos && s.solid) } /// Finds the closest free tile around `start` (spiral search), for spawning characters. pub fn find_free_tile( start: GridPos, layout: &MapLayout, buildings: &Query<(&GridPos, &BuildingState), (With, Without)>, ) -> GridPos { for radius in 0..16i32 { for dy in -radius..=radius { for dx in -radius..=radius { if dx.abs() != radius && dy.abs() != radius { continue; } let pos = GridPos::new(start.x + dx, start.y + dy); if !is_blocked(pos, layout, buildings.iter()) { return pos; } } } } start }