#![allow(clippy::upper_case_acronyms)] #![allow(clippy::new_without_default)] use macroquad::prelude::*; use macroquad::rand::*; pub mod grammar; use grammar::*; const FONT: &str = &"./assets/fira_code.ttf"; const FONT_WIDTH: u16 = 8; const FONT_HEIGHT_REAL: f32 = FONT_WIDTH as f32 / 8.0 * 13.0; const FONT_HEIGHT: u16 = (FONT_HEIGHT_REAL + 4.0) as u16; const FONT_DIFFERENCE: u16 = FONT_HEIGHT - FONT_HEIGHT_REAL as u16; const GRID_WIDTH: usize = 60; const GRID_HEIGHT: usize = 25; // 9 = 3 + height of fishing guy - 1 for some reason const WATER_LEVEL: i32 = 9; const HEIGHT: i32 = FONT_HEIGHT as i32 * 25; // 90 = 60 + 30 const WIDTH: i32 = FONT_WIDTH as i32 * 90; const UPDATE: f64 = 150.0 / 1000.0; // in seconds const ANIM_CHANGE: f64 = 750.0 / 1000.0; fn window_conf() -> Conf { Conf { window_title: "fishing minigame".to_owned(), window_width: WIDTH, window_height: HEIGHT, fullscreen: false, ..Default::default() } } struct Fish { is_right: bool, // middle length length: u8, pos: IVec2, } impl Fish { fn new() -> Self { let is_right = gen_range(0, 2) == 0; let length = gen_range(0, 6); Self { is_right, length, pos: IVec2::new( if is_right { -(length as i32 + 3) } else { GRID_WIDTH as i32 }, gen_range(WATER_LEVEL + 1, GRID_HEIGHT as i32 - 2), ), } } fn into_text(self, grammar: &Grammar) -> Option { grammar.flatten() } } enum Mode { Start, Play, End, } struct Game { mode: Mode, state: State, draw: Draw, grammar: Grammar, } impl Game { async fn new() -> Result { let mut draw = Draw::new().await.unwrap(); let mut state = State::new(); game_setup(&mut state, &mut draw); let grammar = grammar_setup(); Ok(Self { mode: Mode::Start, state, draw, grammar, }) } fn update(&mut self) { // fish update let mut remove = Vec::new(); for (i, fish) in self.state.fish.iter_mut().enumerate() { // if something is hooked and it's not this let is_hooked = self.state.is_hooked.map(|idx| idx == i).unwrap_or(false); if !is_hooked { if fish.is_right { fish.pos.x += 1; if fish.pos.x >= GRID_WIDTH as i32 { remove.push(i); } } else { fish.pos.x -= 1; if fish.pos.x <= -(fish.length as i32 + 3) { remove.push(i); } } } } for i in remove.into_iter().rev() { self.state.fish.remove(i); if let Some(idx) = &mut self.state.is_hooked { if *idx > i { *idx -= 1; } } } if self.state.fish.len() < 3 && gen_range(0, 15) < 1 { self.state.fish.push(Fish::new()); } // collision let hook = self.state.hook; if self.state.is_hooked.is_none() { for (i, fish) in self.state.fish.iter().enumerate() { if fish.pos.y == hook.y && fish.pos.x <= hook.x && fish.pos.x + fish.length as i32 + 3 > hook.x { self.state.is_hooked = Some(i); break; } } } } } struct Draw { // only two frames of animation first_frame: bool, // animated sprites animations: Vec, // static sprites sprites: Vec, font: Font, } impl Draw { async fn new() -> Result { // font: fira code. dimensions: 13.0x8.0 let font = load_ttf_font(FONT).await?; Ok(Self { first_frame: true, animations: Vec::new(), sprites: Vec::new(), font, }) } fn add_sprite(&mut self, sprite: Sprite) -> usize { self.sprites.push(sprite); self.sprites.len() - 1 } fn add_animation(&mut self, animation: Animation) -> usize { self.animations.push(animation); self.animations.len() - 1 } } struct Sprite { width: usize, height: usize, color: Option, sprite: String, } impl Sprite { fn new(width: usize, height: usize, color: Option, sprite: String) -> Self { assert_eq!(width * height, sprite.len() - height + 1); Self { width, height, color, sprite, } } fn draw(&self, pos: IVec2, font: Font) { let pos = pos.as_f32() * Vec2::new(FONT_WIDTH as f32, FONT_HEIGHT as f32); if let Some(color) = self.color { draw_rectangle( pos.x, pos.y, self.width as f32 * FONT_WIDTH as f32, self.height as f32 * FONT_HEIGHT as f32, color, ); } for (i, row) in self.sprite.split('\n').enumerate() { let i = (i + 1) as f32 * FONT_HEIGHT as f32 - FONT_DIFFERENCE as f32; draw_text_ex( row, pos.x, pos.y + i, TextParams { font, color: BLACK, font_size: FONT_HEIGHT - FONT_DIFFERENCE, ..Default::default() }, ); } } } struct Animation { width: usize, height: usize, color: Color, /// animations can have two frames and must be strings frames: [String; 2], } impl Animation { fn new(width: usize, height: usize, color: Color, frames: [String; 2]) -> Self { assert_eq!(width * height, frames[0].len() - height + 1); assert_eq!(width * height, frames[1].len() - height + 1); Self { width, height, color, frames, } } fn draw(&self, pos: IVec2, first_frame: bool, font: Font) { let pos = pos.as_f32() * Vec2::new(FONT_WIDTH as f32, FONT_HEIGHT as f32); draw_rectangle( pos.x, pos.y, self.width as f32 * FONT_WIDTH as f32, self.height as f32 * FONT_HEIGHT as f32, self.color, ); for (i, row) in self.frames[first_frame as usize].split('\n').enumerate() { let i = (i + 1) as f32 * FONT_HEIGHT as f32 - FONT_DIFFERENCE as f32; draw_text_ex( row, pos.x, pos.y + i, TextParams { font, color: BLACK, font_size: FONT_HEIGHT - FONT_DIFFERENCE, ..Default::default() }, ); } } } struct State { // TODO: consider vecdeque instead of stack fish: Vec, hook: IVec2, // which fish is hooked is_hooked: Option, // positions of other things (unmoving) positions: Vec, // parallel vec of sprites corresponding to ^ sprites: Vec, caught_fish: Vec, } enum DrawType { Anim(usize), Sprite(usize), } impl State { fn new() -> Self { State { fish: Vec::new(), hook: IVec2::new(GRID_WIDTH as i32 / 2, WATER_LEVEL + 5), is_hooked: None, positions: Vec::new(), sprites: Vec::new(), caught_fish: Vec::new(), } } } #[macroquad::main(window_conf)] async fn main() { srand(get_time().to_bits()); let mut game = Game::new().await.unwrap(); let mut updated = get_time(); let mut anim_updated = get_time(); loop { match game.mode { Mode::Start => { // draw clear_background(SKYBLUE); draw_text_centered( &"press ENTER to start", HEIGHT as f32 / 2.0 - FONT_HEIGHT as f32 * 2.0, game.draw.font, ); draw_text_centered( &"use up and down to move the hook", HEIGHT as f32 / 2.0 - FONT_HEIGHT as f32, game.draw.font, ); draw_text_centered( &"use space to catch a fish", HEIGHT as f32 / 2.0, game.draw.font, ); draw_text_centered( &"use escape to end the game", HEIGHT as f32 / 2.0 + FONT_HEIGHT as f32, game.draw.font, ); if is_key_pressed(KeyCode::Enter) { game.mode = Mode::Play; } } Mode::Play => { if is_key_pressed(KeyCode::Escape) { game.mode = Mode::End; continue; } if is_key_pressed(KeyCode::Up) { game.state.hook.y = (game.state.hook.y - 1).max(WATER_LEVEL); } if is_key_pressed(KeyCode::Down) { game.state.hook.y = (game.state.hook.y + 1).min(GRID_HEIGHT as i32 - 2); } if let Some(idx) = game.state.is_hooked { game.state.fish[idx].pos.y = game.state.hook.y; if is_key_pressed(KeyCode::Space) { let fish = game.state.fish.remove(idx); game.state .caught_fish .push(fish.into_text(&game.grammar).unwrap()); game.state.is_hooked = None; } } if get_time() - updated >= UPDATE { game.update(); updated = get_time(); } game.draw(); if get_time() - anim_updated >= ANIM_CHANGE { game.draw.first_frame = !game.draw.first_frame; anim_updated = get_time(); } } Mode::End => { clear_background(SKYBLUE); draw_text_centered( &format!( "you caught {} fish, thanks for playing", game.state.caught_fish.len() ), HEIGHT as f32 / 2.0, game.draw.font, ); draw_text_centered( &"press ENTER to restart", HEIGHT as f32 / 2.0 + FONT_HEIGHT as f32, game.draw.font, ); if is_key_pressed(KeyCode::Enter) { game.state = State::new(); game.draw.animations.clear(); game.draw.sprites.clear(); game_setup(&mut game.state, &mut game.draw); game.mode = Mode::Start; } } } next_frame().await; } } impl Game { fn draw(&self) { const FISH_WIDTH: f32 = FONT_WIDTH as f32 * GRID_WIDTH as f32; const H: f32 = HEIGHT as f32; const WL: f32 = WATER_LEVEL as f32 * FONT_HEIGHT as f32; // water clear_background(SKYBLUE); // sky draw_rectangle(0.0, 0.0, FISH_WIDTH, WL, WHITE); // ground draw_rectangle( 0.0, H - FONT_HEIGHT as f32, FISH_WIDTH, FONT_HEIGHT as f32, BEIGE, ); // static sprites for (pos, sprite) in self.state.positions.iter().zip(self.state.sprites.iter()) { match sprite { DrawType::Anim(i) => { self.draw.animations[*i].draw(*pos, self.draw.first_frame, self.draw.font) } DrawType::Sprite(i) => self.draw.sprites[*i].draw(*pos, self.draw.font), } } // fish for fish in self.state.fish.iter() { let sprite = &self.draw.sprites[fish.length as usize * 2 + fish.is_right as usize]; sprite.draw(fish.pos, self.draw.font); } // hook self.draw.sprites[12].draw(self.state.hook, self.draw.font); // line let sprite = (0..self.state.hook.y - 5) .map(|_| "|\n") .collect::(); let sprite = sprite.trim().to_string(); let sprite = Sprite::new(1, self.state.hook.y as usize - 5, None, sprite); sprite.draw(IVec2::new(GRID_WIDTH as i32 / 2, 5), self.draw.font); // sidebar draw_rectangle(FISH_WIDTH, 0.0, WIDTH as f32 - FISH_WIDTH, H, LIGHTGRAY); // draw fish info // sidebar width: 480 - 8px? margins * 2 = 464 / 16 = 29 characters / line let mut y = 30.0; let x = GRID_WIDTH as f32 * FONT_WIDTH as f32 + 8.0; for text in self.state.caught_fish.iter().rev() { if y > HEIGHT as f32 + FONT_HEIGHT as f32 { break; } y += 20.0; let mut line = "".to_string(); line.reserve_exact(28); for word in text.split_whitespace() { if line.len() + word.len() > 28 { draw_text_ex( &line, x, y, TextParams { font: self.draw.font, color: BLACK, font_size: FONT_HEIGHT - FONT_DIFFERENCE, ..Default::default() }, ); line.clear(); y += FONT_HEIGHT as f32; } line += word; line += " "; } draw_text_ex( &line, x, y, TextParams { font: self.draw.font, color: BLACK, font_size: FONT_HEIGHT - FONT_DIFFERENCE, ..Default::default() }, ); y += FONT_HEIGHT as f32; } } } fn grammar_setup() -> Grammar { let rules = [ ("origin".to_string(), vec!["#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#fish#. #fishFact#".to_string(), "#seaFish#. #seaFishFact#".to_string(), "#largeFish#. #largeFishFact#".to_string(), "#whale#. #whaleFact#".to_string(), "#unnervingFish#. #unnervingFishFact#".to_string(), "sea bass. no, wait--- it's at least a c+!".to_string()]), ("fish".to_string(), vec!["rainbow trout".to_string(), "salmon".to_string(), "black bass".to_string(), "catfish".to_string(), "brook trout".to_string(), "cod".to_string(), "minnow".to_string(), "turbofish".to_string()]), ("fishFact".to_string(), vec!["this fish loves to swim about.".to_string(), "this fish was sleeping and you woke it up, which is kind of rude.".to_string(), "this fish will grant you three wishes if you don't eat it but you weren't going to do that anyway.".to_string(), "fish are neat :)".to_string(), "this fish likes to hide under rocks.".to_string(), "this fish can breathe underwater.".to_string(), "this fish likes to blow bubbles.".to_string(), "this fish likes to flick its tail.".to_string(), "this fish can do a backflip.".to_string(), "this fish is very tired and wants to rest.".to_string(), "this fish wants to show you some sick card tricks.".to_string()]), ("unnervingFish".to_string(), vec!["swamp eel".to_string(), "pike".to_string(), "jellyfish".to_string(), "sunfish".to_string(), "angler fish".to_string(), "squid".to_string()]), ("unnervingFishFact".to_string(), vec!["#fishFact#".to_string(), "i'm spooked by this fish.".to_string(), "this fish has so many teeth.".to_string(), "you feel like this fish will haunt your nightmares.".to_string(), "please put it back.".to_string()]), ("largeFish".to_string(), vec!["lemon shark".to_string(), "mako shark".to_string(), "whale shark".to_string(), "great white shark".to_string()]), ("largeFishFact".to_string(), vec!["#seaFishFact#".to_string(), "#fishFact#".to_string(), "this fish is incredibly large.".to_string(), "OH GOD IT'S JUST SO BIG".to_string(), "this fish might eat you? you're not sure, it hasn't happened yet".to_string(), "this fish has so many teeth.".to_string()]), ("whale".to_string(), vec!["blue whale".to_string(), "humpback whale".to_string(), "sperm whale".to_string(), "right whale".to_string(), "razor back whale".to_string()]), ("whaleFact".to_string(), vec!["#seaFishFact#".to_string(), "#largeFishFact#".to_string(), "\"Oh, man! Admire and model thyself after the whale! Do thou, too, remain warm among ice. Do thou, too, live in this world without being of it. Be cool at the equator; keep thy blood fluid at the pole. Like the great dome of St. Peter's, and like the great whale, retain, O man! in all seasons a temperature of thine own.\" (melville 343)".to_string(), "\"I. A Fast-Fish belongs to the party fast to it. II. A Loose-Fish is fair game for anybody who can soonest catch it.\" (melville 441)".to_string(), "\"All men live enveloped in whale lines.\" (melville 315)".to_string(), "\"Let him go. I know little more of him, nor does anybody else.\" (melville 153)".to_string(), "\"a whale is A SPOUTING FISH WITH A HORIZONTAL TAIL. There you have him.\" (melville 148)".to_string()]), ("seaFish".to_string(), vec!["black sea bass".to_string(), "mackerel".to_string(), "tuna".to_string(), "sea bass".to_string(), "anchovy".to_string()]), ("seaFishFact".to_string(), vec!["#fishFact#".to_string(), "this fish lives in the sea and you're not sure how it got here.".to_string(), "you're not even sure if this fish can live in freshwater, but okay.".to_string()]) ]; Grammar::from(rules) } fn game_setup(state: &mut State, draw: &mut Draw) { // fish for i in 0..6 { let mut middle = "".to_string(); middle.reserve_exact(i); for _ in 0..i { middle += ":"; } draw.add_sprite(Sprite::new( i + 3, 1, Some(SKYBLUE), "<".to_owned() + &middle.clone() + "><", )); draw.add_sprite(Sprite::new( i + 3, 1, Some(SKYBLUE), "><".to_owned() + &middle + ">", )); } // hook draw.add_sprite(Sprite::new(1, 1, Some(SKYBLUE), "&".to_owned())); // seaweed let mut plant_already = [false; GRID_WIDTH]; for _ in 0..5 { let mut x = gen_range(1, GRID_WIDTH - 1); while plant_already[x] { x = gen_range(1, GRID_WIDTH - 1); } plant_already[x] = true; let height = gen_range(2, (GRID_HEIGHT - WATER_LEVEL as usize) / 2); let mut plant = "".to_string(); plant.reserve_exact(height + 2); for _ in 0..height / 2 + 1 { plant += "\\\n/\n"; } let start = gen_range(0, 2) == 0; let anim = if start { [ plant[..height * 2 - 1].to_string(), plant[2..height * 2 + 1].to_string(), ] } else { [ plant[2..height * 2 + 1].to_string(), plant[..height * 2 - 1].to_string(), ] }; let anim = Animation::new(1, height, GREEN, anim); let i = draw.add_animation(anim); state .positions .push(IVec2::new(x as i32, GRID_HEIGHT as i32 - height as i32 - 1)); state.sprites.push(DrawType::Anim(i)); } // water let mut water = "^".to_string(); water.reserve_exact(GRID_WIDTH); for _ in 0..GRID_WIDTH / 2 { water += "~^"; } let anim = Animation::new( GRID_WIDTH, 1, SKYBLUE, [water[..GRID_WIDTH].to_string(), water[1..].to_string()], ); let i = draw.add_animation(anim); state.positions.push(IVec2::new(0, WATER_LEVEL)); state.sprites.push(DrawType::Anim(i)); // ground let groud_chars = [ "-", "=", ".", "__", "--", "==", "..", "___", "---", "===", "...", ]; let mut ground = "".to_string(); let mut i = 0; while i < GRID_WIDTH { let add = groud_chars[gen_range(0, groud_chars.len())]; i += add.len(); ground += add; } ground = ground[..GRID_WIDTH].to_string(); let i = draw.add_sprite(Sprite::new(GRID_WIDTH, 1, Some(BEIGE), ground)); state.positions.push(IVec2::new(0, GRID_HEIGHT as i32 - 1)); state.sprites.push(DrawType::Sprite(i)); // water // dude #[rustfmt::skip] let dude = r#" O |--o_______________________. |__ ===== | | | | | | "#; let i = draw.add_sprite(Sprite::new(31, 6, None, dude.to_string())); state.positions.push(IVec2::new(0, 3)); state.sprites.push(DrawType::Sprite(i)); } fn draw_text_centered(text: &str, y: f32, font: Font) { let dims = measure_text(text, Some(font), FONT_HEIGHT - FONT_DIFFERENCE, 1.0); draw_text_ex( text, WIDTH as f32 / 2.0 - dims.width / 2.0, y, TextParams { font, color: BLACK, font_size: FONT_HEIGHT - FONT_DIFFERENCE, ..Default::default() }, ); }