199 lines
6.2 KiB
Rust
199 lines
6.2 KiB
Rust
//! 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<MapLayout>);
|
|
|
|
#[derive(Resource, Clone, Copy)]
|
|
pub struct SpawnPoint(pub GridPos);
|
|
|
|
pub fn setup_world(mut commands: Commands, settings: Res<ServerSettings>, data: Res<GameData>) {
|
|
let assets = game_core::assets_dir();
|
|
let layout = match MapLayout::load(&assets, &settings.map) {
|
|
Ok(layout) => layout,
|
|
Err(e) => return crate::fail(&mut commands, e.to_string()),
|
|
};
|
|
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(),
|
|
},
|
|
));
|
|
|
|
let mut save = None;
|
|
if let Some(storage) = &settings.storage {
|
|
let (storage, loaded) = match persistence::Storage::open(storage, &settings.map) {
|
|
Ok(opened) => opened,
|
|
Err(e) => {
|
|
return crate::fail(
|
|
&mut commands,
|
|
format!("failed to open the world database: {e}"),
|
|
);
|
|
}
|
|
};
|
|
info!("world `{}` ({})", storage.world.name, storage.world.id);
|
|
if loaded.is_some() && storage.world.map != settings.map {
|
|
warn!(
|
|
"world was created on map `{}`, now running `{}`",
|
|
storage.world.map, settings.map
|
|
);
|
|
}
|
|
save = loaded;
|
|
commands.insert_resource(storage);
|
|
}
|
|
match save {
|
|
Some(save) => {
|
|
info!(
|
|
"restoring persistent world: {} buildings, {} items, {} characters",
|
|
save.buildings.len(),
|
|
save.items.len(),
|
|
save.characters.len()
|
|
);
|
|
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::<i64>().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<String, i64>,
|
|
) -> Option<Entity> {
|
|
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<Machine>,
|
|
) -> 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<Item = (&'a GridPos, &'a BuildingState)>,
|
|
) -> 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<Building>, Without<Character>)>,
|
|
) -> 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
|
|
}
|