diff --git a/assets/bases/t-2.png b/assets/bases/t-2.png new file mode 100644 index 0000000..f245442 Binary files /dev/null and b/assets/bases/t-2.png differ diff --git a/assets/bases/t-3.png b/assets/bases/t-3.png new file mode 100644 index 0000000..47a872f Binary files /dev/null and b/assets/bases/t-3.png differ diff --git a/assets/bases/t-4.png b/assets/bases/t-4.png new file mode 100644 index 0000000..35359a8 Binary files /dev/null and b/assets/bases/t-4.png differ diff --git a/assets/bases/t-7.png b/assets/bases/t-7.png new file mode 100644 index 0000000..9494c57 Binary files /dev/null and b/assets/bases/t-7.png differ diff --git a/assets/bases/t-9.png b/assets/bases/t-9.png new file mode 100644 index 0000000..4ea6e7b Binary files /dev/null and b/assets/bases/t-9.png differ diff --git a/assets/parallax/00_sky.png b/assets/parallax/00_sky.png new file mode 100644 index 0000000..fb5e109 Binary files /dev/null and b/assets/parallax/00_sky.png differ diff --git a/assets/parallax/01_far_city.png b/assets/parallax/01_far_city.png new file mode 100644 index 0000000..94195a7 Binary files /dev/null and b/assets/parallax/01_far_city.png differ diff --git a/assets/parallax/02_middle.png b/assets/parallax/02_middle.png new file mode 100644 index 0000000..c37b680 Binary files /dev/null and b/assets/parallax/02_middle.png differ diff --git a/assets/parallax/03_main.png b/assets/parallax/03_main.png new file mode 100644 index 0000000..65ed672 Binary files /dev/null and b/assets/parallax/03_main.png differ diff --git a/assets/parallax/04_street.png b/assets/parallax/04_street.png new file mode 100644 index 0000000..bd9e6ab Binary files /dev/null and b/assets/parallax/04_street.png differ diff --git a/assets/parallax/05_foreground.png b/assets/parallax/05_foreground.png new file mode 100644 index 0000000..56154ca Binary files /dev/null and b/assets/parallax/05_foreground.png differ diff --git a/src/camera_movement.rs b/src/camera_movement.rs index 209c7e7..8c1cb75 100644 --- a/src/camera_movement.rs +++ b/src/camera_movement.rs @@ -25,25 +25,25 @@ pub fn camera_movement_system( Without, )>, ) { - let mut highest = 0.0; + let mut highest = 100.0; for (transform, block, ..) in query.iter_mut() { if transform.translation.y > highest { highest = transform.translation.y; } } - highest += 00.0; + let target_height = highest - 100.0; let increase = 1.0; - if highest > camera_movement.height { + if target_height > camera_movement.height { camera_movement.height += increase; - } else if highest < camera_movement.height - increase { + } else if target_height < camera_movement.height - increase { camera_movement.height -= increase; } for mut transform in camera_query.iter_mut() { - transform.translation.y = camera_movement.height * 0.5 + 25.0; + transform.translation.y = camera_movement.height; // camera should slowly zoom out as we get higher //transform.scale = Vec3::new(1.0, 1.0, 1.0) * (1.0 + camera_movement.height / 1000.0); } diff --git a/src/environment/car.rs b/src/environment/car.rs index 95f942a..f4eaa16 100644 --- a/src/environment/car.rs +++ b/src/environment/car.rs @@ -5,6 +5,7 @@ use rand::{random, thread_rng, Rng}; use crate::block::{DestroyBlockOnContact, BLOCK_COLLISION_GROUP}; use crate::floor::FLOOR_COLLISION_GROUP; use crate::level::{LevelLifecycle, UpdateLevelStats}; +use crate::{CAR_MAX_HEIGHT, CAR_MIN_HEIGHT}; pub struct CarPlugin; @@ -69,7 +70,10 @@ pub fn spawn_car_system( let direction = if random() { 1.0 } else { -1.0 }; - let car_position = Vec2::new(direction * 1000.0, random::() * 40.0 + -50.0); + let car_position = Vec2::new( + direction * 1000.0, + thread_rng().gen_range(CAR_MIN_HEIGHT..CAR_MAX_HEIGHT), + ); let car_velocity = Vec2::new(direction * -thread_rng().gen_range(70.0..100.0), 0.0); diff --git a/src/environment/city.rs b/src/environment/city.rs index d3b94ff..e1d8686 100644 --- a/src/environment/city.rs +++ b/src/environment/city.rs @@ -1,43 +1,174 @@ -use crate::state::LevelState; -use crate::VERTICAL_VIEWPORT_SIZE; use bevy::prelude::*; +use bevy::sprite::Anchor; + +use crate::state::LevelState; +use crate::{MainCamera, FLOOR_HEIGHT, VERTICAL_VIEWPORT_SIZE}; pub struct CityPlugin; impl Plugin for CityPlugin { fn build(&self, app: &mut App) { - app.add_systems(OnEnter(LevelState::Playing), (setup_city)); + app.add_systems(OnEnter(LevelState::Playing), (setup_city)) + .add_systems(Update, update_auto_width); } } +#[derive(Component)] +pub struct AutoWidth { + aspect_ratio: f32, + open_top: bool, + parallax: f32, +} + pub fn setup_city(mut commands: Commands, assets: Res) { - commands.spawn( - (SpriteBundle { - transform: Transform::from_xyz(0.0, 20.0, 100.0), - texture: assets.load("foreground.png"), + let asset_size = Vec2::new(3840.0, 4634.0); + + let aspect_ratio = asset_size.y / asset_size.x; + + commands.spawn(( + AutoWidth { + aspect_ratio, + open_top: true, + parallax: 0.5, + }, + SpriteBundle { + transform: Transform::from_xyz(0.0, 0.0, 100.0), + texture: assets.load("parallax/05_foreground.png"), + sprite: Sprite { + custom_size: Some(Vec2::new(1.0, aspect_ratio)), + anchor: Anchor::BottomCenter, + ..Default::default() + }, + ..Default::default() + }, + )); + + commands.spawn(( + AutoWidth { + aspect_ratio: 691.0 / 3840.0, + open_top: true, + parallax: 0.0, + }, + SpriteBundle { + transform: Transform::from_xyz(0.0, 0.0, -5.0), + texture: assets.load("parallax/04_street.png"), + sprite: Sprite { + custom_size: Some(Vec2::new(1.0, 691.0 / 3840.0)), + anchor: Anchor::BottomCenter, + ..Default::default() + }, + ..Default::default() + }, + )); + commands.spawn(( + AutoWidth { + aspect_ratio: 818.0 / 3840.0, + open_top: true, + parallax: 0.0, + }, + SpriteBundle { + transform: Transform::from_xyz(0.0, 0.0, -10.0), + texture: assets.load("parallax/03_main.png"), sprite: Sprite { - custom_size: Some(Vec2::new( - VERTICAL_VIEWPORT_SIZE * 16.0 / 9.0, - VERTICAL_VIEWPORT_SIZE, - )), + custom_size: Some(Vec2::new(1.0, 818.0 / 3840.0)), + anchor: Anchor::BottomCenter, ..Default::default() }, ..Default::default() - }), - ); + }, + )); + commands.spawn(( + AutoWidth { + aspect_ratio, + open_top: true, + parallax: -0.2, + }, + SpriteBundle { + transform: Transform::from_xyz(0.0, 0.0, -20.0), + texture: assets.load("parallax/02_middle.png"), + sprite: Sprite { + custom_size: Some(Vec2::new(1.0, aspect_ratio)), + anchor: Anchor::BottomCenter, + ..Default::default() + }, + ..Default::default() + }, + )); + commands.spawn(( + AutoWidth { + aspect_ratio, + open_top: true, + parallax: -0.4, + }, + SpriteBundle { + transform: Transform::from_xyz(0.0, 0.0, -30.0), + texture: assets.load("parallax/01_far_city.png"), + sprite: Sprite { + custom_size: Some(Vec2::new(1.0, aspect_ratio)), + anchor: Anchor::BottomCenter, + ..Default::default() + }, + ..Default::default() + }, + )); - commands.spawn( - (SpriteBundle { + commands.spawn(( + AutoWidth { + aspect_ratio, + open_top: false, + parallax: -0.5, + }, + SpriteBundle { transform: Transform::from_xyz(0.0, 0.0, -100.0), - texture: assets.load("background.png"), + texture: assets.load("parallax/00_sky.png"), sprite: Sprite { - custom_size: Some(Vec2::new( - VERTICAL_VIEWPORT_SIZE * 16.0 / 9.0, - VERTICAL_VIEWPORT_SIZE, - )), + custom_size: Some(Vec2::new(1.0, aspect_ratio)), + anchor: Anchor::BottomCenter, ..Default::default() }, ..Default::default() - }), - ); + }, + )); +} + +pub fn update_auto_width( + mut query: Query<(&AutoWidth, &mut Transform)>, + transform: Query<(&GlobalTransform, &Camera), With>, +) { + let (camera_transform, camera) = transform.single(); + let viewport = camera.logical_viewport_size().unwrap(); + let top_left = camera + .viewport_to_world_2d( + camera_transform, + Vec2::new(-viewport.x / 2.0, viewport.y / 2.0), + ) + .unwrap(); + let bottom_right = camera + .viewport_to_world_2d( + camera_transform, + Vec2::new(viewport.x / 2.0, -viewport.y / 2.0), + ) + .unwrap(); + + let world_width = bottom_right.x - top_left.x; + let world_height = bottom_right.y - top_left.y; + + let world_aspect_ratio = world_width / world_height; + + for (auto_width, mut transform) in &mut query.iter_mut() { + // the images should be scaled so they always fill the screen + + transform.translation.y = + -VERTICAL_VIEWPORT_SIZE / 2.0 - camera_transform.translation().y * auto_width.parallax; + + if world_aspect_ratio > auto_width.aspect_ratio || auto_width.open_top { + // the world is wider than the image + transform.scale.x = world_width; + transform.scale.y = world_width; + } else { + // the world is taller than the image + transform.scale.x = world_height * auto_width.aspect_ratio; + transform.scale.y = world_height * auto_width.aspect_ratio; + } + } } diff --git a/src/floor.rs b/src/floor.rs index b59a38c..deba456 100644 --- a/src/floor.rs +++ b/src/floor.rs @@ -26,11 +26,11 @@ pub fn setup_floor(mut commands: Commands) { commands.spawn(( SpriteBundle { - sprite: Sprite { - custom_size: Some(Vec2::new(width, height)), - color: Color::rgb(0.0, 0.0, 0.0), - ..Default::default() - }, + // sprite: Sprite { + // custom_size: Some(Vec2::new(width, height)), + // color: Color::rgb(0.0, 0.0, 0.0), + // ..Default::default() + // }, transform: Transform::from_xyz(0.0, -height / 2.0 + FLOOR_HEIGHT, 0.0), ..Default::default() }, diff --git a/src/level.rs b/src/level.rs index b938453..7b3e9ac 100644 --- a/src/level.rs +++ b/src/level.rs @@ -55,6 +55,19 @@ pub struct LevelBase { pub rotation: f32, } +#[derive(Debug, Clone)] +pub enum LaunchPlatformKind { + Static, + Vertical, + Free, +} + +#[derive(Debug, Clone)] +pub struct LaunchPlatform { + pub translation: Vec2, + pub kind: LaunchPlatformKind, +} + const fn default_level_base() -> LevelBase { LevelBase { width: 100.0, @@ -72,6 +85,7 @@ pub struct Level { pub bases: &'static [LevelBase], pub enabled_effects: &'static [(EffectType, f32)], pub effect_likelihood: f32, + pub intro_text: &'static str, } pub const DEFAULT_EFFECTS: [(EffectType, f32); 2] = @@ -88,6 +102,7 @@ pub const DEFAULT_LEVEL: Level = Level { }], enabled_effects: &DEFAULT_EFFECTS, effect_likelihood: 0.05, + intro_text: "Welcome to the game!", }; #[derive(Debug, Clone)] @@ -105,6 +120,7 @@ pub struct LevelLifecycle; pub static LEVELS: [Level; 6] = [ Level { level: 0, + intro_text: "Test Level", goal: LevelGoal::ReachHeight(200.0), time_limit: Some(Duration::from_secs(60)), max_blocks: None, @@ -116,6 +132,9 @@ pub static LEVELS: [Level; 6] = [ }, Level { level: 1, + intro_text: "Welcome to your first day at Big Bad Buildings, Inc. Your job is to operate the Tower Thrower 3000, a state-of-the-art machine that constructs buildings by throwing blocks.\ + For your first building, reach a target height of 200m.\ + ", goal: LevelGoal::ReachHeight(200.0), time_limit: Some(Duration::from_secs(60)), max_blocks: None, diff --git a/src/level_intro_dialog.rs b/src/level_intro_dialog.rs new file mode 100644 index 0000000..5ce8284 --- /dev/null +++ b/src/level_intro_dialog.rs @@ -0,0 +1,56 @@ +use crate::level::Level; +use crate::state::LevelState; +use bevy::prelude::*; +use bevy_egui::egui::{Color32, Frame}; +use bevy_egui::{egui, EguiContext, EguiContexts}; + +pub struct LevelIntroDialogPlugin; + +impl Plugin for LevelIntroDialogPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(LevelState::Playing), setup_level_intro_dialog) + .add_systems(Update, update_level_intro_dialog) + .init_resource::(); + } +} + +#[derive(Resource, Default)] +pub struct LevelIntroDialog { + pub visible: bool, +} + +pub fn setup_level_intro_dialog(mut dialog: ResMut) { + dialog.visible = true; +} + +pub fn update_level_intro_dialog( + mut dialog: ResMut, + mut egui: EguiContexts, + level: Res, +) { + if dialog.visible { + egui::Window::new("Level Intro") + .collapsible(false) + .resizable(false) + .title_bar(false) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .frame( + Frame::none() + .fill(Color32::from_rgba_unmultiplied(0, 0, 0, 250)) + .inner_margin(8.0), + ) + .show(egui.ctx_mut(), |ui| { + ui.set_min_size(egui::Vec2::new(400.0, 300.0)); + + ui.heading("Level Intro"); + + ui.label(level.intro_text); + + ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { + if ui.button("START").clicked() { + dialog.visible = false; + } + }); + }); + } +} diff --git a/src/main.rs b/src/main.rs index 5d68448..ec764f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use crate::environment::EnvironmentPlugin; use crate::floor::FloorPlugin; use crate::launch_platform::LaunchPlatformPlugin; use crate::level::LevelPlugin; +use crate::level_intro_dialog::LevelIntroDialogPlugin; use crate::level_ui::LevelUiPlugin; use crate::state::StatePlugin; use crate::target_height_indicator::TargetHeightIndicatorPlugin; @@ -31,6 +32,7 @@ mod environment; mod floor; mod launch_platform; mod level; +mod level_intro_dialog; mod level_ui; mod state; mod target_height_indicator; @@ -47,7 +49,11 @@ pub const GRAVITY: f32 = -9.81 * PIXELS_PER_METER; pub const PHYSICS_DT: f32 = 1.0 / 60.0; pub const VERTICAL_VIEWPORT_SIZE: f32 = 400.0; -pub const FLOOR_HEIGHT: f32 = -100.0; +pub const FLOOR_HEIGHT: f32 = -VERTICAL_VIEWPORT_SIZE / 2.0 + 10.0; + +pub const CAR_MIN_HEIGHT: f32 = FLOOR_HEIGHT + 20.0; + +pub const CAR_MAX_HEIGHT: f32 = FLOOR_HEIGHT / 2.0; fn main() { App::new() @@ -85,6 +91,7 @@ fn main() { TargetHeightIndicatorPlugin, LevelUiPlugin, UiPlugin, + LevelIntroDialogPlugin, ), )) .add_systems(Startup, (setup_graphics, setup_physics)) diff --git a/src/ui.rs b/src/ui.rs index 64dde84..a281043 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,30 +1,19 @@ use bevy::prelude::*; -use bevy_egui::EguiContexts; +use bevy::window::PrimaryWindow; +use bevy_egui::{egui, EguiContexts, EguiSettings}; pub struct UiPlugin; impl Plugin for UiPlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, button_system); + app.add_systems(Update, (update_ui_scale_factor)); app.add_systems( PreUpdate, (absorb_egui_inputs,) .after(bevy_egui::systems::process_input_system) .before(bevy_egui::EguiSet::BeginFrame), ); - } -} - -fn button_system( - mut interaction_query: Query<(&Interaction, &mut BackgroundColor), Changed>, -) { - for (interaction, mut button_color) in interaction_query.iter_mut() { - *button_color = match interaction { - Interaction::Hovered => Color::ORANGE_RED, - Interaction::Pressed => Color::RED, - _ => Color::GRAY, - } - .into(); + app.add_systems(Startup, egui_setup); } } @@ -33,3 +22,26 @@ fn absorb_egui_inputs(mut mouse: ResMut>, mut contexts: EguiC mouse.reset_all(); } } + +fn update_ui_scale_factor( + mut egui_settings: ResMut, + windows: Query<&Window, With>, +) { + if let Ok(window) = windows.get_single() { + egui_settings.scale_factor = 1.25; + } +} + +fn egui_setup(mut egui: EguiContexts) { + let ctx = egui.ctx_mut(); + + ctx.style_mut(|style| { + style.spacing.button_padding = egui::Vec2::new(16.0, 8.0); + + // style + // .text_styles + // .get_mut(&egui::TextStyle::Button) + // .unwrap() + // .size = 24.0; + }); +}