draw_lr/
lib.rs

1#![warn(missing_docs)]
2//! Crate to make maps & interact with the [Line Rider](https://linerider.com) game. The base module in the crate contains the
3//! main object definitions and the extension(s) build on top of them.
4
5use std::fmt::Debug;
6use std::fs;
7
8use serde::Serialize;
9
10/// Coordinate system to represent vectors.
11#[derive(Default, Serialize, Debug, Copy, Clone)]
12pub struct Coordinates {
13    x: f64,
14    y: f64,
15}
16
17/// Riders (characters with snowboards) with some starting position/velocity.
18#[derive(Default, Serialize, Debug, Copy, Clone)]
19pub struct Rider {
20    #[serde(rename = "startPosition")]
21    start_position: Coordinates,
22    #[serde(rename = "startVelocity")]
23    start_velocity: Coordinates,
24    remountable: usize,
25}
26
27/// Layers on the game.
28#[derive(Default, Serialize, Debug)]
29pub struct Layer {
30    id: usize,
31    name: String,
32    visible: bool,
33    editable: bool,
34}
35
36impl Layer {
37    /// Default layer for the game.
38    pub fn new() -> Self {
39        Layer {
40            id: 0,
41            name: "Base Layer".to_string(),
42            visible: true,
43            editable: true,
44        }
45    }
46}
47
48/// Single line representation representation that stretches from (`x1`, `y1`) to (`x2`, `y2`)
49/// on the 2D coordinate system of the game; and of type `kind`.
50#[derive(Default, Serialize, Debug, Copy, Clone)]
51pub struct Line {
52    /// Game requires (unique) id for every line -- we make this an Option type so that the game handles the
53    /// enumeration of lines passed to it
54    id: Option<usize>,
55    #[serde(rename = "type")] // JSON representation has "type", which is a reserved keyword in Rust
56    kind: usize,
57    x1: f64,
58    y1: f64,
59    x2: f64,
60    y2: f64,
61    /// Whether the line is flipped
62    flipped: bool,
63    #[serde(rename = "leftExtended")] // JSON representation requires camelCase; Rust requires snake_case
64    left_extended: bool,
65    #[serde(rename = "rightExtended")]
66    right_extended: bool,
67}
68
69/// Version used for game.
70#[derive(Serialize, Debug)]
71pub struct Version(String);
72
73/// Crate only tested version for default game -- can override.
74impl Default for Version {
75    fn default() -> Self {
76        Self("6.2".into())
77    }
78}
79
80/// Main representation of a Line Rider game.
81#[derive(Default, Serialize, Debug)]
82pub struct Game {
83    label: String,
84    creator: String,
85    description: String,
86    duration: usize,
87    version: Version,
88    audio: Option<String>,
89    #[serde(rename = "startPosition")] // JSON representation requires camelCase; Rust requires snake_case
90    start_position: Coordinates,
91    riders: Vec<Rider>,
92    layers: Vec<Layer>,
93    lines: Vec<Line>,
94}
95
96impl Game {
97    /// Creates a new version of the game that can be instantiated and have lines added to.
98    pub fn new() -> Self {
99        Game {
100            label: "Track created by lr-rust".to_string(),
101            creator: "lr-rust".to_string(),
102            duration: 120,
103            layers: Vec::from([Layer::new()]),
104            ..Game::default()
105        }
106    }
107
108    /// Add a singular line to the game.
109    pub fn add_line(&mut self, line: &Line) {
110        let line_with_id = Line {
111            id: Some(self.lines.len() + 1),
112            ..*line
113        };
114        self.lines.push(line_with_id);
115    }
116
117    /// Add several lines to the game.
118    pub fn add_lines<'a, T: Iterator<Item = &'a Line>>(&mut self, lines: T) {
119        for line in lines {
120            self.add_line(line);
121        }
122    }
123
124    /// Add a singular rider to the game.
125    pub fn add_rider(&mut self, rider: &Rider) {
126        self.riders.push(*rider);
127    }
128
129    /// Add several riders to the game.
130    pub fn add_riders<'a, T: Iterator<Item = &'a Rider>>(&mut self, riders: T) {
131        for rider in riders {
132            self.add_rider(rider);
133        }
134    }
135
136    /// Construct JSON representation of game that can be imported.
137    pub fn construct_game(&self) -> String {
138        serde_json::to_string(&self).unwrap()
139    }
140
141    /// Writes the JSON representation to a given filename.
142    pub fn write_to_file(&self, filename: &str) -> std::io::Result<()> {
143        fs::write(filename, self.construct_game())?;
144        Ok(())
145    }
146}
147
148/// Extension definitions and functions to create Line Rider maps with.
149pub mod extension {
150    use std::ops::Range;
151
152    use crate::*;
153
154    /// Options for passing coordinates to functions.
155    #[derive(Clone, Copy)]
156    pub enum CoordOptions {
157        /// Random coordinates in a default range
158        Rand,
159        /// Random coordinates over range
160        RandRange(Coordinates, Coordinates),
161        /// Specify exact coordinates (`Some`) or use default (`None`)
162        Other(Option<Coordinates>),
163        /// Evenly space out coordinates over range
164        EvenlySpaced(Coordinates, Coordinates),
165    }
166
167    /// Creates a list of `n` riders, at some `start_position` and `speed_range`; all with characteristic `remountable`.
168    pub fn create_riders(n: usize, start_position: CoordOptions, speed_range: CoordOptions, remountable: Option<usize>) -> Vec<Rider> {
169        fn check_min_max(min: f64, max: f64) {
170            // Min/max checks might not be strictly necessary, but may prevent bugs.
171            if max < min {
172                panic!("Max ({:.}) is less than min ({:.})", max, min);
173            }
174        }
175        fn even_spaced_rider(min: Coordinates, max: Coordinates, i: usize, n: usize) -> Coordinates {
176            check_min_max(min.x, max.x);
177            check_min_max(min.y, max.y);
178
179            Coordinates {
180                x: min.x + i as f64 * (max.x - min.x) / (n - 1) as f64,
181                y: min.y + i as f64 * (max.y - min.y) / (n - 1) as f64,
182            }
183        }
184        /// Gets a random number between range.
185        fn coord_between(min: f64, max: f64) -> f64 {
186            check_min_max(min, max);
187            min + rand::random::<f64>() * (max - min)
188        }
189
190        fn match_coords(coordinates: CoordOptions, i: usize, n: usize) -> Option<Coordinates> {
191            match coordinates {
192                CoordOptions::Rand => Some(Coordinates {
193                    x: coord_between(-10.0, 10.0),
194                    y: coord_between(-10.0, 10.0),
195                }),
196
197                CoordOptions::RandRange(min, max) => Some(Coordinates {
198                    x: coord_between(min.x, max.x),
199                    y: coord_between(min.y, max.y),
200                }),
201
202                CoordOptions::EvenlySpaced(min, max) => Some(even_spaced_rider(min, max, i, n)),
203                CoordOptions::Other(x) => x,
204            }
205        }
206
207        let mut riders: Vec<Rider> = Vec::new();
208
209        for i in 0..n {
210            let start_position: Option<Coordinates> = match_coords(start_position, i, n);
211            let start_velocity: Option<Coordinates> = match_coords(speed_range, i, n);
212
213            riders.push(Rider {
214                start_position: start_position.unwrap_or_default(),
215                start_velocity: start_velocity.unwrap_or_default(),
216                remountable: remountable.unwrap_or_default(),
217            });
218        }
219
220        riders
221    }
222
223    /// Creates a polygon with given characteristics.
224    /// As sides -> \inf, the function can better approximate a circle.
225    pub fn polygon_lines(sides: u16, radius: u16, start_position: Option<Coordinates>, rotation: Option<f64>, kind: usize) -> Vec<Line> {
226        let center = start_position.unwrap_or_default();
227
228        let mut polygon_lines: Vec<Line> = Vec::new();
229
230        let vertex_degree: f64 = std::f64::consts::TAU / sides as f64;
231        let initial_angle: f64 = vertex_degree / 2_f64 + rotation.unwrap_or_default();
232
233        // We'll use a sliding window from the first point that starts (vertex_angle) / 2 clockwise from 0° until we've gone around 360°.
234        let mut first_point: Coordinates = Coordinates {
235            x: center.x + radius as f64 * initial_angle.cos(),
236            y: center.y + radius as f64 * initial_angle.sin(),
237        };
238
239        for i in 1..sides + 1 {
240            let second_point: Coordinates = Coordinates {
241                x: center.x + radius as f64 * (initial_angle + i as f64 * vertex_degree).cos(),
242                y: center.y + radius as f64 * (initial_angle + i as f64 * vertex_degree).sin(),
243            };
244
245            polygon_lines.push(Line {
246                id: None,
247                kind,
248                x1: first_point.x,
249                y1: first_point.y,
250                x2: second_point.x,
251                y2: second_point.y,
252                flipped: true,
253                left_extended: true,
254                right_extended: true,
255            });
256
257            first_point = second_point;
258        }
259
260        polygon_lines
261    }
262
263    /// Creates lines to sketch out polygon with a given thickness (see `polygon_lines`).
264    pub fn thick_polygon_lines(
265        sides: u16,
266        radius: u16,
267        start_position: Option<Coordinates>,
268        rotation: Option<f64>,
269        thickness: u16,
270        kind: usize,
271    ) -> Vec<Line> {
272        let mut single_polygon_lines: Vec<Line> = Vec::new();
273
274        for i in 0..thickness {
275            single_polygon_lines.extend(polygon_lines(sides, radius + i, start_position, rotation, kind));
276        }
277
278        single_polygon_lines
279    }
280
281    /// Creates and returns lines to sketch out a function `func` over a given range `range`;
282    /// with n `iterations` done over integer steps. All lines created will be of type `kind`.
283    pub fn function_lines(func: fn(f64) -> f64, range: Range<i64>, iterations: Option<u8>, kind: Option<usize>) -> Vec<Line> {
284        let mut function_lines: Vec<Line> = Vec::new();
285        let num_iterations = iterations.unwrap_or(10);
286
287        let mut last_x: f64 = range.start as f64;
288        let mut last_y: f64 = func(last_x);
289
290        for i in range {
291            for j in (1..num_iterations).rev() {
292                // Approximate divisions between integer units
293                let x = i as f64 + (j as f64 / num_iterations as f64);
294                let y = func(x);
295                function_lines.push(Line {
296                    kind: kind.unwrap_or(1),
297                    x1: last_x,
298                    y1: last_y,
299                    x2: x,
300                    y2: y,
301                    ..Line::default()
302                });
303                last_x = x;
304                last_y = y;
305            }
306        }
307
308        function_lines
309    }
310}