diff --git a/src/game/ball/mod.rs b/src/game/ball/mod.rs index 90b1de3..cdbef66 100644 --- a/src/game/ball/mod.rs +++ b/src/game/ball/mod.rs @@ -4,13 +4,14 @@ use bevy_ggrs::prelude::*; use super::GameState; use super::components::Team; -use super::field::{CellClicked, Wall, toggle_cell}; +use super::field::{CellClicked, Wall}; +use super::item::spawn_item; use super::paddle::{Paddle, move_paddles}; mod respawn; -const FIRST_BALL_SPEED: f32 = 300.0; -const BALL_RADIUS: f32 = 10.0; +pub const FIRST_BALL_SPEED: f32 = 300.0; +pub const BALL_RADIUS: f32 = 10.0; pub struct BallPlugin; @@ -24,10 +25,11 @@ impl Plugin for BallPlugin { check_collision, respawn::respawn_balls, respawn::handle_respawning_balls, + respawn::despawn_stopped_balls, ) .chain() .after(move_paddles) - .before(toggle_cell) + .before(spawn_item) .run_if(in_state(GameState::InGame)), ) .rollback_component_with_copy::() @@ -140,7 +142,7 @@ fn check_collision( // Check for cell collisions for (cell_entity, cell, cell_team, cell_transform) in &q_cell { - if cell_team != ball_team { + if cell_team == ball_team { continue; } let closest_point = Aabb2d::new(cell_transform.translation.truncate(), cell.half_size) @@ -156,7 +158,10 @@ fn check_collision( let normal = diff.normalize(); velocity.0 = velocity.reflect(normal); ball_transform.translation += (normal * (ball.radius - diff.length())).extend(0.); - events.write(CellClicked { cell: cell_entity }); + events.write(CellClicked { + cell: cell_entity, + team: *ball_team, + }); continue 'ball; } } diff --git a/src/game/ball/respawn.rs b/src/game/ball/respawn.rs index 8d00635..091f8db 100644 --- a/src/game/ball/respawn.rs +++ b/src/game/ball/respawn.rs @@ -76,3 +76,15 @@ pub fn handle_respawning_balls( } } } + +#[allow(clippy::type_complexity)] +pub fn despawn_stopped_balls( + mut commands: Commands, + q_ball: Query<(Entity, &Velocity), (With, Without)>, +) { + for (entity, velocity) in q_ball { + if velocity.0.length_squared() < 0.01 { + commands.entity(entity).despawn(); + } + } +} diff --git a/src/game/components.rs b/src/game/components.rs index 616c932..c7bd997 100644 --- a/src/game/components.rs +++ b/src/game/components.rs @@ -2,3 +2,18 @@ use bevy::prelude::*; #[derive(Component, Deref, DerefMut, PartialEq, Clone, Copy)] pub struct Team(pub usize); + +impl Team { + pub const ITEM: Self = Self(2); + + pub fn hue(&self) -> f32 { + match self.0 { + 0 => 0., + 1 => 180., + _ => 60., + } + } +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Deref, DerefMut)] +pub struct Count(pub usize); diff --git a/src/game/field/mod.rs b/src/game/field/mod.rs index 9ac18a2..978def1 100644 --- a/src/game/field/mod.rs +++ b/src/game/field/mod.rs @@ -1,7 +1,15 @@ use bevy::prelude::*; -use bevy_ggrs::prelude::*; +use bevy_ggrs::{LocalPlayers, prelude::*}; -use super::{GameState, components::Team, online::network_role::NetworkRole}; +use super::{ + GameState, + components::{Count, Team}, +}; + +pub const FIELD_WIDTH: i32 = 10; +pub const FIELD_HEIGHT: i32 = 10; +pub const CELL_SIZE: f32 = 50.; +pub const CELL_THICKNESS: f32 = 5.; pub struct FieldPlugin; @@ -33,44 +41,40 @@ pub struct Cell { #[derive(Event)] pub struct CellClicked { pub cell: Entity, + pub team: Team, } -fn rotate(mut camera: Single<&mut Transform, With>, role: Res) { - if matches!(*role, NetworkRole::Client) { - return; +fn rotate(mut camera: Single<&mut Transform, With>, local_players: Res) { + if local_players.0.first().map(|x| *x == 1).unwrap_or(false) { + camera.rotate_z(std::f32::consts::PI); } - camera.rotate_z(std::f32::consts::PI); } fn setup_field(mut commands: Commands) { - let cell_size = 50.; - let cell_thickness = 5.; - let field_width = 10; - let field_height = 10; - // spawn cells for i in 0..2 { - for x in -field_width / 2..field_width / 2 { - for y in 0..field_height { + for x in -FIELD_WIDTH / 2..FIELD_WIDTH / 2 { + for y in 0..FIELD_HEIGHT { + let team = Team(i); commands .spawn(( Cell { - half_size: Vec2::splat(cell_size / 2.), + half_size: Vec2::splat(CELL_SIZE / 2.), }, - Team(i), + team, Sprite::from_color( - Color::hsl(180. * i as f32, 0.6, 0.7), - Vec2::splat(cell_size), + Color::hsl(team.hue(), 0.6, 0.7), + Vec2::splat(CELL_SIZE), ), Transform::from_xyz( - (x as f32 + 0.5) * cell_size, - ((1. - 2. * i as f32) * (y as f32 + 0.5)) * cell_size, + (x as f32 + 0.5) * CELL_SIZE, + ((2. * i as f32 - 1.) * (y as f32 + 0.5)) * CELL_SIZE, 5., ), children![( Sprite::from_color( - Color::hsl(180. * i as f32, 0.8, 0.7), - Vec2::splat(cell_size - cell_thickness) + Color::hsl(team.hue(), 0.8, 0.7), + Vec2::splat(CELL_SIZE - CELL_THICKNESS) ), Transform::IDENTITY, )], @@ -82,8 +86,8 @@ fn setup_field(mut commands: Commands) { // spawn walls let wall_thickness = 1000.; - let wall_width = field_width as f32 * cell_size; - let wall_height = field_height as f32 * cell_size * 2.; + let wall_width = FIELD_WIDTH as f32 * CELL_SIZE; + let wall_height = FIELD_HEIGHT as f32 * CELL_SIZE * 2.; let half_size = Vec2::new(wall_width, wall_thickness) / 2.; @@ -129,10 +133,17 @@ fn setup_field(mut commands: Commands) { pub fn toggle_cell( mut q_cell: Query<&mut Team, With>, mut q_click: EventReader, + mut count: Local, ) { for event in q_click.read() { if let Ok(mut team) = q_cell.get_mut(event.cell) { - team.0 = 1 - **team; + if 10 <= count.0 { + *team = Team::ITEM; + count.0 = 0; + } else { + *team = event.team; + } + count.0 += 1; } } } @@ -143,10 +154,10 @@ fn update_cell_color( mut q_child: Query<&mut Sprite, Without>, ) { for (children, team, mut sprite) in q_cell { - sprite.color = Color::hsl(180. * team.0 as f32, 0.6, 0.7); + sprite.color = Color::hsl(team.hue(), 0.6, 0.7); for child in children { if let Ok(mut sprite) = q_child.get_mut(*child) { - sprite.color = Color::hsl(180. * team.0 as f32, 0.8, 0.7); + sprite.color = Color::hsl(team.hue(), 0.8, 0.7); } } } diff --git a/src/game/item/mod.rs b/src/game/item/mod.rs new file mode 100644 index 0000000..1c29adc --- /dev/null +++ b/src/game/item/mod.rs @@ -0,0 +1,193 @@ +use bevy::{ + color::palettes::css, + math::bounding::{Aabb2d, IntersectsVolume}, + prelude::*, +}; +use bevy_ggrs::prelude::*; + +use super::{ + GameState, + ball::{BALL_RADIUS, Ball, Velocity}, + components::{Count, Team}, + field::{CELL_SIZE, Cell, CellClicked, FIELD_WIDTH, toggle_cell}, + paddle::{PADDLE_HEIGHT, Paddle}, +}; + +const ITEM_FALL_SPEED: f32 = 150.0; +const MULTI_BALL_COUNT: i32 = 2; +const MAX_BALL_SPEED: f32 = 60000000.0; +const MAX_BALL_COUNT: usize = 20; +const SPEED_UP_MULTIPLIER: f32 = 1.2; +const ENLARGE_PADDLE_MULTIPLIER: f32 = 1.5; +const ITEM_SIZE: f32 = 20.0; + +pub struct ItemPlugin; + +impl Plugin for ItemPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + GgrsSchedule, + ( + spawn_item, + move_items, + check_paddle_collision, + apply_item_effect, + ) + .chain() + .before(toggle_cell) + .run_if(in_state(GameState::InGame)), + ) + .rollback_component_with_copy::() + .add_event::(); + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ItemType { + EnlargePaddle, + SpeedUp, + MultiBall, +} + +#[derive(Component, Clone, Copy, Debug)] +pub struct Item { + item_type: ItemType, +} + +#[derive(Event)] +struct ItemCollected { + team: Team, + item_type: ItemType, +} + +pub fn spawn_item( + mut commands: Commands, + mut ev: EventReader, + q_cell: Query<(&Transform, &Team), With>, + mut count: Local, +) { + for ev in ev.read() { + let Ok((cell_transform, cell_team)) = q_cell.get(ev.cell) else { + continue; + }; + if *cell_team != Team::ITEM { + continue; + } + let item_type = match count.0 % 3 { + 0 => ItemType::EnlargePaddle, + 1 => ItemType::SpeedUp, + _ => ItemType::MultiBall, + }; + count.0 += 1; + + commands + .spawn(( + Item { item_type }, + ev.team, + Sprite::from_color(css::YELLOW, Vec2::splat(ITEM_SIZE)), + Transform::from_translation(cell_transform.translation), + )) + .add_rollback(); + } +} + +fn move_items(q_items: Query<(&Team, &mut Transform), With>, time: Res