pub mod config; mod events; mod machinery; mod objects; mod physics; mod resources; mod widgets; use crate::config::{ ASTEROID_SMALL_COLOR, BACKGROUND_COLOR, BULLET_COLOR, BULLET_LIFETIME, BULLET_SPEED, PLAYER_SHIP_COLOR, SHIP_ROTATION, SHIP_THRUST, SHIP_THRUSTER_COLOR_ACTIVE, SHIP_THRUSTER_COLOR_INACTIVE, WINDOW_SIZE, }; use crate::machinery::AsteroidSpawner; use crate::objects::{Bullet, Ship}; use crate::physics::AngularVelocity; use bevy::prelude::*; use bevy_rapier2d::{ plugin::{NoUserData, RapierPhysicsPlugin}, prelude::*, render::RapierDebugRenderPlugin, }; use machinery::Lifetime; use resources::{GameAssets, Lives, Score, WorldSize}; pub struct AsteroidPlugin; impl Plugin for AsteroidPlugin { fn build(&self, app: &mut App) { app.add_plugins(( widgets::GameMenuPlugin, widgets::preparation_widget_plugin, RapierPhysicsPlugin::::pixels_per_meter(10.0), RapierDebugRenderPlugin::default(), )) .insert_resource(ClearColor(BACKGROUND_COLOR)) .insert_resource(WorldSize { width: WINDOW_SIZE.x, height: WINDOW_SIZE.y, }) .insert_resource(Lives(3)) .register_type::() .insert_resource(Score(0)) .insert_resource(AsteroidSpawner::new()) .init_resource::() .add_systems(Startup, spawn_camera) .add_systems( OnEnter(GameState::Playing), (objects::spawn_player, spawn_ui), ) .add_systems( FixedUpdate, ( input_ship_thruster, input_ship_rotation, input_ship_shoot, physics::wrap_entities, machinery::tick_asteroid_manager, objects::spawn_asteroid.after(machinery::tick_asteroid_manager), objects::split_asteroids, objects::bullet_impact_listener, objects::ship_impact_listener, physics::collision_listener, // TODO: Remove debug printing debug_collision_event_printer, machinery::tick_lifetimes, ) .run_if(in_state(GameState::Playing)), ) .add_systems( FixedPostUpdate, ( physics::integrate_velocity, physics::integrate_angular_velocity, ) .run_if(in_state(GameState::Playing)), ) .add_event::() .add_event::() .add_event::() .add_event::(); app.insert_state(GameState::Playing); } } fn debug_collision_event_printer(mut collision_events: EventReader) { for event in collision_events.read() { dbg!(event); } } #[derive(Clone, Debug, Eq, Hash, PartialEq, States)] pub enum GameState { TitleScreen, // Program is started. Present title screen and await user start GetReady, // Short timer to let the player get ready after pressing start Playing, // Player has started the game. Run the main loop GameOver, // Game has ended. Present game over dialogue and await user restart } fn spawn_camera(mut commands: Commands) { commands.spawn(Camera2d); } /* Checks if "W" is pressed and increases velocity accordingly. */ fn input_ship_thruster( keyboard_input: Res>, mut query: Query<(&mut physics::Velocity, &Transform, &mut Children), With>, mut commands: Commands, game_assets: Res, ) { // TODO: Maybe change for a Single> so this only runs for the one ship // buuut... that would silently do nothing if there are 0 or >1 ships, and // I might want to crash on purpose in that case. let Ok((mut velocity, transform, children)) = query.single_mut() else { let count = query.iter().count(); panic!("There should be exactly one player ship! Instead, there seems to be {count}."); }; let thrusters = children .first() .expect("Couldn't find first child, which should be the thruster"); if keyboard_input.pressed(KeyCode::KeyW) { velocity.0 += (transform.rotation * Vec3::X).xy() * SHIP_THRUST; commands .entity(*thrusters) .insert(MeshMaterial2d(game_assets.thruster_mat_active())); } else { commands .entity(*thrusters) .insert(MeshMaterial2d(game_assets.thruster_mat_inactive())); } } /* Checks if "A" or "D" is pressed and updates the player's Rotation component accordingly Does *not* rotate the graphical widget! (that's done by the `apply_rotation_to_mesh` system) */ fn input_ship_rotation( keyboard_input: Res>, mut query: Query<&mut AngularVelocity, With>, ) { let Ok(mut angular_vel) = query.single_mut() else { let count = query.iter().count(); panic!("There should be exactly one player ship! Instead, there seems to be {count}."); }; if keyboard_input.pressed(KeyCode::KeyA) { angular_vel.0 = SHIP_ROTATION; } else if keyboard_input.pressed(KeyCode::KeyD) { angular_vel.0 = -SHIP_ROTATION; } else { angular_vel.0 = 0.0; } } fn input_ship_shoot( keyboard_input: Res>, ship: Single<(&Transform, &physics::Velocity), With>, game_assets: Res, mut commands: Commands, ) { let (ship_pos, ship_vel) = *ship; // Derive bullet velocity, add to the ship's velocity let bullet_vel = (ship_pos.rotation * Vec3::X).xy() * BULLET_SPEED; let bullet_vel = bullet_vel + ship_vel.0; // TODO: create a timer for the gun fire rate. // For now, spawn one for each press of the spacebar. if keyboard_input.just_pressed(KeyCode::Space) { commands.spawn(( Bullet, Collider::ball(0.2), Sensor, ActiveEvents::COLLISION_EVENTS, ActiveCollisionTypes::STATIC_STATIC, physics::Velocity(bullet_vel), Mesh2d(game_assets.bullet().0), MeshMaterial2d(game_assets.bullet().1), ship_pos.clone(), // clone ship transform Lifetime(Timer::from_seconds(BULLET_LIFETIME, TimerMode::Once)), )); } } fn spawn_ui(mut commands: Commands, score: Res, lives: Res) { commands.spawn(( Text::new(format!("Score: {score:?} | Lives: {lives:?}")), TextFont::from_font_size(25.0), )); }