├── .gitignore ├── examples ├── resources │ ├── characters.toml │ └── script.txt └── main.rs ├── README.md ├── Cargo.toml ├── LICENSE └── src ├── interface.rs ├── parser.rs ├── lexer.rs ├── character.rs ├── game.rs ├── lib.rs └── animation.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | .idea 4 | .vscode 5 | examples/resources 6 | !examples/resources/*.txt 7 | !examples/resources/*.toml 8 | -------------------------------------------------------------------------------- /examples/resources/characters.toml: -------------------------------------------------------------------------------- 1 | [Character.Happy] 2 | image = "/character-happy.png" 3 | scale = [0.5, 0.5] 4 | 5 | [Character.Sad] 6 | image = "/character-sad.png" 7 | scale = [0.5, 0.5] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kanna 2 | A visual novel engine written in Rust. 3 | 4 | ## Key Bindings 5 | 6 | ### Developer Mode 7 | These key bindings only work if developer mode is enabled. 8 | - `Ctrl + R` - Saves and reloads the game and then reloads the save -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanna" 3 | version = "0.1.0" 4 | authors = [ 5 | "Hiruna K. Jayamanne ", 6 | "Technocoder <8334328+Techno-coder@users.noreply.github.com>", 7 | ] 8 | edition = "2018" 9 | 10 | [dependencies] 11 | ggez = "^0.5" 12 | toml = "^0.5" 13 | 14 | [dependencies.serde] 15 | version = "^1.0" 16 | features = ["derive"] 17 | -------------------------------------------------------------------------------- /examples/main.rs: -------------------------------------------------------------------------------- 1 | use kanna::*; 2 | 3 | pub fn main() -> ggez::GameResult { 4 | let mut settings = Settings::default(); 5 | settings.resource_paths.push(env!("CARGO_MANIFEST_DIR").to_owned() + "/examples/resources"); 6 | 7 | kanna::game::run(settings, |ctx, settings| { 8 | let mut script = kanna::game::load_script(ctx, "/script.txt")?; 9 | script.characters = kanna::game::load_characters(ctx, "/characters.toml")?; 10 | let history = kanna::game::load_history(ctx, settings) 11 | .unwrap_or_else(|_| History::default()); 12 | kanna::game::load_resources(ctx, &mut script)?; 13 | Ok((script, history)) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /examples/resources/script.txt: -------------------------------------------------------------------------------- 1 | "John Wick" "John Wick needs your credit card number and the three digits on the back so he can win this epic victory and take home the bread." 2 | spawn "Character" "Happy" (320, 240) with glide[] 3 | pause 4 | position "Character" (540, 240) with glide[] 5 | pause 6 | hide "Character" with fade[] 7 | pause 8 | show "Character" with glide[] 9 | pause 10 | change "Character" "Sad" with flip[] 11 | pause 12 | kill "Character" with fade[] 13 | pause 14 | "Bruh Moment" "Hi, this is a bruh moment." 15 | stage "/background.jpg" 16 | 17 | diverge 18 | "Sigh" bruh-moment-sigh 19 | "Rest" bruh-moment-rest 20 | 21 | label bruh-moment-sigh 22 | "Don't sigh me!" 23 | flag sighed 24 | jump bruh-moment-end 25 | 26 | label bruh-moment-rest 27 | "Bruh moments are indeed for resting." 28 | jump bruh-moment-end 29 | 30 | label bruh-moment-end 31 | music "/music.ogg" 32 | "The weather sure is nice today." 33 | 34 | if sighed bad-sigh 35 | jump other 36 | 37 | label bad-sigh 38 | "But it isn't perfect because SOMEONE had to sigh." 39 | 40 | label other 41 | "Lots of clouds and stuff." 42 | "They look like cotton candy." 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hiruna Jayamanne and Technocoder (Techno-coder) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut, Range}; 2 | 3 | use ggez::graphics::{self, Image}; 4 | 5 | use crate::character::Stage; 6 | use crate::Label; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct Render { 10 | pub background: Option, 11 | pub stage: Stage, 12 | pub character: Option, 13 | pub text: Option, 14 | pub branches: Vec<(Button, Label)>, 15 | pub shadow_bars: [graphics::Rect; 2], 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct RenderText { 20 | pub string: String, 21 | pub slice: Range, 22 | pub colour: [f32; 4], 23 | } 24 | 25 | impl RenderText { 26 | /// Creates a `RenderText` with all characters initially displayed. 27 | pub fn new(string: String, colour: [f32; 4]) -> Self { 28 | let slice = Range { start: 0, end: string.len() }; 29 | RenderText { string, slice, colour } 30 | } 31 | 32 | /// Creates a `RenderText` with no characters initially displayed. 33 | pub fn empty(string: String, colour: [f32; 4]) -> Self { 34 | let slice = Range { start: 0, end: 0 }; 35 | RenderText { string, slice, colour } 36 | } 37 | 38 | /// Adds an additional character to be rendered. 39 | /// Does nothing if the end of the string is already rendered. 40 | pub fn step(&mut self) { 41 | self.string[self.slice.end..].char_indices().skip(1) 42 | .next().map(|(index, _)| self.slice.end += index) 43 | .unwrap_or_else(|| self.finish()); 44 | } 45 | 46 | /// Adds all remaining characters to be rendered. 47 | pub fn finish(&mut self) { 48 | self.slice.end = self.string.len(); 49 | } 50 | 51 | /// Checks whether all the characters have been rendered. 52 | pub fn is_finished(&self) -> bool { 53 | self.slice.end == self.string.len() 54 | } 55 | 56 | pub fn fragment(&self) -> graphics::TextFragment { 57 | let string = self.string[self.slice.clone()].to_owned(); 58 | graphics::TextFragment::new(string).color(self.colour.into()) 59 | } 60 | } 61 | 62 | #[derive(Debug)] 63 | pub struct TextBox { 64 | pub text: RenderText, 65 | pub position: (f32, f32), 66 | pub size: (f32, f32), 67 | pub colour: [f32; 4], 68 | pub padding: f32, 69 | pub alignment: graphics::Align, 70 | } 71 | 72 | impl TextBox { 73 | pub fn new(text: RenderText, position: (f32, f32), size: (f32, f32), colour: [f32; 4]) -> Self { 74 | TextBox { text, position, size, colour, padding: 0.0, alignment: graphics::Align::Left } 75 | } 76 | 77 | pub fn padding(mut self, padding: f32) -> Self { 78 | self.padding = padding; 79 | self 80 | } 81 | 82 | pub fn alignment(mut self, alignment: graphics::Align) -> Self { 83 | self.alignment = alignment; 84 | self 85 | } 86 | 87 | pub fn draw(&self, ctx: &mut ggez::Context) -> ggez::GameResult { 88 | let rectangle = self.rectangle(); 89 | let fragment = self.text.fragment(); 90 | let text_box = graphics::Mesh::new_rectangle(ctx, 91 | graphics::DrawMode::fill(), rectangle, self.colour.into())?; 92 | graphics::draw(ctx, &text_box, graphics::DrawParam::new())?; 93 | 94 | let bounds = [rectangle.w - 2.0 * self.padding, rectangle.h - 2.0 * self.padding]; 95 | let text_position = ([rectangle.x + self.padding, rectangle.y + self.padding], ); 96 | graphics::draw(ctx, graphics::Text::new(fragment) 97 | .set_bounds(bounds, self.alignment), text_position) 98 | } 99 | 100 | pub fn rectangle(&self) -> graphics::Rect { 101 | let (x, y) = self.position; 102 | let (width, height) = self.size; 103 | [x, y, width, height].into() 104 | } 105 | } 106 | 107 | impl Deref for TextBox { 108 | type Target = RenderText; 109 | 110 | fn deref(&self) -> &Self::Target { 111 | &self.text 112 | } 113 | } 114 | 115 | impl DerefMut for TextBox { 116 | fn deref_mut(&mut self) -> &mut Self::Target { 117 | &mut self.text 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | pub struct Button { 123 | pub text: TextBox, 124 | pub default: [f32; 4], 125 | pub hover: [f32; 4], 126 | } 127 | 128 | impl Button { 129 | pub fn new(text: TextBox, default: [f32; 4], hover: [f32; 4]) -> Self { 130 | Button { text, default, hover } 131 | } 132 | 133 | pub fn update(&mut self, (x, y): (f32, f32)) { 134 | match self.text.rectangle().contains([x, y]) { 135 | false => self.text.colour = self.default, 136 | true => self.text.colour = self.hover, 137 | } 138 | } 139 | } 140 | 141 | impl Deref for Button { 142 | type Target = TextBox; 143 | 144 | fn deref(&self) -> &Self::Target { 145 | &self.text 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::{Command, FlagName, Label, lexer::Lexer, Script, Target}; 2 | use crate::animation::AnimationDeclaration; 3 | use crate::character::{CharacterName, InstanceName, StateName}; 4 | 5 | #[derive(Debug, PartialEq)] 6 | pub enum Token { 7 | Identifier(String), 8 | String(String), 9 | Numeric(f32), 10 | ScopeOpen, 11 | ScopeClose, 12 | BracketOpen, 13 | BracketClose, 14 | SquareOpen, 15 | SquareClose, 16 | ListSeparator, 17 | Underscore, 18 | Terminator, 19 | } 20 | 21 | #[derive(Debug, PartialEq)] 22 | pub enum ParserError { 23 | UnmatchedQuote, 24 | ExpectedIdentifier, 25 | ExpectedString, 26 | ExpectedNumeric, 27 | Expected(Token), 28 | UnexpectedToken, 29 | InvalidCommand, 30 | InvalidNumeric, 31 | } 32 | 33 | pub fn parse(string: &str) -> Result> { 34 | let mut errors = Vec::new(); 35 | let mut script = Script::default(); 36 | let lexer = &mut Lexer::new(string); 37 | 38 | loop { 39 | match parse_command(lexer, &mut script) { 40 | Ok(false) => (), 41 | Ok(true) => break, 42 | Err((error, target)) => { 43 | lexer.skip_take(target); 44 | errors.push(error); 45 | } 46 | } 47 | } 48 | 49 | match errors.is_empty() { 50 | false => Err(errors), 51 | true => Ok(script) 52 | } 53 | } 54 | 55 | pub fn parse_command(lexer: &mut Lexer, script: &mut Script) -> Result { 56 | let initial = lexer.token().map_err(|error| 57 | (error, Token::Terminator))?; 58 | let initial = match initial { 59 | Some(token) => token, 60 | None => return Ok(true), 61 | }; 62 | 63 | match initial { 64 | Token::Terminator => (), 65 | Token::Identifier(identifier) => match identifier.as_str() { 66 | "change" => { 67 | let instance = InstanceName(inline(lexer.string())?); 68 | let state = StateName(inline(lexer.string())?); 69 | let animation = animation(lexer)?; 70 | script.commands.push(Command::Change(instance, state, animation)); 71 | } 72 | "diverge" => { 73 | inline(lexer.expect(Token::Terminator))?; 74 | inline(lexer.expect(Token::ScopeOpen))?; 75 | parse_diverge(lexer, script).map_err(|error| (error, Token::ScopeClose))?; 76 | } 77 | "label" => { 78 | let label = Label(inline(lexer.identifier())?); 79 | script.labels.insert(label, Target(script.commands.len())); 80 | } 81 | "position" => { 82 | let instance = InstanceName(inline(lexer.string())?); 83 | let position = position(lexer)?; 84 | let animation = animation(lexer)?; 85 | script.commands.push(Command::Position(instance, position, animation)); 86 | } 87 | "spawn" => { 88 | let character = CharacterName(inline(lexer.string())?); 89 | let state = StateName(inline(lexer.string())?); 90 | let position = position(lexer)?; 91 | let instance_name = match inline(lexer.peek())? { 92 | Some(Token::String(_)) => Some(InstanceName(lexer.string().unwrap())), 93 | _ => None, 94 | }; 95 | 96 | let animation = animation(lexer)?; 97 | script.commands.push(Command::Spawn(character, state, position, instance_name, animation)); 98 | } 99 | "if" => { 100 | let flag = FlagName(inline(lexer.identifier())?); 101 | script.commands.push(Command::If(flag, Label(inline(lexer.identifier())?))); 102 | } 103 | "pause" => script.commands.push(Command::Pause), 104 | "flag" => script.commands.push(Command::Flag(FlagName(inline(lexer.identifier())?))), 105 | "unflag" => script.commands.push(Command::Unflag(FlagName(inline(lexer.identifier())?))), 106 | "kill" => script.commands.push(Command::Kill(InstanceName(inline(lexer.string())?), animation(lexer)?)), 107 | "show" => script.commands.push(Command::Show(InstanceName(inline(lexer.string())?), animation(lexer)?)), 108 | "hide" => script.commands.push(Command::Hide(InstanceName(inline(lexer.string())?), animation(lexer)?)), 109 | "stage" => script.commands.push(Command::Stage(inline(lexer.string())?.into())), 110 | "jump" => script.commands.push(Command::Jump(Label(inline(lexer.identifier())?))), 111 | "music" => script.commands.push(Command::Music(inline(lexer.string())?.into())), 112 | "sound" => script.commands.push(Command::Sound(inline(lexer.string())?.into())), 113 | _ => return Err((ParserError::InvalidCommand, Token::Terminator)), 114 | } 115 | Token::String(string) => match lexer.token().map_err(|error| (error, Token::Terminator))? { 116 | Some(Token::Terminator) => 117 | script.commands.push(Command::Dialogue(None, string)), 118 | Some(Token::String(dialogue)) => { 119 | let character = Some(CharacterName(string)); 120 | script.commands.push(Command::Dialogue(character, dialogue)); 121 | inline(lexer.expect(Token::Terminator))?; 122 | } 123 | _ => return Err((ParserError::Expected(Token::Terminator), Token::Terminator)), 124 | }, 125 | Token::ScopeOpen => return Err((ParserError::UnexpectedToken, Token::ScopeClose)), 126 | _ => return Err((ParserError::UnexpectedToken, Token::Terminator)), 127 | }; 128 | Ok(false) 129 | } 130 | 131 | pub fn inline(result: Result) -> Result { 132 | result.map_err(|error| (error, Token::Terminator)) 133 | } 134 | 135 | pub fn animation(lexer: &mut Lexer) -> Result, (ParserError, Token)> { 136 | match inline(lexer.token())? { 137 | None | Some(Token::Terminator) => return Ok(None), 138 | Some(Token::Identifier(identifier)) if identifier == "with" => (), 139 | Some(_) => return Err((ParserError::UnexpectedToken, Token::Terminator)), 140 | } 141 | 142 | let name = inline(lexer.identifier())?; 143 | inline(lexer.expect(Token::SquareOpen))?; 144 | let mut arguments = Vec::new(); 145 | while let Some(token) = inline(lexer.token())? { 146 | if token == Token::SquareClose { break; } 147 | if !arguments.is_empty() { 148 | inline(lexer.expect(Token::ListSeparator))?; 149 | } 150 | 151 | arguments.push(match token { 152 | Token::Underscore => None, 153 | Token::Numeric(number) => Some(number), 154 | _ => return Err((ParserError::UnexpectedToken, Token::Terminator)), 155 | }); 156 | } 157 | Ok(Some(AnimationDeclaration { name, arguments })) 158 | } 159 | 160 | pub fn position(lexer: &mut Lexer) -> Result<(f32, f32), (ParserError, Token)> { 161 | inline(lexer.expect(Token::BracketOpen))?; 162 | let position_x = inline(lexer.numeric())?; 163 | inline(lexer.expect(Token::ListSeparator))?; 164 | let position_y = inline(lexer.numeric())?; 165 | inline(lexer.expect(Token::BracketClose))?; 166 | Ok((position_x, position_y)) 167 | } 168 | 169 | pub fn parse_diverge(lexer: &mut Lexer, script: &mut Script) -> Result<(), ParserError> { 170 | let mut branches = Vec::new(); 171 | loop { 172 | match lexer.token() { 173 | Ok(Some(Token::ScopeClose)) => { 174 | script.commands.push(Command::Diverge(branches)); 175 | return Ok(()); 176 | } 177 | Ok(Some(Token::String(string))) => { 178 | let identifier = lexer.identifier()?; 179 | branches.push((string, Label(identifier))); 180 | lexer.expect(Token::Terminator)?; 181 | } 182 | Ok(Some(Token::Terminator)) => (), 183 | _ => return Err(ParserError::ExpectedString), 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/lexer.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::iter::Peekable; 3 | use std::str::CharIndices; 4 | 5 | use crate::parser::{ParserError, Token}; 6 | 7 | #[derive(Debug)] 8 | pub struct Lexer<'a> { 9 | string: &'a str, 10 | characters: Peekable>, 11 | indentation: usize, 12 | target_indent: usize, 13 | new_line: bool, 14 | peek: Option, 15 | } 16 | 17 | impl<'a> Lexer<'a> { 18 | pub fn new(string: &'a str) -> Self { 19 | let characters = string.char_indices().peekable(); 20 | Lexer { string, characters, indentation: 0, target_indent: 0, new_line: true, peek: None } 21 | } 22 | 23 | pub fn token(&mut self) -> Result, ParserError> { 24 | match self.peek.take() { 25 | Some(token) => Ok(Some(token)), 26 | None => self.next().transpose(), 27 | } 28 | } 29 | 30 | pub fn peek(&mut self) -> Result, ParserError> { 31 | self.peek = self.token()?; 32 | Ok(self.peek.as_ref()) 33 | } 34 | 35 | pub fn identifier(&mut self) -> Result { 36 | match self.token()? { 37 | Some(Token::Identifier(identifier)) => Ok(identifier), 38 | _ => Err(ParserError::ExpectedIdentifier), 39 | } 40 | } 41 | 42 | pub fn string(&mut self) -> Result { 43 | match self.token()? { 44 | Some(Token::String(string)) => Ok(string), 45 | _ => Err(ParserError::ExpectedString), 46 | } 47 | } 48 | 49 | pub fn numeric(&mut self) -> Result { 50 | match self.token()? { 51 | Some(Token::Numeric(numeric)) => Ok(numeric), 52 | _ => Err(ParserError::ExpectedNumeric), 53 | } 54 | } 55 | 56 | pub fn expect(&mut self, token: Token) -> Result<(), ParserError> { 57 | match self.token()?.as_ref() == Some(&token) { 58 | false => Err(ParserError::Expected(token)), 59 | true => Ok(()) 60 | } 61 | } 62 | 63 | /// Skips all tokens until the target token is consumed. 64 | pub fn skip_take(&mut self, target: Token) { 65 | let target = Ok(target); 66 | while let Some(token) = self.next() { 67 | if token == target { break; } 68 | } 69 | } 70 | } 71 | 72 | impl<'a> Iterator for Lexer<'a> { 73 | type Item = Result; 74 | 75 | fn next(&mut self) -> Option { 76 | match usize::cmp(&self.indentation, &self.target_indent) { 77 | Ordering::Less => { 78 | self.indentation += 1; 79 | return Some(Ok(Token::ScopeOpen)); 80 | } 81 | Ordering::Greater => { 82 | self.indentation -= 1; 83 | return Some(Ok(Token::ScopeClose)); 84 | } 85 | Ordering::Equal => (), 86 | } 87 | 88 | if self.new_line { 89 | self.new_line = false; 90 | let mut target_indent = 0; 91 | while let Some((_, '\t')) = self.characters.peek() { 92 | self.characters.next(); 93 | target_indent += 1; 94 | } 95 | 96 | match self.characters.peek() { 97 | None | Some((_, '\n')) => (), 98 | _ => self.target_indent = target_indent, 99 | } 100 | return self.next(); 101 | } 102 | 103 | let (start, character) = match self.characters.next() { 104 | Some((start, character)) => (start, character), 105 | None if self.target_indent == 0 => return None, 106 | None => { 107 | self.target_indent = 0; 108 | return self.next(); 109 | } 110 | }; 111 | 112 | Some(Ok(match character { 113 | '(' => Token::BracketOpen, 114 | ')' => Token::BracketClose, 115 | ',' => Token::ListSeparator, 116 | '[' => Token::SquareOpen, 117 | ']' => Token::SquareClose, 118 | '_' => Token::Underscore, 119 | '\n' => { 120 | self.new_line = true; 121 | Token::Terminator 122 | } 123 | '"' => loop { 124 | let character = self.characters.peek(); 125 | match character { 126 | Some((_, '"')) => { 127 | let (index, _) = self.characters.next().unwrap(); 128 | let string = self.string[start + 1..index].to_owned(); 129 | break Token::String(escape(string)); 130 | } 131 | Some((_, '\\')) => { 132 | self.characters.next(); 133 | self.characters.next() 134 | } 135 | None | Some((_, '\n')) => 136 | return Some(Err(ParserError::UnmatchedQuote)), 137 | Some(_) => self.characters.next(), 138 | }; 139 | }, 140 | _ => match character.is_whitespace() { 141 | true => return self.next(), 142 | false => { 143 | while let Some((_, character)) = self.characters.peek() { 144 | let is_punctuation = !['-', '.'].contains(character) 145 | && character.is_ascii_punctuation(); 146 | match character.is_whitespace() || is_punctuation { 147 | false => self.characters.next(), 148 | true => break, 149 | }; 150 | } 151 | 152 | let end = self.characters.peek().map(|(index, _)| *index); 153 | let string = &self.string[start..end.unwrap_or(self.string.len())]; 154 | match character == '-' || character.is_digit(10) { 155 | false => Token::Identifier(string.to_owned()), 156 | true => match string.parse() { 157 | Ok(numeric) => Token::Numeric(numeric), 158 | Err(_) => return Some(Err(ParserError::InvalidNumeric)), 159 | } 160 | } 161 | } 162 | } 163 | })) 164 | } 165 | } 166 | 167 | pub fn escape(string: String) -> String { 168 | string.replace("\\n", "\n").replace("\\\"", "\"") 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | 175 | #[test] 176 | fn lexer_string() { 177 | assert_eq!(Lexer::new("string").next(), Some(Ok(Token::Identifier("string".to_owned())))); 178 | assert_eq!(Lexer::new("\"string\"").next(), Some(Ok(Token::String("string".to_owned())))); 179 | assert_eq!(Lexer::new("\"string\\n\"").next(), Some(Ok(Token::String("string\n".to_owned())))); 180 | assert_eq!(Lexer::new("\"\\\"\"").next(), Some(Ok(Token::String("\"".to_owned())))); 181 | assert_eq!(Lexer::new("\"string").next(), Some(Err(ParserError::UnmatchedQuote))); 182 | } 183 | 184 | #[test] 185 | fn lexer_scope() { 186 | assert_eq!(Lexer::new("\t").next(), None); 187 | assert_eq!(Lexer::new("\t\n").next(), Some(Ok(Token::Terminator))); 188 | assert_eq!(&Lexer::new("\tstring").collect::>(), &[Ok(Token::ScopeOpen), 189 | Ok(Token::Identifier("string".to_owned())), Ok(Token::ScopeClose)]); 190 | assert_eq!(&Lexer::new("diverge\n\t\"string\"").collect::>(), 191 | &[Ok(Token::Identifier("diverge".to_owned())), Ok(Token::Terminator), 192 | Ok(Token::ScopeOpen), Ok(Token::String("string".to_owned())), Ok(Token::ScopeClose)]); 193 | } 194 | 195 | #[test] 196 | fn lexer_numeric() { 197 | assert_eq!(Lexer::new("0").next(), Some(Ok(Token::Numeric(0.0)))); 198 | assert_eq!(Lexer::new("1").next(), Some(Ok(Token::Numeric(1.0)))); 199 | assert_eq!(Lexer::new("1.0").next(), Some(Ok(Token::Numeric(1.0)))); 200 | assert_eq!(Lexer::new("-1.0").next(), Some(Ok(Token::Numeric(-1.0)))); 201 | assert_eq!(&Lexer::new("(1.0,)").collect::>(), &[Ok(Token::BracketOpen), 202 | Ok(Token::Numeric(1.0)), Ok(Token::ListSeparator), Ok(Token::BracketClose)]); 203 | } 204 | 205 | #[test] 206 | fn lexer_peek() { 207 | let mut lexer = Lexer::new("with \"string\""); 208 | assert_eq!(lexer.peek(), Ok(Some(&Token::Identifier(String::from("with"))))); 209 | assert_eq!(lexer.identifier(), Ok(String::from("with"))); 210 | assert_eq!(lexer.string(), Ok(String::from("string"))); 211 | assert_eq!(lexer.token(), Ok(None)); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/character.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::{Index, IndexMut}; 3 | use std::path::PathBuf; 4 | 5 | use ggez::graphics; 6 | use serde::Deserialize; 7 | 8 | use crate::{animation::{Animation, AnimationState, InstanceParameter}, Script}; 9 | 10 | #[derive(Debug, Deserialize, Clone, Hash, Eq, PartialEq)] 11 | #[serde(transparent)] 12 | pub struct CharacterName(pub String); 13 | 14 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 15 | pub struct InstanceName(pub String); 16 | 17 | #[derive(Debug, Deserialize, Hash, Eq, PartialEq)] 18 | #[serde(transparent)] 19 | pub struct StateName(pub String); 20 | 21 | /// A state represents a possible character image. 22 | #[derive(Debug, Deserialize, Clone)] 23 | pub struct CharacterState { 24 | /// Path to the image. 25 | pub image: PathBuf, 26 | /// Centre of the image in pixels. 27 | /// This is used when the image sets its position, is scaled, or is rotated. 28 | /// If no position is specified then the pixel centre of the image is used. 29 | pub centre_position: Option<(u16, u16)>, 30 | /// Amount this image is to be scaled by. 31 | /// Default is `(1.0, 1.0)` (normal size). 32 | pub scale: (f32, f32), 33 | } 34 | 35 | impl CharacterState { 36 | /// Creates a new state from the path to the image. 37 | pub fn new>(path: P) -> Self { 38 | Self { 39 | image: path.into(), 40 | centre_position: None, 41 | scale: (1.0, 1.0), 42 | } 43 | } 44 | 45 | /// Sets the centre of the image in pixels. 46 | pub fn centre_position(mut self, (x, y): (u16, u16)) -> Self { 47 | self.centre_position = Some((x, y)); 48 | self 49 | } 50 | 51 | /// Sets the scaling of the image. 52 | pub fn scale(mut self, (x, y): (f32, f32)) -> Self { 53 | self.scale = (x, y); 54 | self 55 | } 56 | } 57 | 58 | /// A character that has been spawned onto the screen. 59 | #[derive(Debug)] 60 | pub struct Instance { 61 | /// An animation acting on the instance. 62 | pub animation: Option>>, 63 | /// Character which this instance belongs to. 64 | pub character: CharacterName, 65 | /// Position of the image centre in pixels. 66 | /// This determines the centre of rotation and scaling. 67 | pub centre_position: (f32, f32), 68 | /// Image that this instance draws to the screen. 69 | pub image: graphics::Image, 70 | /// Position on the screen in pixels. 71 | pub position: (f32, f32), 72 | /// Amount the image is scaled by. 73 | pub scale: (f32, f32), 74 | /// Whether the instance is visible. 75 | pub visible: bool, 76 | /// The colour of the image. 77 | pub colour: [f32; 4], 78 | /// 'To Be Killed' - Whether this instance should be removed after the animation finished. 79 | pub tbk: bool, 80 | } 81 | 82 | impl Instance { 83 | /// Creates a new instance. 84 | pub fn new(script: &Script, character: CharacterName, state: &StateName, position: (f32, f32)) -> Self { 85 | let state = &script.characters[(&character, state)]; 86 | let image = script.images.get(&state.image).unwrap_or_else(|| 87 | panic!("Image at path: {:?}, is not loaded", &state.image)).clone(); 88 | let centre_position = state.centre_position.map(|(x, y)| (x as f32, y as f32)) 89 | .unwrap_or_else(|| (image.width() as f32 / 2.0, image.height() as f32 / 2.0)); 90 | Instance { animation: None, character, centre_position, colour: [1.0; 4], image, position, scale: state.scale, visible: true, tbk: false } 91 | } 92 | 93 | /// The instance progresses any animation it contains. 94 | fn update(&mut self, ctx: &mut ggez::Context) { 95 | if self.animation.is_some() { 96 | let mut parameters = self.create_parameter(); 97 | match self.animation.as_mut().unwrap().update(&mut parameters, ctx) { 98 | AnimationState::Continue => self.update_with_parameter(parameters), 99 | AnimationState::Finished => { 100 | self.animation.take().unwrap().finish(&mut parameters); 101 | self.update_with_parameter(parameters); 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// Draws the instance to the screen. 108 | pub fn draw(&self, ctx: &mut ggez::Context) -> ggez::GameResult { 109 | let (centre_x, centre_y) = self.centre_position; 110 | let offset_x = centre_x / self.image.width() as f32; 111 | let offset_y = centre_y / self.image.height() as f32; 112 | 113 | let (scale_x, scale_y) = self.scale; 114 | let (position_x, position_y) = self.position; 115 | let draw_params = graphics::DrawParam::new() 116 | .dest([position_x, position_y]) 117 | .offset([offset_x, offset_y]) 118 | .scale([scale_x, scale_y]) 119 | .color(self.colour.into()); 120 | graphics::draw(ctx, &self.image, draw_params) 121 | } 122 | 123 | /// Adds an animation onto the Instance. 124 | /// If an animation is already present, it is finished before the new one is applied. 125 | pub fn add_animation(&mut self, animation: Box>) { 126 | if let Some(old_animation) = self.animation.replace(animation) { 127 | let mut parameters = self.create_parameter(); 128 | old_animation.finish(&mut parameters); 129 | self.update_with_parameter(parameters); 130 | } 131 | } 132 | 133 | /// Finish any animation the Instance has. 134 | pub fn finish_animation(&mut self) { 135 | if let Some(animation) = self.animation.take() { 136 | let mut parameters = self.create_parameter(); 137 | animation.finish(&mut parameters); 138 | self.update_with_parameter(parameters); 139 | } 140 | } 141 | 142 | /// Creates a parameter struct that will be given to the animation. 143 | fn create_parameter(&self) -> InstanceParameter { 144 | InstanceParameter { 145 | centre_position: self.centre_position, 146 | image: self.image.clone(), 147 | position: self.position, 148 | scale: self.scale, 149 | visible: self.visible, 150 | colour: self.colour, 151 | } 152 | } 153 | 154 | /// Uses a parameter to update the Instance's own values. 155 | fn update_with_parameter(&mut self, parameters: InstanceParameter) { 156 | self.centre_position = parameters.centre_position; 157 | self.image = parameters.image; 158 | self.position = parameters.position; 159 | self.scale = parameters.scale; 160 | self.visible = parameters.visible; 161 | self.colour = parameters.colour; 162 | } 163 | } 164 | 165 | /// Holds all the current instances. 166 | #[derive(Debug, Default)] 167 | pub struct Stage(pub HashMap); 168 | 169 | impl Stage { 170 | /// Runs all the animations that have been applied onto the instances. 171 | pub fn update(&mut self, ctx: &mut ggez::Context) { 172 | let Stage(stage) = self; 173 | stage.values_mut().for_each(|instance| instance.update(ctx)) 174 | } 175 | 176 | /// Draws all the instances it contains. 177 | pub fn draw(&self, ctx: &mut ggez::Context) -> ggez::GameResult { 178 | let Stage(stage) = self; 179 | stage.values().filter(|instance| instance.visible) 180 | .map(|instance| instance.draw(ctx)).collect() 181 | } 182 | 183 | /// Spawns a new instance onto the stage. 184 | pub fn spawn(&mut self, name: InstanceName, instance: Instance) { 185 | let Stage(stage) = self; 186 | stage.insert(name, instance); 187 | } 188 | 189 | /// Removes an instance from the stage. 190 | pub fn remove(&mut self, name: &InstanceName) { 191 | let Stage(stage) = self; 192 | stage.remove(name); 193 | } 194 | 195 | /// Finishes any animations that are currently on the instances. 196 | pub fn finish_animation(&mut self) { 197 | let Stage(stage) = self; 198 | stage.retain(|_, instance| { 199 | instance.finish_animation(); 200 | !instance.tbk 201 | }) 202 | } 203 | } 204 | 205 | impl Index<&InstanceName> for Stage { 206 | type Output = Instance; 207 | 208 | fn index(&self, index: &InstanceName) -> &Self::Output { 209 | let Stage(stage) = self; 210 | stage.get(index).unwrap_or_else(|| 211 | panic!("Instance: {:?}, does not exist in stage", index)) 212 | } 213 | } 214 | 215 | impl IndexMut<&InstanceName> for Stage { 216 | fn index_mut(&mut self, index: &InstanceName) -> &mut Self::Output { 217 | let Stage(stage) = self; 218 | stage.get_mut(index).unwrap_or_else(|| 219 | panic!("Instance: {:?}, does not exist in stage", index)) 220 | } 221 | } 222 | 223 | /// Holds all the characters and their respective states. 224 | #[derive(Debug, Default, Deserialize)] 225 | #[serde(transparent)] 226 | pub struct Characters(pub HashMap>); 227 | 228 | impl Characters { 229 | /// Adds a character with a map of its states. 230 | pub fn insert(&mut self, name: CharacterName, states: HashMap) { 231 | let Characters(characters) = self; 232 | characters.insert(name, states); 233 | } 234 | } 235 | 236 | impl Index<(&CharacterName, &StateName)> for Characters { 237 | type Output = CharacterState; 238 | 239 | fn index(&self, (character, state): (&CharacterName, &StateName)) -> &Self::Output { 240 | let Characters(characters) = self; 241 | characters.get(character).unwrap_or_else(|| panic!("Character: {:?}, does not exist in map", character)) 242 | .get(state).unwrap_or_else(|| panic!("State: {:?}, does not exist for character: {:?}", state, character)) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/game.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | use ggez::{self, Context, event, graphics, input}; 6 | 7 | use crate::{Characters, Command, History, Label, Render, Script, ScriptState, Settings, Target}; 8 | 9 | #[derive(Debug)] 10 | pub struct GameState { 11 | script: Script, 12 | settings: Settings, 13 | history: History, 14 | state: ScriptState, 15 | render: Render, 16 | reload: bool, 17 | } 18 | 19 | impl GameState { 20 | pub fn load(ctx: &mut ggez::Context, script: Script, 21 | settings: Settings, mut load_history: History) -> Self { 22 | let history = History::default(); 23 | let (state, render) = (ScriptState::default(), Render::default()); 24 | let mut state = GameState { script, settings, history, state, render, reload: false }; 25 | 26 | load_history.divergences.reverse(); 27 | state.state.next_target = Some(Target::default()); 28 | while state.history.execution_count < load_history.execution_count { 29 | match state.script[&state.state.target] { 30 | Command::Diverge(_) => state.diverge(ctx, 31 | &load_history.divergences.pop().unwrap()), 32 | _ => state.advance(ctx), 33 | } 34 | } 35 | 36 | assert!(load_history.divergences.is_empty()); 37 | state 38 | } 39 | 40 | pub fn advance(&mut self, ctx: &mut ggez::Context) { 41 | self.render.stage.finish_animation(); 42 | match &mut self.render.text { 43 | Some(text) if !text.is_finished() => text.finish(), 44 | _ => loop { 45 | self.history.execution_count += 1; 46 | self.state.target = self.state.next_target.take() 47 | .unwrap_or(self.state.target.next()); 48 | 49 | let command = &self.script[&self.state.target]; 50 | command.execute(ctx, &mut self.state, 51 | &mut self.render, &self.script, &self.settings); 52 | 53 | match command { 54 | Command::Pause => break, 55 | Command::Diverge(_) => break, 56 | Command::Dialogue(_, _) => break, 57 | _ => (), 58 | } 59 | }, 60 | } 61 | } 62 | 63 | /// Jumps to a selected label in a divergence. 64 | pub fn diverge(&mut self, ctx: &mut ggez::Context, label: &Label) { 65 | let target = self.script.labels[label].clone(); 66 | self.history.divergences.push(label.clone()); 67 | self.state.next_target = Some(target); 68 | self.render.branches.clear(); 69 | self.advance(ctx); 70 | } 71 | } 72 | 73 | impl event::EventHandler for GameState { 74 | fn update(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult { 75 | rate(ctx, self.settings.text_speed, |_| 76 | Ok(self.render.text.as_mut().map(|text| text.step())))?; 77 | self.state.sounds.retain(ggez::audio::SoundSource::playing); 78 | self.render.stage.update(ctx); 79 | Ok(()) 80 | } 81 | 82 | fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult { 83 | graphics::clear(ctx, graphics::BLACK); 84 | self.render.background.as_ref().map(|image| graphics::draw(ctx, 85 | image, graphics::DrawParam::new())).transpose()?; 86 | self.render.stage.draw(ctx)?; 87 | self.render.character.as_ref().map(|text| text.draw(ctx)).transpose()?; 88 | self.render.text.as_ref().map(|text| text.draw(ctx)).transpose()?; 89 | self.render.branches.iter().try_for_each(|(button, _)| button.draw(ctx))?; 90 | self.render.shadow_bars.iter().try_for_each(|bar| { 91 | let bar = graphics::Mesh::new_rectangle(ctx, 92 | graphics::DrawMode::fill(), *bar, graphics::BLACK)?; 93 | graphics::draw(ctx, &bar, graphics::DrawParam::new()) 94 | })?; 95 | graphics::present(ctx) 96 | } 97 | 98 | fn mouse_button_down_event(&mut self, ctx: &mut ggez::Context, 99 | _: input::mouse::MouseButton, x: f32, y: f32) { 100 | let (x, y) = transform(ctx, (x, y)); 101 | match self.script[&self.state.target] { 102 | Command::Diverge(_) => { 103 | let label = self.render.branches.iter() 104 | .find(|(button, _)| button.rectangle().contains([x, y])); 105 | label.map(|(_, label)| label).cloned() 106 | .map(|label| self.diverge(ctx, &label)); 107 | } 108 | _ => self.advance(ctx), 109 | } 110 | } 111 | 112 | fn mouse_motion_event(&mut self, ctx: &mut ggez::Context, x: f32, y: f32, _: f32, _: f32) { 113 | self.render.branches.iter_mut().for_each(|(button, _)| 114 | button.update(transform(ctx, (x, y)))); 115 | } 116 | 117 | fn key_down_event(&mut self, ctx: &mut Context, key: event::KeyCode, 118 | modifiers: event::KeyMods, _: bool) { 119 | if self.settings.developer { 120 | if modifiers.contains(event::KeyMods::CTRL) { 121 | if key == event::KeyCode::R { 122 | save_history(ctx, &self.settings, &self.history); 123 | self.reload = true; 124 | event::quit(ctx); 125 | } 126 | } 127 | } 128 | } 129 | 130 | fn quit_event(&mut self, ctx: &mut Context) -> bool { 131 | save_history(ctx, &self.settings, &self.history); 132 | false 133 | } 134 | 135 | fn resize_event(&mut self, ctx: &mut ggez::Context, width: f32, height: f32) { 136 | let window_ratio = width / height; 137 | let view_ratio = self.settings.width / self.settings.height; 138 | graphics::set_screen_coordinates(ctx, match view_ratio < window_ratio { 139 | true => { 140 | let (screen_width, view_height) = (height * view_ratio, self.settings.height); 141 | let offset = (width - screen_width) * (self.settings.width / screen_width) / 2.0; 142 | self.render.shadow_bars[1] = [self.settings.width, 0.0, offset, view_height].into(); 143 | self.render.shadow_bars[0] = [-offset, 0.0, offset, view_height].into(); 144 | [-offset, 0.0, self.settings.width + offset * 2.0, view_height] 145 | } 146 | false => { 147 | let (screen_height, view_width) = (width * view_ratio.recip(), self.settings.width); 148 | let offset = (height - screen_height) * (self.settings.height / screen_height) / 2.0; 149 | self.render.shadow_bars[1] = [0.0, self.settings.height, view_width, offset].into(); 150 | self.render.shadow_bars[0] = [0.0, -offset, view_width, offset].into(); 151 | [0.0, -offset, view_width, self.settings.height + offset * 2.0] 152 | } 153 | }.into()).unwrap(); 154 | } 155 | } 156 | 157 | pub fn rate(ctx: &mut ggez::Context, rate: u32, mut function: F) -> ggez::GameResult 158 | where F: FnMut(&mut ggez::Context) -> ggez::GameResult { 159 | Ok(while ggez::timer::check_update_time(ctx, rate) { function(ctx)?; }) 160 | } 161 | 162 | /// Transforms absolute coordinates into screen coordinates. 163 | pub fn transform(ctx: &ggez::Context, (x, y): (f32, f32)) -> (f32, f32) { 164 | let screen = graphics::screen_coordinates(ctx); 165 | let (width, height) = graphics::drawable_size(ctx); 166 | (screen.x + (screen.w / width) * x, screen.y + (screen.h / height) * y) 167 | } 168 | 169 | pub fn run(settings: Settings, mut script: F) -> ggez::GameResult 170 | where F: FnMut(&mut ggez::Context, &Settings) -> ggez::GameResult<(Script, History)> { 171 | let ctx = ggez::ContextBuilder::new("kanna", "kanna") 172 | .window_mode(ggez::conf::WindowMode { 173 | resizable: true, 174 | width: settings.width, 175 | height: settings.height, 176 | ..ggez::conf::WindowMode::default() 177 | }); 178 | 179 | let (ctx, event_loop) = &mut ctx.build()?; 180 | settings.resource_paths.iter().map(std::path::PathBuf::from) 181 | .for_each(|path| ggez::filesystem::mount(ctx, path.as_path(), true)); 182 | 183 | loop { 184 | let (script, history) = script(ctx, &settings)?; 185 | let mut state = GameState::load(ctx, script, settings.clone(), history); 186 | event::run(ctx, event_loop, &mut state)?; 187 | if !state.reload { break Ok(()); } 188 | ctx.continuing = true; 189 | } 190 | } 191 | 192 | /// Loads a script from a given path. No resources are loaded. 193 | /// Loading referenced resources is performed using [`load_resources`](fn.load_resources.html). 194 | pub fn load_script>(ctx: &mut ggez::Context, path: P) -> ggez::GameResult