94 lines
3.6 KiB
Rust
94 lines
3.6 KiB
Rust
//! World type: a named collection of boards loaded from a single `.toml` file.
|
|
|
|
use crate::board::Board;
|
|
use crate::map_file::MapFile;
|
|
use serde::Deserialize;
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
|
|
/// A named world containing one or more boards, loaded from a `.toml` file.
|
|
///
|
|
/// The file groups all boards under a `[boards]` table. Each board key maps
|
|
/// to a full board definition (the same structure as a single-board map file,
|
|
/// but nested under `[boards.<key>.*]`). [`World::start`] names the board the
|
|
/// player enters first.
|
|
///
|
|
/// Scripts live at the world level ([`World::scripts`]) rather than on each
|
|
/// board, so multiple boards can reference the same script name without
|
|
/// duplicating source text.
|
|
pub struct World {
|
|
/// Human-readable name for this world, from the `[world]` header.
|
|
pub name: String,
|
|
/// Key of the starting board; matches a key in [`World::boards`].
|
|
pub start: String,
|
|
/// Named Rhai scripts shared across all boards: script name → source text.
|
|
///
|
|
/// Objects on any board reference a script by name via
|
|
/// [`ObjectDef::script_name`](crate::object_def::ObjectDef). The script pool is
|
|
/// passed to [`GameState::from_world`](crate::game::GameState::from_world) at startup.
|
|
pub scripts: HashMap<String, String>,
|
|
/// All boards in this world, keyed by their identifier string.
|
|
///
|
|
/// Every board is always present here as a shared `Rc<RefCell<Board>>` — no board
|
|
/// is ever "removed" when active. [`GameState`](crate::game::GameState) holds a
|
|
/// clone of the active board's `Rc` and borrows through it.
|
|
pub boards: HashMap<String, Rc<RefCell<Board>>>,
|
|
}
|
|
|
|
/// Serde target for the top-level `.toml` world file.
|
|
#[derive(Deserialize)]
|
|
struct WorldFile {
|
|
world: WorldHeader,
|
|
/// The `[scripts]` table: script name → Rhai source. Shared across all boards.
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
scripts: HashMap<String, String>,
|
|
/// Each value deserializes as a [`MapFile`], reusing the existing per-board
|
|
/// serde types from `map_file.rs`.
|
|
boards: HashMap<String, MapFile>,
|
|
}
|
|
|
|
/// The `[world]` header section of a world file.
|
|
#[derive(Deserialize)]
|
|
struct WorldHeader {
|
|
/// Human-readable name for this world.
|
|
name: String,
|
|
/// Key of the board to start on; must match a key in `[boards]`.
|
|
start: String,
|
|
}
|
|
|
|
/// Load a world from a `.toml` world file at `path`.
|
|
///
|
|
/// Returns an error if the file cannot be read or parsed, if any board fails
|
|
/// the hard-error conversion (only a grid-dimension mismatch qualifies — all
|
|
/// other problems are recorded on [`Board::load_errors`]), or if [`World::start`]
|
|
/// does not match any board key in the file.
|
|
pub fn load(path: &str) -> Result<World, Box<dyn std::error::Error>> {
|
|
let text = std::fs::read_to_string(path)?;
|
|
let wf: WorldFile = toml::from_str(&text)?;
|
|
|
|
// Convert each MapFile into a Board. Grid-dimension mismatches are the only
|
|
// hard errors; everything else is nonfatal and lands on Board::load_errors.
|
|
let mut boards = HashMap::new();
|
|
for (key, map_file) in wf.boards {
|
|
let board = Board::try_from(map_file).map_err(|e| format!("board '{}': {}", key, e))?;
|
|
boards.insert(key, Rc::new(RefCell::new(board)));
|
|
}
|
|
|
|
// Guard: the start key must actually exist in the boards map.
|
|
if !boards.contains_key(&wf.world.start) {
|
|
return Err(format!(
|
|
"start board '{}' not found in world '{}'",
|
|
wf.world.start, wf.world.name
|
|
)
|
|
.into());
|
|
}
|
|
|
|
Ok(World {
|
|
name: wf.world.name,
|
|
start: wf.world.start,
|
|
scripts: wf.scripts,
|
|
boards,
|
|
})
|
|
}
|