pub mod colour; pub mod palette; use colour::Colour; use palette::Palette; #[derive(Debug, Eq, PartialEq)] struct Image { pixels: Vec, // 64 for SD, 256 for HD } #[derive(Debug, Eq, PartialEq)] struct Tile { id: String, // base36 string name: Option, wall: bool, animation_frames: Vec, } #[derive(Debug, Eq, PartialEq)] struct Position { x: u8, y: u8, } #[derive(Debug, Eq, PartialEq)] struct Instance { position: Position, id: String, // item / ending id } #[derive(Debug, Eq, PartialEq)] struct ExitInstance { position: Position, exit: Exit, } #[derive(Debug, Eq, PartialEq)] struct Dialogue { id: String, contents: String, } #[derive(Debug, Eq, PartialEq)] struct Sprite { id: String, // lowercase base36 name: Option, animation_frames: Vec, dialogue: Option, /// dialogue id room: String, /// room id position: Position, } /// avatar is a "sprite" in the game data but with a specific id #[derive(Debug, Eq, PartialEq)] struct Avatar { animation_frames: Vec, room: String, /// room id position: Position, } #[derive(Debug, Eq, PartialEq)] struct Item { id: String, animation_frames: Vec, name: Option, dialogue: Option, // dialogue id } #[derive(Debug, Eq, PartialEq)] struct Exit { /// destination room: String, /// id position: Position, } // same as a dialogue basically #[derive(Debug, Eq, PartialEq)] struct Ending { id: String, dialogue: String, } #[derive(Debug, Eq, PartialEq)] struct Room { id: String, palette: String, // id name: Option, tiles: Vec, // tile ids items: Vec, exits: Vec, endings: Vec, } #[derive(Debug, Eq, PartialEq)] struct Variable { id: String, initial_value: String, } #[derive(Debug, PartialEq)] struct Game { name: String, version: f64, room_format: u8, palettes: Vec, rooms: Vec, tiles: Vec, avatar: Avatar, sprites: Vec, items: Vec, dialogues: Vec, endings: Vec, variables: Vec, } fn example_image_chequers_1() -> Image { Image { pixels: vec![ 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, ] } } fn example_image_chequers_2() -> Image { Image { pixels: vec![ 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, 0,1,0,1,0,1,0,1, 1,0,1,0,1,0,1,0, ] } } fn example_avatar() -> Avatar { Avatar { animation_frames: vec![ Image { pixels: vec![ 0,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, 1,1,1,0,1,1,1,0, 1,0,0,1,1,0,0,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 0,1,1,1,1,1,1,0, ] }, Image { pixels: vec![ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, 1,1,1,0,1,1,1,0, 1,0,0,1,1,0,0,1, 1,1,1,1,1,1,1,1, 0,1,1,1,0,1,1,0, ] }, ], room: "0".to_string(), position: Position { x: 2, y: 5 } } } fn example_sprite() -> Sprite { Sprite { id: "a".to_string(), name: Some("hatch".to_string()), animation_frames: vec![ Image { pixels: vec![ 0,0,0,0,0,0,0,0, 0,1,1,1,1,0,0,0, 0,1,0,0,1,0,0,0, 0,0,1,1,1,1,0,0, 0,0,1,1,1,1,0,0, 0,1,0,1,1,1,1,0, 0,1,0,1,1,1,1,0, 0,1,1,0,1,1,1,1, ] } ], dialogue: Some("SPR_0".to_string()), room: "4".to_string(), position: Position { x: 9, y: 7 } } } fn example_item() -> Item { Item { id: "6".to_string(), animation_frames: vec![ Image { pixels: vec![ 0,1,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,1,0,0, 0,0,1,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,1,0, ] } ], name: Some("door".to_string()), dialogue: Some("ITM_2".to_string()) } } fn example_room() -> Room { Room { id: "a".to_string(), palette: "9".to_string(), name: Some("cellar 7".to_string()), tiles: vec![ "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"1l".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), "y".to_string(),"x".to_string(),"0".to_string(),"0".to_string(),"1j".to_string(),"0".to_string(),"0".to_string(),"1j".to_string(),"1l".to_string(),"0".to_string(),"1j".to_string(),"0".to_string(),"0".to_string(),"1j".to_string(),"0".to_string(),"0".to_string(), "y".to_string(),"y".to_string(),"x".to_string(),"k".to_string(),"k".to_string(),"1c".to_string(),"1x".to_string(),"1y".to_string(),"1m".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), "y".to_string(),"y".to_string(),"y".to_string(),"x".to_string(),"k".to_string(),"s".to_string(),"s".to_string(),"s".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"1g".to_string(),"1f".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"1i".to_string(),"1u".to_string(),"1u".to_string(),"1u".to_string(),"1v".to_string(),"11".to_string(),"19".to_string(),"1b".to_string(),"1a".to_string(),"1e".to_string(),"10".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"11".to_string(),"12".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"10".to_string(),"17".to_string(),"z".to_string(),"18".to_string(),"1e".to_string(),"12".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"1k".to_string(),"14".to_string(),"15".to_string(),"16".to_string(),"1h".to_string(),"z".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"10".to_string(),"1d".to_string(),"1v".to_string(),"1r".to_string(),"1s".to_string(),"1r".to_string(),"1q".to_string(),"1z".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"12".to_string(),"10".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"1i".to_string(),"1n".to_string(),"1o".to_string(),"1o".to_string(),"1o".to_string(),"1p".to_string(),"z".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"10".to_string(),"z".to_string(),"z".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"11".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"k".to_string(), "k".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"z".to_string(),"12".to_string(),"z".to_string(),"z".to_string(),"10".to_string(),"12".to_string(),"k".to_string(), "k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(),"k".to_string(), "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string() ], items: vec![ Instance {position: Position { x: 11, y: 5}, id: "d".to_string()}, Instance {position: Position { x: 8, y: 3}, id: "e".to_string()}, Instance {position: Position { x: 1, y: 0}, id: "5".to_string()}, Instance {position: Position { x: 2, y: 1}, id: "6".to_string()}, Instance {position: Position { x: 3, y: 2}, id: "6".to_string()}, ], exits: vec![ ExitInstance { position: Position { x: 3, y: 3}, exit: Exit { room: "3".to_string(), position: Position { x: 10, y: 6}} }, ], endings: vec![ Instance{position: Position { x: 8, y: 7 }, id: "undefined".to_string()}, ], } } fn example_room_string() -> String { include_str!("../test/resources/room").to_string() } fn example_game_default() -> Game { Game { name: "Write your game's title here".to_string(), version: 6.5, room_format: 1, palettes: vec![ Palette { id: "0".to_string(), name: None, colours: vec![ Colour {red: 0, green: 82, blue: 204 }, Colour {red: 128, green: 159, blue: 255 }, Colour {red: 255, green: 255, blue: 255 }, ] } ], rooms: vec![ Room { id: "0".to_string(), palette: "0".to_string(), name: None, tiles: vec![ "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"a".to_string(),"0".to_string(), "0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(),"0".to_string(), ], items: vec![], exits: vec![], endings: vec![] } ], tiles: vec![ Tile { id: "a".to_string(), name: None, wall: false, animation_frames: vec![ Image { pixels: vec![ 1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,1,1,0,0,1, 1,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1, ] } ] } ], avatar: Avatar { animation_frames: vec![ Image { pixels: vec![ 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, 1,0,1,1,1,1,0,1, 0,0,1,0,0,1,0,0, 0,0,1,0,0,1,0,0, ] } ], room: "0".to_string(), position: Position { x: 4, y: 4 } }, sprites: vec![ Sprite { id: "a".to_string(), name: None, animation_frames: vec![ Image { pixels: vec![ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,1,0,1,0,0,0,1, 0,1,1,1,0,0,0,1, 0,1,1,1,0,0,1,0, 0,1,1,1,1,1,0,0, 0,0,1,1,1,1,0,0, 0,0,1,0,0,1,0,0, ] } ], dialogue: Some("SPR_0".to_string()), room: "0".to_string(), position: Position { x: 8, y: 12 } } ], items: vec![ Item { id: "0".to_string(), animation_frames: vec![ Image { pixels: vec![ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,0, 0,1,1,0,0,1,0,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0, 0,0,0,0,0,0,0,0, ] }, ], name: Some("tea".to_string()), dialogue: Some("ITM_0".to_string()) }, ], dialogues: vec![ Dialogue { id: "SPR_0".to_string(), contents: "I'm a cat".to_string(), }, Dialogue { id: "ITM_0".to_string(), contents: "You found a nice warm cup of tea".to_string(), }, ], endings: vec![], variables: vec![ Variable { id: "a".to_string(), initial_value: "42".to_string() } ], } } impl From for Image { #[inline] fn from(string: String) -> Image { let string = string.replace("\n", ""); let pixels: Vec<&str> = string.split("").collect(); // the above seems to add an extra "" at the start and end of the vec, so strip them below let pixels = &pixels[1..(pixels.len() - 1)]; let pixels: Vec = pixels.iter().map(|&pixel| { pixel.parse::().unwrap() }).collect(); Image { pixels } } } #[test] fn test_image_from_string() { let output = Image::from(include_str!("../test/resources/image").to_string()); let expected = Image { pixels: vec![ 1,1,1,1,1,1,1,1, 1,1,0,0,1,1,1,1, 1,0,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, ] }; assert_eq!(output, expected) } impl ToString for Image { #[inline] fn to_string(&self) -> String { let mut string = String::new(); let sqrt = (self.pixels.len() as f64).sqrt() as usize; // 8 for SD, 16 for HD for line in self.pixels.chunks(sqrt) { for pixel in line { string.push_str(&format!("{}", *pixel)); } string.push('\n'); } string.pop(); // remove trailing newline string } } #[test] fn test_image_to_string() { let output = example_image_chequers_1().to_string(); let expected = include_str!("../test/resources/image-chequers-1").to_string(); assert_eq!(output, expected); } pub trait AnimationFrames { fn to_string(&self) -> String; } impl AnimationFrames for Vec { #[inline] fn to_string(&self) -> String { let mut string = String::new(); let last_frame = self.len() - 1; for (i, frame) in self.into_iter().enumerate() { string.push_str(&frame.to_string()); if i < last_frame { string.push_str(&"\n>\n".to_string()); } } string } } impl From for Tile { fn from(string: String) -> Tile { let mut lines: Vec<&str> = string.lines().collect(); let id = lines[0].replace("TIL ", ""); let last_line = lines.pop().unwrap(); let wall = match last_line == "WAL true" { true => true, false => { lines.push(last_line); false } }; let last_line = lines.pop().unwrap(); let name = match last_line.starts_with("NAME") { true => Some(last_line.replace("NAME ", "").to_string()), false => { lines.push(last_line); None } }; let animation_frames = lines[1..].join(""); let animation_frames: Vec<&str> = animation_frames.split("\n>\n").collect(); let animation_frames: Vec = animation_frames.iter().map(|&frame| { Image::from(frame.to_string()) }).collect(); Tile { id, name, wall, animation_frames } } } #[test] fn test_tile_from_string() { let output = Tile::from(include_str!("../test/resources/tile").to_string()); let expected = Tile { id: "z".to_string(), name: Some("concrete 1".to_string()), wall: true, animation_frames: vec![ Image { pixels: vec![1; 64] } ], }; assert_eq!(output, expected); } impl ToString for Tile { #[inline] fn to_string(&self) -> String { format!( "TIL {}\n{}{}{}", self.id, self.animation_frames.to_string(), if self.name.as_ref().is_some() { format!("\nNAME {}", self.name.as_ref().unwrap())} else {"".to_string() }, if self.wall {"\nWAL true"} else {""} ) } } #[test] fn test_tile_to_string() { let output = Tile { id: "7a".to_string(), name: Some("chequers".to_string()), wall: false, animation_frames: vec![ example_image_chequers_1(), example_image_chequers_2(), ] }.to_string(); let expected = include_str!("../test/resources/tile-chequers").to_string(); assert_eq!(output, expected); } impl From for Position { fn from(string: String) -> Position { // e.g. "2,5" let xy: Vec<&str> = string.split(',').collect(); let x = xy[0].parse().unwrap(); let y = xy[1].parse().unwrap(); Position { x, y } } } #[test] fn test_position_from_string() { assert_eq!(Position::from("4,12".to_string()), Position { x: 4, y: 12 }); } impl ToString for Position { #[inline] fn to_string(&self) -> String { format!("{},{}", self.x, self.y) } } #[test] fn test_position_to_string() { assert_eq!(Position { x: 4, y: 12 }.to_string(), "4,12".to_string()) } impl From for Avatar { fn from(string: String) -> Avatar { let string = string.replace("SPR A\n", ""); let mut lines: Vec<&str> = string.lines().collect(); let room_pos = lines.pop().unwrap().replace("POS ", ""); let room_pos: Vec<&str> = room_pos.split_whitespace().collect(); let room = room_pos[0].to_string(); let position = Position::from(room_pos[1].to_string()); let animation_frames: String = lines.join("\n"); let animation_frames: Vec<&str> = animation_frames.split("\n>\n").collect(); let animation_frames: Vec = animation_frames.iter().map(|&frame| { Image::from(frame.to_string()) }).collect(); Avatar { animation_frames, room, position } } } #[test] fn test_avatar_from_string() { let output = Avatar::from( include_str!("../test/resources/avatar").to_string() ); let expected = example_avatar(); assert_eq!(output, expected); } impl ToString for Avatar { #[inline] fn to_string(&self) -> String { format!( "SPR A\n{}\nPOS {} {}", self.animation_frames.to_string(), self.room, self.position.to_string() ) } } #[test] fn test_avatar_to_string() { assert_eq!(example_avatar().to_string(), include_str!("../test/resources/avatar")); } impl From for Sprite { fn from(string: String) -> Sprite { let mut lines: Vec<&str> = string.lines().collect(); let id = lines[0].replace("SPR ", ""); let mut name = None; let mut dialogue = None; let mut room: Option = None; let mut position: Option = None; for _ in 0..3 { let last_line = lines.pop().unwrap(); if last_line.starts_with("NAME") { name = Some(last_line.replace("NAME ", "").to_string()); } else if last_line.starts_with("DLG") { dialogue = Some(last_line.replace("DLG ", "").to_string()); } else if last_line.starts_with("POS") { let last_line = last_line.replace("POS ", ""); let room_position: Vec<&str> = last_line.split(' ').collect(); room = Some(room_position[0].to_string()); position = Some(Position::from(room_position[1].to_string())); } else { lines.push(last_line); break; } } let room = room.unwrap(); let position = position.unwrap(); // todo dedupe let animation_frames = lines[1..].join(""); let animation_frames: Vec<&str> = animation_frames.split("\n>\n").collect(); let animation_frames: Vec = animation_frames.iter().map(|&frame| { Image::from(frame.to_string()) }).collect(); Sprite { id, name, animation_frames, dialogue, room, position } } } #[test] fn test_sprite_from_string() { let output = Sprite::from( include_str!("../test/resources/sprite").to_string() ); let expected = example_sprite(); assert_eq!(output, expected); } impl ToString for Sprite { #[inline] fn to_string(&self) -> String { format!( "SPR {}\n{}{}{}\nPOS {} {}", self.id, self.animation_frames.to_string(), if self.name.as_ref().is_some() { format!("\nNAME {}", self.name.as_ref().unwrap()) } else { "".to_string() }, if self.dialogue.as_ref().is_some() { format!("\nDLG {}", self.dialogue.as_ref().unwrap()) } else { "".to_string() }, self.room, self.position.to_string(), ) } } #[test] fn test_sprite_to_string() { assert_eq!(example_sprite().to_string(), include_str!("../test/resources/sprite").to_string()); } impl From for Item { fn from(string: String) -> Item { let mut lines: Vec<&str> = string.lines().collect(); let id = lines[0].replace("ITM ", ""); let mut name = None; let mut dialogue = None; for _ in 0..2 { let last_line = lines.pop().unwrap(); if last_line.starts_with("NAME") { name = Some(last_line.replace("NAME ", "").to_string()); } else if last_line.starts_with("DLG") { dialogue = Some(last_line.replace("DLG ", "").to_string()); } else { lines.push(last_line); break; } } // todo dedupe let animation_frames = lines[1..].join(""); let animation_frames: Vec<&str> = animation_frames.split("\n>\n").collect(); let animation_frames: Vec = animation_frames.iter().map(|&frame| { Image::from(frame.to_string()) }).collect(); Item { id, name, animation_frames, dialogue } } } #[test] fn test_item_from_string() { let output = Item::from(include_str!("../test/resources/item").to_string()); let expected = example_item(); assert_eq!(output, expected); } impl ToString for Item { #[inline] fn to_string(&self) -> String { format!( "ITM {}\n{}{}{}", self.id, self.animation_frames.to_string(), if self.name.is_some() { format!("\nNAME {}", self.name.as_ref().unwrap()) } else { "".to_string() }, if self.dialogue.is_some() { format!("\nDLG {}", self.dialogue.as_ref().unwrap()) } else { "".to_string() }, ) } } #[test] fn test_item_to_string() { let output = example_item().to_string(); let expected = include_str!("../test/resources/item").to_string(); assert_eq!(output, expected); } impl From for Exit { fn from(string: String) -> Exit { // e.g. "4 3,3" let room_position: Vec<&str> = string.split(' ').collect(); let room = room_position[0].to_string(); let position = Position::from(room_position[1].to_string()); Exit { room, position } } } #[test] fn test_exit_from_string() { assert_eq!( Exit::from("a 12,13".to_string()), Exit { room: "a".to_string(), position: Position { x: 12, y: 13 } } ); } impl ToString for Exit { fn to_string(&self) -> String { format!("{} {}", self.room, self.position.to_string()) } } #[test] fn test_exit_to_string() { assert_eq!( Exit { room: "8".to_string(), position: Position { x: 5, y: 6 } }.to_string(), "8 5,6".to_string() ); } impl From for Ending { fn from(string: String) -> Ending { let string = string.replace("END ", ""); let id_dialogue: Vec<&str> = string.lines().collect(); let id = id_dialogue[0].to_string(); let dialogue = id_dialogue[1].to_string(); Ending { id, dialogue } } } #[test] fn test_ending_from_string() { assert_eq!( Ending::from(include_str!("../test/resources/ending").to_string()), Ending { id: "a".to_string(), dialogue: "This is a long line of dialogue. Blah blah blah".to_string() } ); } impl ToString for Ending { #[inline] fn to_string(&self) -> String { format!("END {}\n{}", self.id, self.dialogue) } } #[test] fn test_ending_to_string() { assert_eq!( Ending { id: "7".to_string(), dialogue: "This is another long ending. So long, farewell, etc.".to_string() }.to_string(), "END 7\nThis is another long ending. So long, farewell, etc.".to_string() ); } impl From for Dialogue { fn from(string: String) -> Dialogue { let lines: Vec<&str> = string.lines().collect(); let id = lines[0].replace("DLG ", "").to_string(); let contents = lines[1..].join("\n"); Dialogue { id, contents } } } #[test] fn test_dialogue_from_string() { assert_eq!( Dialogue::from("DLG h\nhello\ngoodbye".to_string()), Dialogue { id: "h".to_string(), contents: "hello\ngoodbye".to_string()} ) } impl ToString for Dialogue { #[inline] fn to_string(&self) -> String { format!("DLG {}\n{}", self.id, self.contents) } } #[test] fn test_dialogue_to_string() { assert_eq!( Dialogue { id: "y".to_string(), contents: "This is a bit of dialogue,\nblah blah\nblah blah".to_string() }.to_string(), "DLG y\nThis is a bit of dialogue,\nblah blah\nblah blah".to_string() ); } impl From for Variable { fn from(string: String) -> Variable { let id_value: Vec<&str> = string.split('\n').collect(); let id = id_value[0].replace("VAR ", "").to_string(); let initial_value = id_value[1].to_string(); Variable { id, initial_value } } } #[test] fn test_variable_from_string() { assert_eq!( Variable::from("VAR a\n42".to_string()), Variable { id: "a".to_string(), initial_value: "42".to_string()} ); } impl ToString for Variable { #[inline] fn to_string(&self) -> String { format!("VAR {}\n{}", self.id, self.initial_value) } } #[test] fn test_variable_to_string() { let output = Variable { id: "c".to_string(), initial_value: "57".to_string() }.to_string(); let expected = "VAR c\n57".to_string(); assert_eq!(output, expected); } impl From for Room { fn from(string: String) -> Room { // todo handle room_format? let mut lines: Vec<&str> = string.lines().collect(); let id = lines[0].replace("ROOM ", ""); let mut name = None; let mut palette = "0".to_string(); let mut items: Vec = Vec::new(); let mut exits: Vec = Vec::new(); let mut endings: Vec = Vec::new(); loop { let last_line = lines.pop().unwrap(); if last_line.starts_with("NAME") { name = Some(last_line.replace("NAME ", "").to_string()); } else if last_line.starts_with("PAL") { palette = last_line.replace("PAL ", "").to_string(); } else if last_line.starts_with("ITM") { let last_line = last_line.replace("ITM ", ""); let item_position: Vec<&str> = last_line.split(' ').collect(); let item_id = item_position[0]; let position = item_position[1]; let position = Position::from(position.to_string()); items.push(Instance { position, id: item_id.to_string() }); } else if last_line.starts_with("EXT") { let last_line = last_line.replace("EXT ", ""); let parts: Vec<&str> = last_line.split(' ').collect(); let position = Position::from(parts[0].to_string()); let exit = Exit::from(format!("{} {}", parts[1], parts[2])); exits.push(ExitInstance { position, exit }); } else if last_line.starts_with("END") { let last_line = last_line.replace("END ", ""); let ending_position: Vec<&str> = last_line.split(' ').collect(); let ending = ending_position[0].to_string(); let position = ending_position[1].to_string(); let position = Position::from(position); endings.push(Instance { position, id: ending }); } else { lines.push(last_line); break; } } let lines = &lines[1..]; let mut tiles: Vec = Vec::new(); for line in lines.into_iter() { let line: Vec<&str> = line.split(",").collect(); for tile_id in line { tiles.push(tile_id.to_string()); } } Room { id, palette, name, tiles, items, exits, endings } } } #[test] fn test_room_from_string() { assert_eq!(Room::from(example_room_string()), example_room()); } impl ToString for Room { fn to_string(&self) -> String { let mut tiles = String::new(); let mut items = String::new(); let mut exits = String::new(); let mut endings = String::new(); let sqrt = (self.tiles.len() as f64).sqrt() as usize; // 8 for SD, 16 for HD for line in self.tiles.chunks(sqrt) { for tile in line { tiles.push_str(&format!("{},", tile)); } tiles.pop(); // remove trailing comma tiles.push_str("\n"); } tiles.pop(); // remove trailing newline for instance in &self.items { items.push_str( &format!("\nITM {} {}", instance.id, instance.position.to_string()) ); } for instance in &self.exits { exits.push_str( &format!( "\nEXT {} {}", instance.position.to_string(), instance.exit.to_string(), ) ); } for instance in &self.endings { endings.push_str( &format!("\nEND {} {}", instance.id, instance.position.to_string()) ); } format!( "ROOM {}\n{}{}{}{}{}\nPAL {}", self.id, tiles, if self.name.as_ref().is_some() { format!("\nNAME {}", self.name.as_ref().unwrap()) } else { "".to_string() }, items, exits, endings, self.palette ) } } #[test] fn test_room_to_string() { assert_eq!(example_room().to_string(), example_room_string()); } impl From for Game { fn from(string: String) -> Game { // dialogues and endings can have 2+ line breaks inside, so deal with these separately // otherwise, everything can be split on a double line break (\n\n) let mut dialogues: Vec = Vec::new(); let mut endings: Vec = Vec::new(); let mut variables: Vec = Vec::new(); let main_split: Vec<&str> = string.split("\n\nDLG").collect(); let main = main_split[0].to_string(); let mut dialogues_endings_variables: String = main_split[1..].join("\n\nDLG"); let variable_segments = dialogues_endings_variables.clone(); let variable_segments: Vec<&str> = variable_segments.split("\n\nVAR").collect(); if variable_segments.len() > 0 { dialogues_endings_variables = variable_segments[0].to_string(); let variable_segments = variable_segments[1..].to_owned(); for segment in variable_segments { let segment = format!("VAR{}", segment); variables.push(Variable::from(segment)); } } let ending_segments = dialogues_endings_variables.clone(); let ending_segments: Vec<&str> = ending_segments.split("\n\nEND").collect(); if ending_segments.len() > 0 { dialogues_endings_variables = ending_segments[0].to_string(); let ending_segments = ending_segments[1..].to_owned(); for segment in ending_segments { let segment = format!("END{}", segment); endings.push(Ending::from(segment)); } } let dialogue_segments = format!("\n\nDLG {}", dialogues_endings_variables.trim()); let dialogue_segments: Vec<&str> = dialogue_segments.split("\n\nDLG").collect(); for segment in dialogue_segments[1..].to_owned() { let segment = format!("DLG{}", segment); dialogues.push(Dialogue::from(segment)); } let segments: Vec<&str> = main.split("\n\n").collect(); let name = segments[0].to_string(); let mut version: f64 = 1.0; let mut room_format: u8 = 1; let mut palettes: Vec = Vec::new(); let mut rooms: Vec = Vec::new(); let mut tiles: Vec = Vec::new(); let mut avatar: Option = None; // unwrap this later let mut sprites: Vec = Vec::new(); let mut items: Vec = Vec::new(); for segment in segments[1..].to_owned() { let segment = segment.to_string(); if segment.starts_with("# BITSY VERSION") { version = segment.replace("# BITSY VERSION ", "").parse().unwrap(); } else if segment.starts_with("! ROOM_FORMAT") { room_format = segment.replace("! ROOM_FORMAT ", "").parse().unwrap(); } else if segment.starts_with("PAL") { palettes.push(Palette::from(segment)); } else if segment.starts_with("ROOM") { rooms.push(Room::from(segment)); } else if segment.starts_with("TIL") { tiles.push(Tile::from(segment)); } else if segment.starts_with("SPR A") { avatar = Some(Avatar::from(segment)); } else if segment.starts_with("SPR") { sprites.push(Sprite::from(segment)); } else if segment.starts_with("ITM") { items.push(Item::from(segment)); } } assert!(avatar.is_some()); let avatar = avatar.unwrap(); Game { name, version, room_format, palettes, rooms, tiles, avatar, sprites, items, dialogues, endings, variables, } } } #[test] fn test_game_from_string() { let output = Game::from( include_str!["../test/resources/default.bitsy"].to_string() ); let expected = example_game_default(); assert_eq!(output, expected); } impl ToString for Game { #[inline] fn to_string(&self) -> String { let mut segments: Vec = Vec::new(); // todo refactor for palette in &self.palettes { segments.push(palette.to_string()); } for room in &self.rooms { segments.push(room.to_string()); } for tile in &self.tiles { segments.push(tile.to_string()); } segments.push(self.avatar.to_string()); for sprite in &self.sprites { segments.push(sprite.to_string()); } for item in &self.items { segments.push(item.to_string()); } for dialogue in &self.dialogues { segments.push(dialogue.to_string()); } for ending in &self.endings { segments.push(ending.to_string()); } for variable in &self.variables { segments.push(variable.to_string()); } format!( "{}\n\n# BITSY VERSION {}\n\n! ROOM_FORMAT {}\n\n{}\n\n", &self.name, &self.version, &self.room_format, segments.join("\n\n"), ) } } #[test] fn test_game_to_string() { let output = example_game_default().to_string(); let expected = include_str!["../test/resources/default.bitsy"].to_string(); assert_eq!(output, expected); }