diff --git a/assets/Whale/Cannon(50x30).png b/assets/Whale/Cannon(50x30).png new file mode 100644 index 0000000000..dffabfc308 Binary files /dev/null and b/assets/Whale/Cannon(50x30).png differ diff --git a/assets/Whale/Cannonball(32x36).png b/assets/Whale/Cannonball(32x36).png new file mode 100644 index 0000000000..db2196d6aa Binary files /dev/null and b/assets/Whale/Cannonball(32x36).png differ diff --git a/assets/levels/lev01.json b/assets/levels/lev01.json index b3448830f0..0f8290d56d 100644 --- a/assets/levels/lev01.json +++ b/assets/levels/lev01.json @@ -329,6 +329,18 @@ "x":210.0, "y":548.0 }, + { + "height":0, + "id":70, + "name":"cannon", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":700.0, + "y":548.0 + }, { "height":0, "id":68, diff --git a/src/main.rs b/src/main.rs index d79cb12899..265ead39ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ pub enum GameType { struct Resources { hit_fxses: EmittersCache, + cannonball_hit_fxses: EmittersCache, explosion_fxses: EmittersCache, life_explosion_fxses: EmittersCache, tiled_map: tiled::Map, @@ -41,6 +42,8 @@ struct Resources { whale: Texture2D, whale_red: Texture2D, grenades: Texture2D, + cannon: Texture2D, + cannonballs: Texture2D, curse: Texture2D, flying_curses: Texture2D, gun: Texture2D, @@ -64,6 +67,10 @@ struct Resources { pub const HIT_FX: &'static str = r#"{"local_coords":false,"emission_shape":"Point","one_shot":true,"lifetime":0.2,"lifetime_randomness":0,"explosiveness":0.65,"amount":41,"shape":{"Circle":{"subdivisions":10}},"emitting":false,"initial_direction":{"x":0,"y":-1},"initial_direction_spread":6.2831855,"initial_velocity":73.9,"initial_velocity_randomness":0.2,"linear_accel":0,"size":5.6000004,"size_randomness":0.4,"blend_mode":"Alpha","colors_curve":{"start":{"r":0.8200004,"g":1,"b":0.31818175,"a":1},"mid":{"r":0.71000004,"g":0.36210018,"b":0,"a":1},"end":{"r":0.02,"g":0,"b":0.000000007152557,"a":1}},"gravity":{"x":0,"y":0},"post_processing":{}} "#; +/// Has no size randomness, in order to make it clear to players which the radius is. +pub const CANNONBALL_HIT_FX: &'static str = r#"{"local_coords":false,"emission_shape":"Point","one_shot":true,"lifetime":0.2,"lifetime_randomness":0,"explosiveness":0.65,"amount":41,"shape":{"Circle":{"subdivisions":10}},"emitting":false,"initial_direction":{"x":0,"y":-1},"initial_direction_spread":6.2831855,"initial_velocity":73.9,"initial_velocity_randomness":0.2,"linear_accel":0,"size":64.0,"size_randomness":0.0,"blend_mode":"Alpha","colors_curve":{"start":{"r":0.8200004,"g":1,"b":0.31818175,"a":1},"mid":{"r":0.71000004,"g":0.36210018,"b":0,"a":1},"end":{"r":0.02,"g":0,"b":0.000000007152557,"a":1}},"gravity":{"x":0,"y":0},"post_processing":{}} +"#; + pub const EXPLOSION_FX: &'static str = r#"{"local_coords":false,"emission_shape":{"Sphere":{"radius":0.6}},"one_shot":true,"lifetime":0.35,"lifetime_randomness":0,"explosiveness":0.6,"amount":131,"shape":{"Circle":{"subdivisions":10}},"emitting":false,"initial_direction":{"x":0,"y":-1},"initial_direction_spread":6.2831855,"initial_velocity":316,"initial_velocity_randomness":0.6,"linear_accel":-7.4000025,"size":5.5,"size_randomness":0.3,"size_curve":{"points":[[0.005,1.48],[0.255,1.0799999],[1,0.120000005]],"interpolation":"Linear","resolution":30},"blend_mode":"Additive","colors_curve":{"start":{"r":0.9825908,"g":1,"b":0.13,"a":1},"mid":{"r":0.8,"g":0.19999999,"b":0.2000002,"a":1},"end":{"r":0.101,"g":0.099,"b":0.099,"a":1}},"gravity":{"x":0,"y":-500},"post_processing":{}} "#; @@ -102,6 +109,12 @@ impl Resources { let sproinger = load_texture("assets/Whale/Sproinger(32x32).png").await?; sproinger.set_filter(FilterMode::Nearest); + let cannon = load_texture("assets/Whale/Cannon(50x30).png").await?; + cannon.set_filter(FilterMode::Nearest); + + let cannonballs = load_texture("assets/Whale/Cannonball(32x36).png").await?; + cannonballs.set_filter(FilterMode::Nearest); + let curse = load_texture("assets/Whale/Curse(32x32).png").await?; curse.set_filter(FilterMode::Nearest); @@ -157,6 +170,8 @@ impl Resources { ); let hit_fxses = EmittersCache::new(nanoserde::DeJson::deserialize_json(HIT_FX).unwrap()); + let cannonball_hit_fxses = + EmittersCache::new(nanoserde::DeJson::deserialize_json(CANNONBALL_HIT_FX).unwrap()); let explosion_fxses = EmittersCache::new(nanoserde::DeJson::deserialize_json(EXPLOSION_FX).unwrap()); let life_explosion_fxses = @@ -164,6 +179,7 @@ impl Resources { Ok(Resources { hit_fxses, + cannonball_hit_fxses, explosion_fxses, life_explosion_fxses, tiled_map, @@ -171,6 +187,8 @@ impl Resources { whale, whale_red, grenades, + cannon, + cannonballs, curse, flying_curses, gun, @@ -195,8 +213,9 @@ impl Resources { async fn game(game_type: GameType, map: &str) { use nodes::{ - Bullets, Camera, Crate, Curse, Decoration, FlyingCurses, Fxses, GameState, Grenades, - LevelBackground, Mines, Muscet, Player, ScoreCounter, Shoes, Sproinger, Sword, MachineGun, + Bullets, Camera, Cannon, Cannonballs, Crate, Curse, Decoration, FlyingCurses, Fxses, + GameState, Grenades, LevelBackground, Mines, Muscet, Player, ScoreCounter, Shoes, + Sproinger, Sword, MachineGun, }; let resources_loading = start_coroutine({ @@ -323,6 +342,13 @@ async fn game(game_type: GameType, map: &str) { scene::add_node(grenade); wat_facing ^= true; } + if object.name == "cannon" { + let mut cannon = + Cannon::new(wat_facing, vec2(object.world_x - 35., object.world_y - 25.)); + cannon.throw(false); + scene::add_node(cannon); + wat_facing ^= true; + } if object.name == "crate" { let mut crate_node = @@ -353,6 +379,7 @@ async fn game(game_type: GameType, map: &str) { } scene::add_node(FlyingCurses::new()); + scene::add_node(Cannonballs::new()); scene::add_node(Bullets::new()); diff --git a/src/nodes.rs b/src/nodes.rs index db22ba7317..224f467881 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -2,6 +2,9 @@ mod armed_grenade; mod armed_mine; mod bullets; mod camera; +mod cannon; +mod cannonball; +mod crates; mod curse; mod decoration; mod flying_curse; @@ -13,16 +16,18 @@ mod mines; mod muscet; pub mod player; mod score_counter; -mod sword; -mod sproinger; -mod crates; mod shoes; +mod sproinger; +mod sword; mod machine_gun; pub use armed_grenade::ArmedGrenade; pub use armed_mine::ArmedMine; pub use bullets::Bullets; pub use camera::Camera; +pub use cannon::Cannon; +pub use cannonball::Cannonballs; +pub use crates::Crate; pub use curse::Curse; pub use decoration::Decoration; pub use flying_curse::FlyingCurses; @@ -34,8 +39,7 @@ pub use mines::Mines; pub use muscet::Muscet; pub use player::Player; pub use score_counter::ScoreCounter; -pub use sword::Sword; -pub use sproinger::Sproinger; -pub use crates::Crate; pub use shoes::Shoes; +pub use sproinger::Sproinger; +pub use sword::Sword; pub use machine_gun::MachineGun; diff --git a/src/nodes/cannon.rs b/src/nodes/cannon.rs new file mode 100644 index 0000000000..147f74391a --- /dev/null +++ b/src/nodes/cannon.rs @@ -0,0 +1,301 @@ +use macroquad::{ + color, + prelude::{ + animation::{AnimatedSprite, Animation}, + collections::storage, + coroutines::{start_coroutine, wait_seconds, Coroutine}, + draw_circle, draw_circle_lines, draw_texture_ex, get_frame_time, + scene::{self, Handle, HandleUntyped, RefMut}, + vec2, Color, DrawTextureParams, Rect, Vec2, + }, +}; + +use crate::Resources; + +use super::{ + cannonball::CANNONBALL_HEIGHT, + player::{capabilities, PhysicsBody, Weapon, PLAYER_HITBOX_HEIGHT, PLAYER_HITBOX_WIDTH}, + Player, +}; + +const INITIAL_CANNONBALLS: i32 = 3; +const MAXIMUM_CANNONBALLS: i32 = 3; + +const CANNON_WIDTH: f32 = 50.; +const CANNON_HEIGHT: f32 = 30.; +const CANNON_ANIMATION_BASE: &'static str = "base"; + +const CANNON_THROWBACK: f32 = 1050.0; +const SHOOTING_GRACE_TIME: f32 = 1.0; // seconds + +pub struct Cannon { + cannon_sprite: AnimatedSprite, + + pub thrown: bool, + + pub amount: i32, + pub body: PhysicsBody, + + origin_pos: Vec2, + deadly_dangerous: bool, + + grace_time: f32, +} + +impl Cannon { + pub fn new(facing: bool, pos: Vec2) -> Self { + let cannon_sprite = AnimatedSprite::new( + CANNON_WIDTH as u32, + CANNON_HEIGHT as u32, + &[Animation { + name: CANNON_ANIMATION_BASE.to_string(), + row: 0, + frames: 1, + fps: 1, + }], + false, + ); + + Self { + cannon_sprite, + body: PhysicsBody { + pos, + facing, + angle: 0.0, + speed: vec2(0., 0.), + collider: None, + on_ground: false, + last_frame_on_ground: false, + have_gravity: true, + bouncyness: 0.0, + }, + thrown: false, + amount: INITIAL_CANNONBALLS, + origin_pos: pos, + deadly_dangerous: false, + grace_time: 0., + } + } + + fn draw_hud(&self) { + let full_color = Color::new(0.8, 0.9, 1.0, 1.0); + let empty_color = Color::new(0.8, 0.9, 1.0, 0.8); + for i in 0..MAXIMUM_CANNONBALLS { + let x = self.body.pos.x + 15.0 * i as f32; + + if i >= self.amount { + draw_circle_lines(x, self.body.pos.y - 12.0, 4.0, 2., empty_color); + } else { + draw_circle(x, self.body.pos.y - 12.0, 4.0, full_color); + }; + } + } + + pub fn throw(&mut self, force: bool) { + self.thrown = true; + + if force { + self.body.speed = if self.body.facing { + vec2(600., -200.) + } else { + vec2(-600., -200.) + }; + } else { + self.body.angle = 3.5; + } + + let mut resources = storage::get_mut::(); + + let cannon_mount_pos = if self.body.facing { + vec2(30., 10.) + } else { + vec2(-50., 10.) + }; + + if self.body.collider.is_none() { + self.body.collider = Some(resources.collision_world.add_actor( + self.body.pos + cannon_mount_pos, + 40, + 30, + )); + } else { + resources.collision_world.set_actor_position( + self.body.collider.unwrap(), + self.body.pos + cannon_mount_pos, + ); + } + self.origin_pos = self.body.pos + cannon_mount_pos / 2.; + } + + pub fn shoot(node_h: Handle, player: Handle) -> Coroutine { + let coroutine = async move { + { + let mut node = scene::get_node(node_h); + + if node.amount <= 0 || node.grace_time > 0. { + let player = &mut *scene::get_node(player); + player.state_machine.set_state(Player::ST_NORMAL); + + node.grace_time -= get_frame_time(); + + return; + } else { + node.grace_time = SHOOTING_GRACE_TIME; + } + + let mut cannonballs = + scene::find_node_by_type::().unwrap(); + let cannonball_pos = vec2( + node.body.pos.x, + node.body.pos.y - 20. - (CANNONBALL_HEIGHT as f32 / 2.), + ); + cannonballs.spawn_cannonball(cannonball_pos, node.body.facing, player); + + let player = &mut *scene::get_node(player); + player.body.speed.x = -CANNON_THROWBACK * player.body.facing_dir(); + } + + wait_seconds(0.08).await; + + { + let mut node = scene::get_node(node_h); + + node.amount -= 1; + + let player = &mut *scene::get_node(player); + player.state_machine.set_state(Player::ST_NORMAL); + } + }; + + start_coroutine(coroutine) + } + + pub fn gun_capabilities() -> capabilities::Gun { + fn throw(node: HandleUntyped, force: bool) { + let mut node = scene::get_untyped_node(node).unwrap().to_typed::(); + + Cannon::throw(&mut *node, force); + } + + fn shoot(node: HandleUntyped, player: Handle) -> Coroutine { + let node = scene::get_untyped_node(node) + .unwrap() + .to_typed::() + .handle(); + + Cannon::shoot(node, player) + } + + fn is_thrown(node: HandleUntyped) -> bool { + let node = scene::get_untyped_node(node).unwrap().to_typed::(); + + node.thrown + } + + fn pick_up(node: HandleUntyped) { + let mut node = scene::get_untyped_node(node).unwrap().to_typed::(); + + node.body.angle = 0.; + node.amount = INITIAL_CANNONBALLS; + + node.thrown = false; + } + + capabilities::Gun { + throw, + shoot, + is_thrown, + pick_up, + } + } +} + +impl scene::Node for Cannon { + fn ready(mut node: RefMut) { + node.provides::(( + node.handle().untyped(), + node.handle().lens(|node| &mut node.body), + Self::gun_capabilities(), + )); + } + + fn fixed_update(mut node: RefMut) { + node.cannon_sprite.update(); + + if node.thrown { + node.body.update(); + node.body.update_throw(); + + if (node.origin_pos - node.body.pos).length() > 70. { + node.deadly_dangerous = true; + } + if node.body.speed.length() <= 200.0 { + node.deadly_dangerous = false; + } + if node.body.on_ground { + node.deadly_dangerous = false; + } + + if node.deadly_dangerous { + let others = scene::find_nodes_by_type::(); + let cannon_hit_box = Rect::new( + node.body.pos.x, + node.body.pos.y, + CANNON_WIDTH, + CANNON_HEIGHT, + ); + + for mut other in others { + if Rect::new( + other.body.pos.x, + other.body.pos.y, + PLAYER_HITBOX_WIDTH, + PLAYER_HITBOX_HEIGHT, + ) + .overlaps(&cannon_hit_box) + { + other.kill(!node.body.facing); + } + } + } + } + + node.grace_time -= get_frame_time(); + } + + fn draw(node: RefMut) { + let resources = storage::get_mut::(); + + let cannon_mount_pos = if node.thrown == false { + if node.body.facing { + vec2(5., 16.) + } else { + vec2(-30., 16.) + } + } else { + if node.body.facing { + vec2(-25., 0.) + } else { + vec2(5., 0.) + } + }; + + draw_texture_ex( + resources.cannon, + node.body.pos.x + cannon_mount_pos.x, + node.body.pos.y + cannon_mount_pos.y, + color::WHITE, + DrawTextureParams { + source: Some(node.cannon_sprite.frame().source_rect), + dest_size: Some(node.cannon_sprite.frame().dest_size), + flip_x: !node.body.facing, + rotation: node.body.angle, + ..Default::default() + }, + ); + + if node.thrown == false { + node.draw_hud(); + } + } +} diff --git a/src/nodes/cannonball.rs b/src/nodes/cannonball.rs new file mode 100644 index 0000000000..c7763d0bed --- /dev/null +++ b/src/nodes/cannonball.rs @@ -0,0 +1,197 @@ +use macroquad::{ + color, + experimental::{ + animation::{AnimatedSprite, Animation}, + collections::storage, + scene::RefMut, + }, + prelude::{scene::Handle, *}, +}; + +use crate::{nodes::player::PhysicsBody, Resources}; + +use super::{ + player::{PLAYER_HITBOX_HEIGHT, PLAYER_HITBOX_WIDTH}, + Player, +}; + +const CANNONBALL_COUNTDOWN_DURATION: f32 = 0.5; +/// After shooting, the owner is safe for this amount of time. This is crucial, otherwise, given the +/// large hitbox, they will die immediately on shoot. +/// The formula is simplified (it doesn't include mount position, run speed and throwback). +const CANNONBALL_OWNER_SAFE_TIME: f32 = + (EXPLOSION_HITBOX_WIDTH / 2.) / CANNONBALL_INITIAL_SPEED_X_REL; + +const CANNONBALL_WIDTH: f32 = 32.; +pub const CANNONBALL_HEIGHT: f32 = 36.; +const CANNONBALL_ANIMATION_ROLLING: &'static str = "rolling"; +const CANNONBALL_INITIAL_SPEED_X_REL: f32 = 600.; +const CANNONBALL_INITIAL_SPEED_Y: f32 = -200.; +const CANNONBALL_MOUNT_X_REL: f32 = 20.; +const CANNONBALL_MOUNT_Y: f32 = 40.; + +const EXPLOSION_HITBOX_WIDTH: f32 = 4. * CANNONBALL_WIDTH; +const EXPLOSION_HITBOX_HEIGHT: f32 = 4. * CANNONBALL_HEIGHT; + +pub struct Cannonball { + cannonball_sprite: AnimatedSprite, + body: PhysicsBody, + lived: f32, + countdown: f32, + owner: Handle, + owner_safe_countdown: f32, +} + +impl Cannonball { + pub fn new(pos: Vec2, facing: bool, owner: Handle) -> Self { + // This can be easily turned into a single sprite, rotated via DrawTextureParams. + // + let cannonball_sprite = AnimatedSprite::new( + CANNONBALL_WIDTH as u32, + CANNONBALL_HEIGHT as u32, + &[Animation { + name: CANNONBALL_ANIMATION_ROLLING.to_string(), + row: 0, + frames: 1, + fps: 1, + }], + true, + ); + + let speed = if facing { + vec2(CANNONBALL_INITIAL_SPEED_X_REL, CANNONBALL_INITIAL_SPEED_Y) + } else { + vec2(-CANNONBALL_INITIAL_SPEED_X_REL, CANNONBALL_INITIAL_SPEED_Y) + }; + + let mut body = PhysicsBody { + pos, + facing, + angle: 0.0, + speed, + collider: None, + on_ground: false, + last_frame_on_ground: false, + have_gravity: true, + bouncyness: 1.0, + }; + + let mut resources = storage::get_mut::(); + + let cannonball_mount_pos = if facing { + vec2(CANNONBALL_MOUNT_X_REL, CANNONBALL_MOUNT_Y) + } else { + vec2(-CANNONBALL_MOUNT_X_REL, CANNONBALL_MOUNT_Y) + }; + + body.collider = Some(resources.collision_world.add_actor( + body.pos + cannonball_mount_pos, + CANNONBALL_WIDTH as i32, + CANNONBALL_HEIGHT as i32, + )); + + Self { + cannonball_sprite, + body, + lived: 0.0, + countdown: CANNONBALL_COUNTDOWN_DURATION, + owner, + owner_safe_countdown: CANNONBALL_OWNER_SAFE_TIME, + } + } +} + +pub struct Cannonballs { + cannonballs: Vec, +} + +impl Cannonballs { + pub fn new() -> Self { + Cannonballs { + cannonballs: vec![], + } + } + + pub fn spawn_cannonball(&mut self, pos: Vec2, facing: bool, owner: Handle) { + self.cannonballs.push(Cannonball::new(pos, facing, owner)); + } +} + +impl scene::Node for Cannonballs { + fn fixed_update(mut node: RefMut) { + for cannonball in &mut node.cannonballs { + cannonball.body.update(); + cannonball.lived += get_frame_time(); + cannonball.owner_safe_countdown -= get_frame_time(); + } + + node.cannonballs.retain(|cannonball| { + let hit_fxses = &mut storage::get_mut::().cannonball_hit_fxses; + + let explosion_position = + cannonball.body.pos + vec2(CANNONBALL_WIDTH / 2., CANNONBALL_HEIGHT / 2.); + + if cannonball.lived < cannonball.countdown { + let cannonball_owner_id = scene::get_node(cannonball.owner).id; + + let cannonball_hitbox = Rect::new( + cannonball.body.pos.x + (CANNONBALL_WIDTH - EXPLOSION_HITBOX_WIDTH) / 2., + cannonball.body.pos.y + (CANNONBALL_HEIGHT - EXPLOSION_HITBOX_HEIGHT) / 2., + EXPLOSION_HITBOX_WIDTH, + EXPLOSION_HITBOX_HEIGHT, + ); + + for mut player in scene::find_nodes_by_type::() { + if player.id != cannonball_owner_id || cannonball.owner_safe_countdown < 0. { + let player_hitbox = Rect::new( + player.body.pos.x, + player.body.pos.y, + PLAYER_HITBOX_WIDTH, + PLAYER_HITBOX_HEIGHT, + ); + if player_hitbox.intersect(cannonball_hitbox).is_some() { + hit_fxses.spawn(explosion_position); + + scene::find_node_by_type::() + .unwrap() + .shake(); + + let direction = cannonball.body.pos.x + > (player.body.pos.x + PLAYER_HITBOX_WIDTH / 2.); + player.kill(direction); + + return false; + } + } + } + + return true; + } + + hit_fxses.spawn(explosion_position); + + false + }); + } + + fn draw(mut node: RefMut) { + let resources = storage::get_mut::(); + for cannonball in &mut node.cannonballs { + cannonball.cannonball_sprite.update(); + + draw_texture_ex( + resources.cannonballs, + cannonball.body.pos.x, + cannonball.body.pos.y, + color::WHITE, + DrawTextureParams { + source: Some(cannonball.cannonball_sprite.frame().source_rect), + dest_size: Some(cannonball.cannonball_sprite.frame().dest_size), + flip_x: cannonball.body.facing, + rotation: 0.0, + ..Default::default() + }, + ); + } + } +} diff --git a/src/nodes/fxses.rs b/src/nodes/fxses.rs index 136ece8aa3..216be251fa 100644 --- a/src/nodes/fxses.rs +++ b/src/nodes/fxses.rs @@ -18,6 +18,7 @@ impl scene::Node for Fxses { let _z = telemetry::ZoneGuard::new("draw particles"); resources.hit_fxses.draw(); + resources.cannonball_hit_fxses.draw(); resources.explosion_fxses.draw(); push_camera_state();