mission2teegarden_b_map/
lib.rs

1#![allow(clippy::tabs_in_doc_comments)]
2#![warn(rust_2018_idioms, unreachable_pub)]
3#![deny(rustdoc::bare_urls, rustdoc::broken_intra_doc_links)]
4#![forbid(unused_must_use, unsafe_code)]
5
6//! This crate allows you to create maps/levels for [Mission to Teegarden b](https://github.com/LuckyTurtleDev/mission2teegarden-b).
7//! It can be used instead of the game binary `mission2teegarden-b`.
8//! Every feature provided by this crate is also provided by the game itself.
9//! The only benefits is that this crate is much smaller.
10//!
11//! # Mapeditor
12//! Mission to Teegarden b allow creating custom maps, by using the [Tiled Map editor](https://www.mapeditor.org/).
13//! This does include support for available instructions and story.
14//! <div align="center">
15//!		<img src="https://github.com/LuckyTurtleDev/mission2teegarden-b/assets/44570204/68403ebd-ce64-4baa-bba2-b52962b89d5c" width=60%>
16//! </div>
17//!
18//! ### Limitaions
19//! There exist some conditions and limitation how the map is structured:
20//! * The map must be finite.
21//! * All layers must be finite.
22//! * No custom Tileset can be used. So only the Tilesets available at Github
23//! ([`BaseTiles.tsx`](https://github.com/LuckyTurtleDev/mission2teegarden_b/blob/main/pc/assets/img/BaseTiles/BaseTiles.tsx),
24//! [`ObjectTiles.tsx`](https://github.com/LuckyTurtleDev/mission2teegarden_b/blob/main/pc/assets/img/ObjectTiles/ObjectTiles.tsx),
25//! [`Player.tsx`](https://github.com/LuckyTurtleDev/mission2teegarden_b/blob/main/pc/assets/img/Player/Player.tsx)) can be used.
26//! * All layers must be a Tile layer.
27//! * The 1. Layer must only use Tiles from the `BaseTiles` set.
28//! * The 2. Layer must only use Tiles from the `ObjectTiles` set.
29//! * The 3. Layer must only use Tiles from the `Player` set.
30//! * If a field at layer 1. is not set `Grass` is used as default.
31//! * If player `i` have a start position. All player `<i` must also have a start position.
32//! * At least player 1 must have a start position.
33//! * If a global goal was not set, each player (which have a start position), must have a player goal.
34//!
35//! ### Available Instructions
36//! Available instruction can be added, by adding a "Custom properties" with type `int` to the Map.
37//! The properties must be named like the fields of the [`AvailableCards`](crate::AvailableCards) struct.
38//! If no properties for an instruction is set, `0` is used as default.
39//! Keep in mind that the player can only use `12` cards in total.
40//!
41//! ### Story
42//! An optional story can be added by creating a map property called `story` from type `string`
43//! As decoding the [toml](https://toml.io/) format is used.
44//! Currently, only story elements before and after the level are supported.
45//!
46//! Take a look at this example story:
47//! ```
48//! # let toml = r#"
49//! [[pre_level]]
50//! text = "hi, I am the captain ..."
51//! profil = "Captain"
52//! background = "OuterSpace"
53//!
54//! [[pre_level]]
55//! text = "now it is you turn!"
56//!
57//! [[after_level]]
58//! text = "You have mastered the challenge!"
59//! profil = "Captain"
60//! # "#;
61//! # let _config: mission2teegarden_b_map::story::Story = basic_toml::from_str(&toml).unwrap_or_else(|err| panic!("{}", err));
62//! ```
63//! The story exist out two lists `pre_level` and `after_level`, both are optional.
64//! Each list include zero or more [`Speech`s](crate::story::Speech).
65//! The [`Speech`s](crate::story::Speech) from `pre_level` are shown before the level starts.
66//! The ones from `after_level` are show, after the level was finish successfully.
67//! A [`Speech`](crate::story::Speech) exist out of a `text`, a `profil`picture and a `background`.
68//! The last two are optional.
69//! `profil` defined the picture, which is show left from the text.
70//! All variants of [`Character`](`crate::story::Character`) can be used for this.
71//! If `profil` is not set, no picture will be shown.
72//! `background` define the background with is show above the text.
73//! All variants of [`Background`](`crate::story::Background`) can be used for this.
74//! If `background` is not set, the level will be shown.
75//!
76//! For more informations see the [`Story`](`crate::story::Story`) struct.
77//!
78//! ### Map validation
79//! The map can be validated  by using the game or this crate,
80//! by exectuing one of the following commands.
81//! ```bash
82//! mission2teegarden-b validate-map <FILE>
83//! mission2teegarden-b-map validate <FILE>
84//! ```
85//!
86//! ### Map export
87//! Map exporting works similary to validation:
88//! ```bash
89//! mission2teegarden-b export-map <FILE>
90//! mission2teegarden-b-map export <FILE>
91//! ```
92//! Executing one of the commands creates a file with the same basename as the original file and the extension `.m2tb_map` inside the current working directory.
93//! Since the map format is not stable yet and can not be editet after exporting, it is strongly recommanded to keep the original `.tmx` file
94//!
95//! ### Play Map
96//! To play a map start the game and navigate to
97//! `Play -> Import Level`.
98//!
99//! Alternatvie the command line interface can be used:
100//! ```bash
101//! mission2teegarden-b play [FILE]
102//! ```
103
104use anyhow::{bail, Context};
105use basic_toml as toml;
106use log::debug;
107use mission2teegarden_b_models::AvailableCards;
108use ron::error::SpannedError;
109use serde::{
110	ser::{SerializeMap, Serializer},
111	Deserialize, Serialize
112};
113use std::{
114	f32::consts::PI,
115	ffi::OsStr,
116	fs::read_to_string,
117	io, iter,
118	path::{Path, PathBuf}
119};
120use story::Story;
121use thiserror::Error;
122use tiled::{LayerTile, LayerType, Loader, Properties};
123
124pub mod commands;
125pub mod story;
126pub mod tiles;
127use tiles::{InvalidTile, MapBaseTile, ObjectTile, Passable, PlayerTile, Tile};
128
129pub const MAP_FILE_EXTENSION: &str = "m2tb_map";
130
131/// allow Serialization of MapProporties
132struct PropertiesSerde(Properties);
133impl Serialize for PropertiesSerde {
134	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135	where
136		S: Serializer
137	{
138		let mut map = serializer.serialize_map(Some(self.0.len()))?;
139		for (key, value) in self.0.clone() {
140			match value {
141				tiled::PropertyValue::IntValue(value) => {
142					map.serialize_entry(&key, &value)
143				},
144				tiled::PropertyValue::BoolValue(value) => {
145					map.serialize_entry(&key, &value)
146				},
147				tiled::PropertyValue::FileValue(value) => {
148					map.serialize_entry(&key, &value)
149				},
150				tiled::PropertyValue::FloatValue(value) => {
151					map.serialize_entry(&key, &value)
152				},
153				tiled::PropertyValue::ColorValue(_) => Ok(()), /* should I return an error instead? */
154				tiled::PropertyValue::ObjectValue(value) => {
155					map.serialize_entry(&key, &value)
156				},
157				tiled::PropertyValue::StringValue(value) => {
158					map.serialize_entry(&key, &value)
159				},
160			}?;
161		}
162		map.end()
163	}
164}
165
166#[derive(Clone, Debug, Deserialize, Serialize)]
167struct MapProperties {
168	#[serde(flatten)]
169	cards: AvailableCards,
170	name: Option<String>,
171	story: Option<String>
172}
173
174#[derive(Clone, Debug, Deserialize, Serialize)]
175pub struct Player {
176	pub position: (u8, u8),
177	pub orientation: Orientation,
178	pub goal: Option<(u8, u8)>
179}
180
181#[derive(Clone, Debug, Deserialize, Serialize)]
182pub struct Map {
183	pub name: String,
184	pub width: u8,
185	pub height: u8,
186	pub base_layer: Vec<Vec<(MapBaseTile, Orientation)>>,
187	pub object_layer: Vec<Vec<Option<(ObjectTile, Orientation)>>>,
188	pub global_goal: Option<(u8, u8)>,
189	//this was a stupid idea
190	//now I must impl everything 4 times
191	//I should refactor this to `[Option<Player>;4]`, if I have time.
192	//Or add at leat an interator over all Players.
193	pub player_1: Player,
194	pub player_2: Option<Player>,
195	pub player_3: Option<Player>,
196	pub player_4: Option<Player>,
197	pub cards: AvailableCards,
198	#[serde(default)]
199	pub story: Story
200}
201
202#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
203pub enum Orientation {
204	#[default]
205	North,
206	South,
207	East,
208	West
209}
210
211impl Orientation {
212	/// return the rotate of the Orientation in grad
213	pub fn rotation(&self) -> f32 {
214		match self {
215			Self::North => 0.0,
216			Self::South => PI,
217			Self::East => 0.5 * PI,
218			Self::West => 1.5 * PI
219		}
220	}
221}
222
223#[derive(Error, Debug)]
224#[error("Invalid Tile Oritation (horizontally flip: {}, vertically flip: {}, diagonally flip: {})\nKeep in mind that only rotation is supported", .filp_h, .filp_v, .filp_d)]
225pub struct InvalidOritation {
226	///Whether this tile is flipped on its Y axis (horizontally).
227	filp_h: bool,
228	///Whether this tile is flipped on its X axis (vertically).
229	filp_v: bool,
230	///Whether this tile is flipped diagonally.
231	filp_d: bool
232}
233
234impl TryFrom<&LayerTile<'_>> for Orientation {
235	type Error = InvalidOritation;
236	fn try_from(value: &LayerTile<'_>) -> Result<Self, Self::Error> {
237		match (value.flip_h, value.flip_v, value.flip_d) {
238			(false, false, false) => Ok(Orientation::North),
239			(true, true, false) => Ok(Orientation::South),
240			(true, false, true) => Ok(Orientation::East),
241			(false, true, true) => Ok(Orientation::West),
242			_ => Err(InvalidOritation {
243				filp_h: value.flip_h,
244				filp_v: value.flip_v,
245				filp_d: value.flip_d
246			})
247		}
248	}
249}
250
251#[derive(Error, Debug)]
252pub enum MapError {
253	#[error("error loading file {0}")]
254	TieledError(#[from] tiled::Error),
255	#[error("map has to many layers")]
256	ToManyLayers,
257	#[error("{0}. Layer should be a {1}")]
258	WrongLayerType(usize, String),
259	#[error("{0}. Layer Infinite")]
260	InfiniteTileLayer(String),
261	#[error("Map is to widht. Max size is 255x255 tiles")]
262	ToWidth,
263	#[error("Map is to hight. Max size is 255x255 tiles")]
264	ToHight,
265	#[error("Found invalid Tile at Layes {0}: {1}")]
266	InvalidTile(usize, InvalidTile),
267	#[error("Player is missing")]
268	PlayerMissing(usize),
269	#[error("{0}")]
270	InvalidOritation(#[from] InvalidOritation),
271	#[error("Failed to load Map Properties:\n{}\n{}", .str, .err)]
272	MapProperty { str: String, err: serde_json::Error },
273	#[error("failed to read file story file {1:?}:\n{0}")]
274	IoError(io::Error, PathBuf),
275	#[error("could not prase story toml file:\n{0}")]
276	TomlError(#[from] toml::Error)
277}
278
279impl Map {
280	///return if all static tiles at x,y postion are passable
281	pub fn passable(&self, x: u8, y: u8) -> bool {
282		if x >= self.width || y >= self.height {
283			//car can leave the map an drive away (game over)
284			return true;
285		}
286		self.base_layer[x as usize][y as usize].0.passable()
287			&& self.object_layer[x as usize][y as usize]
288				.map(|obejct| obejct.0.passable())
289				.unwrap_or(true)
290	}
291
292	///Load map from String.
293	///Allowing to load map from binary format
294	pub fn from_string(str: &str) -> Result<Self, SpannedError> {
295		ron::from_str(str)
296	}
297
298	///Convert map to String, to be used as binary file format
299	#[allow(clippy::inherent_to_string)]
300	pub fn to_string(&self) -> String {
301		ron::to_string(self).unwrap()
302	}
303
304	pub fn from_tmx(path: impl AsRef<Path>) -> Result<Self, MapError> {
305		let path = path.as_ref();
306		let map = Loader::new().load_tmx_map(path)?;
307		let width: u8 = map.width.try_into().map_err(|_| MapError::ToWidth)?;
308		let height: u8 = map.height.try_into().map_err(|_| MapError::ToHight)?;
309		let map_properties =
310			serde_json::to_string_pretty(&PropertiesSerde(map.properties.clone()))
311				.unwrap();
312		debug!("load Map Properties: {map_properties}");
313		//Do I really need to convert this to json and back?
314		//Is their no serde intern format, which I can use?
315		//Why can I not use ron for this https://github.com/ron-rs/ron/issues/456 ?
316		let map_properties: MapProperties = serde_json::from_str(&map_properties)
317			.map_err(|err| MapError::MapProperty {
318				str: map_properties,
319				err
320			})?;
321		let cards = map_properties.cards;
322		let name = map_properties
323			.name
324			.unwrap_or_else(|| path.to_string_lossy().into());
325		let mut base_layer = Vec::with_capacity(height as usize);
326		let mut object_layer = Vec::with_capacity(height as usize);
327		let mut global_goal = None;
328		let mut player_1 = None;
329		let mut player_2 = None;
330		let mut player_3 = None;
331		let mut player_4 = None;
332		for (i, layer) in map.layers().enumerate() {
333			// this is ugly. Should i refactor this?
334			match i {
335				0 => match layer.layer_type() {
336					LayerType::Tiles(tile_layer) => {
337						for x in 0..width {
338							let mut column = Vec::with_capacity(width as usize);
339							for y in 0..height {
340								let tile_and_orientation = match tile_layer
341									.get_tile(x.into(), y.into())
342								{
343									Some(tile) => (
344										MapBaseTile::try_from(&tile).map_err(|err| {
345											MapError::InvalidTile(i, err)
346										})?,
347										Orientation::try_from(&tile)?
348									),
349									None => (MapBaseTile::default(), Default::default())
350								};
351								column.push(tile_and_orientation);
352							}
353							base_layer.push(column);
354						}
355					},
356					_ => return Err(MapError::WrongLayerType(i, "TileLayer".to_owned()))
357				},
358				1 => match layer.layer_type() {
359					LayerType::Tiles(tile_layer) => {
360						for x in 0..width {
361							let mut column = Vec::with_capacity(width as usize);
362							for y in 0..height {
363								let tile = match tile_layer.get_tile(x.into(), y.into()) {
364									Some(tile) => Some((
365										ObjectTile::try_from(&tile).map_err(|err| {
366											MapError::InvalidTile(i, err)
367										})?,
368										Orientation::try_from(&tile)?
369									)),
370									None => None
371								};
372								column.push(tile);
373							}
374							object_layer.push(column);
375						}
376					},
377					_ => return Err(MapError::WrongLayerType(i, "TileLayer".to_owned()))
378				},
379				2 => match layer.layer_type() {
380					LayerType::Tiles(tile_layer) => {
381						let mut player1_goal = None;
382						let mut player2_goal = None;
383						let mut player3_goal = None;
384						let mut player4_goal = None;
385						for x in 0..width {
386							for y in 0..height {
387								if let Some(tile) =
388									tile_layer.get_tile(x.into(), y.into())
389								{
390									let orientation = Orientation::try_from(&tile)?;
391									let tile = PlayerTile::try_from(&tile)
392										.map_err(|err| MapError::InvalidTile(i, err))?;
393									let player = Some(Player {
394										position: (x, y),
395										orientation,
396										goal: None
397									});
398									let goal = Some((x, y));
399									match tile {
400										PlayerTile::Car1 => player_1 = player,
401										PlayerTile::Car2 => player_2 = player,
402										PlayerTile::Car3 => player_3 = player,
403										PlayerTile::Car4 => player_4 = player,
404										PlayerTile::Goal1 => player1_goal = goal,
405										PlayerTile::Goal2 => player2_goal = goal,
406										PlayerTile::Goal3 => player3_goal = goal,
407										PlayerTile::Goal4 => player4_goal = goal,
408										PlayerTile::GlobalGoal => global_goal = goal
409									}
410								}
411							}
412						}
413						//this has become ugly; Mabye I should store the players in another way
414						//maybe an arry of [player;4]
415						player_1 = player_1.map(|mut f| {
416							f.goal = player1_goal;
417							f
418						});
419						player_2 = player_2.map(|mut f| {
420							f.goal = player2_goal;
421							f
422						});
423						player_3 = player_3.map(|mut f| {
424							f.goal = player3_goal;
425							f
426						});
427						player_4 = player_4.map(|mut f| {
428							f.goal = player4_goal;
429							f
430						});
431					},
432					_ => return Err(MapError::WrongLayerType(i, "TileLayer".to_owned()))
433				},
434				_ => return Err(MapError::ToManyLayers)
435			}
436		}
437		let player_1 = player_1.ok_or(MapError::PlayerMissing(1))?;
438		// if player i does exist, player i-1 must also exist
439		if (player_4.is_some() && player_3.is_none())
440			|| (player_3.is_some() && player_2.is_none())
441		{
442			player_2.as_ref().ok_or(MapError::PlayerMissing(2))?;
443			player_3.as_ref().ok_or(MapError::PlayerMissing(3))?;
444			player_4.as_ref().ok_or(MapError::PlayerMissing(4))?;
445		}
446
447		let story: Story = if let Some(story_toml) = map_properties.story {
448			toml::from_str(&story_toml)?
449		} else {
450			Default::default()
451		};
452
453		Ok(Map {
454			name,
455			width,
456			height,
457			base_layer,
458			object_layer,
459			global_goal,
460			player_1,
461			player_2,
462			player_3,
463			player_4,
464			cards,
465			story
466		})
467	}
468
469	/// load a map from file.
470	/// Can be at tiled map or a Misson to Teegarden b map.
471	pub fn load_from_file<P>(path: P) -> anyhow::Result<Self>
472	where
473		P: AsRef<Path>
474	{
475		let path = path.as_ref();
476		if path.extension() == Some(OsStr::new(MAP_FILE_EXTENSION)) {
477			let file = read_to_string(path)
478				.with_context(|| format!("failed to read file {path:?}"))?;
479			let map = Self::from_string(&file).with_context(|| "failed to prase file")?;
480			return Ok(map);
481		}
482		if path.extension() == Some(OsStr::new("tmx")) {
483			let map = Self::from_tmx(path)?;
484			return Ok(map);
485		}
486		bail!(
487			"unsupported file extension {:?}",
488			path.extension().unwrap_or_else(|| OsStr::new("None"))
489		)
490	}
491
492	pub fn iter_player(&self) -> impl Iterator<Item = &Player> {
493		iter::once(&self.player_1)
494			.chain(iter::once(&self.player_2).flatten())
495			.chain(iter::once(&self.player_3).flatten())
496			.chain(iter::once(&self.player_4).flatten())
497	}
498
499	pub fn iter_mut_player(&mut self) -> impl Iterator<Item = &mut Player> {
500		iter::once(&mut self.player_1)
501			.chain(iter::once(&mut self.player_2).flatten())
502			.chain(iter::once(&mut self.player_3).flatten())
503			.chain(iter::once(&mut self.player_4).flatten())
504	}
505
506	/// return an iterator over all BasteTiles and its x and y postion
507	pub fn iter_base_layer(
508		&self
509	) -> impl Iterator<Item = (u8, u8, MapBaseTile, Orientation)> + '_ {
510		self.base_layer.iter().enumerate().flat_map(|(x, y_vec)| {
511			y_vec
512				.iter()
513				.enumerate()
514				.map(move |(y, item)| (x as u8, y as u8, item.0, item.1))
515		})
516	}
517
518	/// return an iterator over all ObjectTiles and its x and y postion
519	pub fn iter_object_layer(
520		&self
521	) -> impl Iterator<Item = (u8, u8, ObjectTile, Orientation)> + '_ {
522		self.object_layer.iter().enumerate().flat_map(|(x, y_vec)| {
523			y_vec.iter().enumerate().filter_map(move |(y, item)| {
524				item.map(|item| (x as u8, y as u8, item.0, item.1))
525			})
526		})
527	}
528
529	/// return an iterator over all player goals tiles and its x and y postion
530	pub fn iter_player_goals(&self) -> impl Iterator<Item = (u8, u8, PlayerTile)> + '_ {
531		iter::once(self.global_goal)
532			.flatten()
533			.map(|(x, y)| (x, y, PlayerTile::GlobalGoal))
534			.chain(
535				iter::once(&self.player_1)
536					.filter_map(|player| player.goal)
537					.map(|(x, y)| (x, y, PlayerTile::Goal1))
538			)
539			.chain(
540				iter::once(&self.player_2)
541					.flatten()
542					.filter_map(|player| player.goal)
543					.map(|(x, y)| (x, y, PlayerTile::Goal2))
544			)
545			.chain(
546				iter::once(&self.player_3)
547					.flatten()
548					.filter_map(|player| player.goal)
549					.map(|(x, y)| (x, y, PlayerTile::Goal3))
550			)
551			.chain(
552				iter::once(&self.player_4)
553					.flatten()
554					.filter_map(|player| player.goal)
555					.map(|(x, y)| (x, y, PlayerTile::Goal4))
556			)
557	}
558
559	/// return an iterator over all static Tiles and its x and y postion.
560	/// starting from the lowest layer
561	pub fn iter_all(&self) -> impl Iterator<Item = (u8, u8, Tile, Orientation)> + '_ {
562		let base = self.iter_base_layer().map(|(x, y, tile, orientation)| {
563			(x, y, Tile::MapBaseTile(tile.to_owned()), orientation)
564		});
565		let objects = self.iter_object_layer().map(|(x, y, tile, orientation)| {
566			(x, y, Tile::MapObjectTile(tile.to_owned()), orientation)
567		});
568		let goals = self
569			.iter_player_goals()
570			.map(|(x, y, tile)| (x, y, Tile::PlayerTile(tile), Orientation::default()));
571		base.chain(objects).chain(goals)
572	}
573}