Skip to main content

audio_code/
level.rs

1//! Build levels from text maps.
2//!
3//! Drawing a map as text is the quickest way to make a level:
4//!
5//! ```no_run
6//! # use audio_code::prelude::*;
7//! # fn f(ctx: &mut Context) {
8//! let level = ctx.physics.build_level(
9//!     "
10//!     ##########
11//!     #P...#..C#
12//!     #....#...#
13//!     #..C.....#
14//!     ##########
15//!     ",
16//!     2.0, // every character is a 2 x 2 meter tile
17//! );
18//! let start = level.point('P').unwrap();
19//! for coin in level.points('C') {
20//!     // place a coin at `coin`
21//! }
22//! # }
23//! ```
24//!
25//! * `#` becomes a fixed wall tagged `"wall"` (3 meters high). Neighbouring walls in a row are
26//!   merged into one body.
27//! * `.` and spaces are empty floor.
28//! * Every other character is remembered as a point you can look up with [`Level::point`]
29//!   and [`Level::points`].
30//!
31//! The first line of the map is north (highest y), columns go east (x). Leading and trailing
32//! blank lines and the common indentation are ignored, so the map can be written inline.
33
34use std::collections::HashMap;
35
36use glam::Vec3;
37
38use crate::physics::{Body, BodyId, World};
39
40const WALL_HEIGHT: f32 = 3.0;
41
42/// The result of [`World::build_level`].
43#[derive(Debug, Clone, Default)]
44pub struct Level {
45    /// Wall bodies that were created.
46    pub walls: Vec<BodyId>,
47    points: HashMap<char, Vec<Vec3>>,
48    width: f32,
49    depth: f32,
50}
51
52impl Level {
53    /// Center of the first tile marked with `marker`, e.g. the player start.
54    pub fn point(&self, marker: char) -> Option<Vec3> {
55        self.points.get(&marker).and_then(|p| p.first().copied())
56    }
57
58    /// Centers of all tiles marked with `marker`, from north-west to south-east.
59    pub fn points(&self, marker: char) -> Vec<Vec3> {
60        self.points.get(&marker).cloned().unwrap_or_default()
61    }
62
63    /// Size of the map in meters (east-west, north-south).
64    pub fn size(&self) -> (f32, f32) {
65        (self.width, self.depth)
66    }
67}
68
69impl World {
70    /// Creates walls from a text map. See the [`level`](crate::level) module.
71    pub fn build_level(&mut self, map: &str, tile_size: f32) -> Level {
72        let lines: Vec<&str> = map.lines().collect();
73        let first = lines.iter().position(|l| !l.trim().is_empty()).unwrap_or(lines.len());
74        let last = lines.iter().rposition(|l| !l.trim().is_empty()).map_or(first, |i| i + 1);
75        let lines = &lines[first..last];
76        let indent = lines
77            .iter()
78            .filter(|l| !l.trim().is_empty())
79            .map(|l| l.len() - l.trim_start().len())
80            .min()
81            .unwrap_or(0);
82        let rows: Vec<Vec<char>> = lines.iter().map(|l| l.chars().skip(indent).collect()).collect();
83
84        let tile = tile_size.max(0.01);
85        let height = rows.len();
86        let mut level = Level {
87            width: rows.iter().map(Vec::len).max().unwrap_or(0) as f32 * tile,
88            depth: height as f32 * tile,
89            ..Level::default()
90        };
91        let center = |col: f32, row: usize| Vec3::new((col + 0.5) * tile, (height - 1 - row) as f32 * tile + tile * 0.5, 0.0);
92
93        for (r, row) in rows.iter().enumerate() {
94            let mut c = 0;
95            while c < row.len() {
96                match row[c] {
97                    '#' => {
98                        let start = c;
99                        while c < row.len() && row[c] == '#' {
100                            c += 1;
101                        }
102                        let run = (c - start) as f32;
103                        let mut pos = center(start as f32 + (run - 1.0) / 2.0, r);
104                        pos.z = WALL_HEIGHT / 2.0;
105                        let wall = Body::fixed(pos).with_box(run * tile, tile, WALL_HEIGHT).with_tag("wall");
106                        level.walls.push(self.add(wall));
107                        continue;
108                    }
109                    '.' | ' ' => {}
110                    marker => level.points.entry(marker).or_default().push(center(c as f32, r)),
111                }
112                c += 1;
113            }
114        }
115        level
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn parses_walls_and_points() {
125        let mut w = World::new();
126        let level = w.build_level(
127            "
128            ####
129            #P.#
130            #.C#
131            ####
132            ",
133            2.0,
134        );
135        // Top and bottom rows merge into one body each, the side walls are single tiles.
136        assert_eq!(level.walls.len(), 6);
137        assert_eq!(level.point('P'), Some(Vec3::new(3.0, 5.0, 0.0)));
138        assert_eq!(level.points('C'), vec![Vec3::new(5.0, 3.0, 0.0)]);
139        assert_eq!(level.size(), (8.0, 8.0));
140        assert!(w.at_point(Vec3::new(1.0, 7.0, 1.0)).len() == 1, "north west corner is a wall");
141
142        let player = w.add(Body::kinematic(level.point('P').unwrap()).with_sphere(0.4));
143        assert!(w.move_by(player, Vec3::new(0.0, 5.0, 0.0)).blocked);
144    }
145}