Compare commits

..

No commits in common. "main" and "v1.0.3" have entirely different histories.
main ... v1.0.3

36 changed files with 53 additions and 811 deletions

60
Cargo.lock generated
View File

@ -1409,7 +1409,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash 1.1.0", "rustc-hash",
"shlex", "shlex",
"syn", "syn",
] ]
@ -1813,7 +1813,7 @@ dependencies = [
"log", "log",
"rangemap", "rangemap",
"rayon", "rayon",
"rustc-hash 1.1.0", "rustc-hash",
"rustybuzz", "rustybuzz",
"self_cell", "self_cell",
"swash", "swash",
@ -1925,18 +1925,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deprecate-until"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704"
dependencies = [
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "1.0.0" version = "1.0.0"
@ -2656,16 +2644,15 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "hexlab" name = "hexlab"
version = "0.6.1" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e247b21d6885136e4a910e1e6fac755d8dc56112c40ebf1062582f0644d62c62" checksum = "7d2fbc6c41965686841aa5ea0e1af448730d0902274e49251c7d1fb7c78fffb9"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_reflect", "bevy_reflect",
"bevy_utils", "bevy_utils",
"glam", "glam",
"hexx", "hexx",
"pathfinding",
"rand", "rand",
"thiserror 2.0.6", "thiserror 2.0.6",
] ]
@ -2907,15 +2894,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "integer-sqrt"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "io-kit-sys" name = "io-kit-sys"
version = "0.4.1" version = "0.4.1"
@ -3157,7 +3135,7 @@ dependencies = [
[[package]] [[package]]
name = "maze-ascension" name = "maze-ascension"
version = "1.1.3" version = "1.0.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy", "bevy",
@ -3250,7 +3228,7 @@ dependencies = [
"indexmap", "indexmap",
"log", "log",
"pp-rs", "pp-rs",
"rustc-hash 1.1.0", "rustc-hash",
"spirv", "spirv",
"termcolor", "termcolor",
"thiserror 1.0.69", "thiserror 1.0.69",
@ -3271,7 +3249,7 @@ dependencies = [
"once_cell", "once_cell",
"regex", "regex",
"regex-syntax 0.8.5", "regex-syntax 0.8.5",
"rustc-hash 1.1.0", "rustc-hash",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
"unicode-ident", "unicode-ident",
@ -3785,20 +3763,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathfinding"
version = "4.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301ad6aa19104eeb9af172b3d6a4ab8a5ea26234890baf2fcb1cbbc3f05f674b"
dependencies = [
"deprecate-until",
"indexmap",
"integer-sqrt",
"num-traits",
"rustc-hash 2.1.0",
"thiserror 2.0.6",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -4220,12 +4184,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -5266,7 +5224,7 @@ dependencies = [
"parking_lot", "parking_lot",
"profiling", "profiling",
"raw-window-handle", "raw-window-handle",
"rustc-hash 1.1.0", "rustc-hash",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wgpu-hal", "wgpu-hal",
@ -5308,7 +5266,7 @@ dependencies = [
"range-alloc", "range-alloc",
"raw-window-handle", "raw-window-handle",
"renderdoc-sys", "renderdoc-sys",
"rustc-hash 1.1.0", "rustc-hash",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wasm-bindgen", "wasm-bindgen",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "maze-ascension" name = "maze-ascension"
authors = ["Kristofers Solo <dev@kristofers.xyz>"] authors = ["Kristofers Solo <dev@kristofers.xyz>"]
version = "1.1.3" version = "1.0.3"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -18,7 +18,7 @@ tracing = { version = "0.1", features = [
"release_max_level_warn", "release_max_level_warn",
] } ] }
hexx = { version = "0.19", features = ["bevy_reflect", "grid"] } hexx = { version = "0.19", features = ["bevy_reflect", "grid"] }
hexlab = { version = "0.6", features = ["bevy", "pathfinding"] } hexlab = { version = "0.5", features = ["bevy"] }
bevy-inspector-egui = { version = "0.28", optional = true } bevy-inspector-egui = { version = "0.28", optional = true }
bevy_egui = { version = "0.31", optional = true } bevy_egui = { version = "0.31", optional = true }
thiserror = "2.0" thiserror = "2.0"

View File

@ -1,64 +0,0 @@
use bevy::{input::mouse::MouseWheel, prelude::*};
use crate::constants::{BASE_ZOOM_SPEED, MAX_ZOOM, MIN_ZOOM, SCROLL_MODIFIER};
pub(super) fn plugin(app: &mut App) {
app.add_systems(Update, camera_zoom);
}
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub struct MainCamera;
pub fn spawn_camera(mut commands: Commands) {
commands.spawn((
Name::new("Camera"),
MainCamera,
Camera3d::default(),
Transform::from_xyz(200., 200., 0.).looking_at(Vec3::ZERO, Vec3::Y),
// Render all UI to this camera.
// Not strictly necessary since we only use one camera,
// but if we don't use this component, our UI will disappear as soon
// as we add another camera. This includes indirect ways of adding cameras like using
// [ui node outlines](https://bevyengine.org/news/bevy-0-14/#ui-node-outline-gizmos)
// for debugging. So it's good to have this here for future-proofing.
IsDefaultUiCamera,
));
}
fn camera_zoom(
mut query: Query<&mut Transform, With<MainCamera>>,
mut scrool_evr: EventReader<MouseWheel>,
keyboard: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
) {
let Ok(mut transform) = query.get_single_mut() else {
return;
};
let current_distance = transform.translation.length();
// Calculate zoom speed based on distance
let distance_multiplier = (current_distance / MIN_ZOOM).sqrt();
let adjusted_zoom_speed = BASE_ZOOM_SPEED * distance_multiplier;
let mut zoom_delta = 0.0;
if keyboard.pressed(KeyCode::Equal) || keyboard.pressed(KeyCode::NumpadAdd) {
zoom_delta += adjusted_zoom_speed * time.delta_secs() * 25.;
}
if keyboard.pressed(KeyCode::Minus) || keyboard.pressed(KeyCode::NumpadSubtract) {
zoom_delta -= adjusted_zoom_speed * time.delta_secs() * 25.;
}
for ev in scrool_evr.read() {
zoom_delta += ev.y * adjusted_zoom_speed * SCROLL_MODIFIER;
}
if zoom_delta != 0.0 {
let forward = transform.translation.normalize();
let new_distance = (current_distance - zoom_delta).clamp(MIN_ZOOM, MAX_ZOOM);
transform.translation = forward * new_distance;
}
}

View File

@ -3,24 +3,3 @@ pub const WALL_OVERLAP_MODIFIER: f32 = 1.25;
pub const FLOOR_Y_OFFSET: u8 = 200; pub const FLOOR_Y_OFFSET: u8 = 200;
pub const MOVEMENT_COOLDOWN: f32 = 1.0; // one second cooldown pub const MOVEMENT_COOLDOWN: f32 = 1.0; // one second cooldown
pub const TITLE: &str = "Maze Ascension: The Labyrinth of Echoes"; pub const TITLE: &str = "Maze Ascension: The Labyrinth of Echoes";
// Base score constants
pub const BASE_FLOOR_SCORE: usize = 100;
// Floor progression constants
pub const FLOOR_PROGRESSION_MULTIPLIER: f32 = 1.2;
pub const MIN_TIME_MULTIPLIER: f32 = 0.2; // Minimum score multiplier for time
pub const TIME_BONUS_MULTIPLIER: f32 = 1.5;
// Time scaling constants
pub const BASE_PERFECT_TIME: f32 = 10.0; // Base time for floor 1
pub const TIME_INCREASE_FACTOR: f32 = 0.15; // Each floor adds 15% more time
// Constants for camera control
pub const BASE_ZOOM_SPEED: f32 = 10.0;
#[cfg(not(target_family = "wasm"))]
pub const SCROLL_MODIFIER: f32 = 1.;
#[cfg(target_family = "wasm")]
pub const SCROLL_MODIFIER: f32 = 0.01;
pub const MIN_ZOOM: f32 = 50.0;
pub const MAX_ZOOM: f32 = 2500.0;

View File

@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
#[derive(Debug, Reflect, Component, Deref, DerefMut, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Reflect, Component, Deref, DerefMut)]
#[reflect(Component)] #[reflect(Component)]
pub struct Floor(pub u8); pub struct Floor(pub u8);

View File

@ -1,19 +0,0 @@
use bevy::prelude::*;
use crate::floor::components::{CurrentFloor, Floor};
pub fn hide_upper_floors(
mut query: Query<(&mut Visibility, &Floor)>,
current_query: Query<&Floor, With<CurrentFloor>>,
) {
let Ok(current_floor) = current_query.get_single() else {
return;
};
for (mut visibility, floor) in query.iter_mut() {
if floor > current_floor {
*visibility = Visibility::Hidden
} else {
*visibility = Visibility::Visible
}
}
}

View File

@ -1,12 +1,10 @@
mod despawn; mod despawn;
mod hide;
mod movement; mod movement;
mod spawn; mod spawn;
use crate::screens::Screen; use crate::screens::Screen;
use bevy::prelude::*; use bevy::prelude::*;
use despawn::despawn_floor; use despawn::despawn_floor;
use hide::hide_upper_floors;
use movement::{handle_floor_transition_events, move_floors}; use movement::{handle_floor_transition_events, move_floors};
use spawn::spawn_floor; use spawn::spawn_floor;
@ -18,7 +16,6 @@ pub(super) fn plugin(app: &mut App) {
despawn_floor, despawn_floor,
handle_floor_transition_events, handle_floor_transition_events,
move_floors, move_floors,
hide_upper_floors,
) )
.chain() .chain()
.run_if(in_state(Screen::Gameplay)), .run_if(in_state(Screen::Gameplay)),

View File

@ -74,7 +74,7 @@ pub fn handle_floor_transition_events(
} }
for event in event_reader.read() { for event in event_reader.read() {
let Ok((current_entity, current_floor)) = current_query.get_single() else { let Some((current_entity, current_floor)) = current_query.get_single().ok() else {
continue; continue;
}; };

View File

@ -3,11 +3,9 @@ pub mod components;
mod systems; mod systems;
use bevy::{ecs::system::RunSystemOnce, prelude::*}; use bevy::{ecs::system::RunSystemOnce, prelude::*};
use components::IdleTimer;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.register_type::<IdleTimer>() app.add_plugins(systems::plugin);
.add_plugins(systems::plugin);
} }
pub fn spawn_hint_command(world: &mut World) { pub fn spawn_hint_command(world: &mut World) {

View File

@ -1,6 +1,5 @@
pub mod asset_tracking; pub mod asset_tracking;
pub mod audio; pub mod audio;
pub mod camera;
pub mod constants; pub mod constants;
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub mod dev_tools; pub mod dev_tools;
@ -9,7 +8,6 @@ pub mod hint;
pub mod maze; pub mod maze;
pub mod player; pub mod player;
pub mod screens; pub mod screens;
pub mod stats;
pub mod theme; pub mod theme;
use bevy::{ use bevy::{
@ -17,7 +15,6 @@ use bevy::{
audio::{AudioPlugin, Volume}, audio::{AudioPlugin, Volume},
prelude::*, prelude::*,
}; };
use camera::spawn_camera;
use constants::TITLE; use constants::TITLE;
use theme::{palette::rose_pine, prelude::ColorScheme}; use theme::{palette::rose_pine, prelude::ColorScheme};
@ -72,8 +69,6 @@ impl Plugin for AppPlugin {
floor::plugin, floor::plugin,
player::plugin, player::plugin,
hint::plugin, hint::plugin,
stats::plugin,
camera::plugin,
)); ));
// Enable dev tools for dev builds. // Enable dev tools for dev builds.
@ -95,6 +90,21 @@ enum AppSet {
Update, Update,
} }
fn spawn_camera(mut commands: Commands) {
commands.spawn((
Name::new("Camera"),
Camera3d::default(),
Transform::from_xyz(200., 200., 0.).looking_at(Vec3::ZERO, Vec3::Y),
// Render all UI to this camera.
// Not strictly necessary since we only use one camera,
// but if we don't use this component, our UI will disappear as soon
// as we add another camera. This includes indirect ways of adding cameras like using
// [ui node outlines](https://bevyengine.org/news/bevy-0-14/#ui-node-outline-gizmos)
// for debugging. So it's good to have this here for future-proofing.
IsDefaultUiCamera,
));
}
fn load_background(mut commands: Commands) { fn load_background(mut commands: Commands) {
let colorcheme = rose_pine::RosePineDawn::Base; let colorcheme = rose_pine::RosePineDawn::Base;
commands.insert_resource(ClearColor(colorcheme.to_color())); commands.insert_resource(ClearColor(colorcheme.to_color()));

View File

@ -95,6 +95,14 @@ impl MazeConfig {
} }
} }
// TO
// 3928551514041614914
// (4, 0)
// FROM
// 7365371276044996661
// ()
impl Default for MazeConfig { impl Default for MazeConfig {
fn default() -> Self { fn default() -> Self {
Self::new( Self::new(

View File

@ -2,13 +2,7 @@ pub mod common;
pub mod despawn; pub mod despawn;
pub mod respawn; pub mod respawn;
pub mod spawn; pub mod spawn;
mod toggle_pause;
use bevy::prelude::*; use bevy::prelude::*;
use toggle_pause::toggle_walls;
use crate::screens::Screen; pub(super) fn plugin(_app: &mut App) {}
pub(super) fn plugin(app: &mut App) {
app.add_systems(Update, toggle_walls.run_if(state_changed::<Screen>));
}

View File

@ -15,7 +15,7 @@ use crate::{
components::{HexMaze, MazeConfig, Tile, Wall}, components::{HexMaze, MazeConfig, Tile, Wall},
resources::GlobalMazeConfig, resources::GlobalMazeConfig,
}, },
screens::GameplayElement, screens::Screen,
theme::palette::rose_pine::RosePineDawn, theme::palette::rose_pine::RosePineDawn,
}; };
@ -62,7 +62,7 @@ pub fn spawn_maze(
config.clone(), config.clone(),
Transform::from_translation(Vec3::ZERO.with_y(y_offset)), Transform::from_translation(Vec3::ZERO.with_y(y_offset)),
Visibility::Visible, Visibility::Visible,
GameplayElement, StateScoped(Screen::Gameplay),
)) ))
.insert_if(CurrentFloor, || floor == 1) // Only floor 1 gets CurrentFloor .insert_if(CurrentFloor, || floor == 1) // Only floor 1 gets CurrentFloor
.id(); .id();

View File

@ -1,13 +0,0 @@
use bevy::prelude::*;
use crate::{maze::components::Wall, screens::Screen};
pub fn toggle_walls(mut query: Query<&mut Visibility, With<Wall>>, state: Res<State<Screen>>) {
for mut visibility in query.iter_mut() {
*visibility = match *state.get() {
Screen::Gameplay => Visibility::Inherited,
Screen::Pause => Visibility::Hidden,
_ => *visibility,
}
}
}

View File

@ -16,7 +16,7 @@ pub struct MovementSpeed(pub f32);
impl Default for MovementSpeed { impl Default for MovementSpeed {
fn default() -> Self { fn default() -> Self {
Self(100.) Self(200.)
} }
} }

View File

@ -4,7 +4,6 @@ mod movement;
pub mod respawn; pub mod respawn;
mod sound_effect; mod sound_effect;
pub mod spawn; pub mod spawn;
mod toggle_pause;
mod vertical_transition; mod vertical_transition;
use crate::{screens::Screen, AppSet}; use crate::{screens::Screen, AppSet};
@ -12,7 +11,6 @@ use bevy::prelude::*;
use input::player_input; use input::player_input;
use movement::player_movement; use movement::player_movement;
use sound_effect::play_movement_sound; use sound_effect::play_movement_sound;
use toggle_pause::toggle_player;
use vertical_transition::handle_floor_transition; use vertical_transition::handle_floor_transition;
use super::assets::PlayerAssets; use super::assets::PlayerAssets;
@ -32,5 +30,4 @@ pub(super) fn plugin(app: &mut App) {
.chain() .chain()
.run_if(in_state(Screen::Gameplay)), .run_if(in_state(Screen::Gameplay)),
); );
app.add_systems(Update, toggle_player.run_if(state_changed::<Screen>));
} }

View File

@ -5,7 +5,7 @@ use crate::{
assets::{blue_material, generate_pill_mesh}, assets::{blue_material, generate_pill_mesh},
components::{CurrentPosition, Player}, components::{CurrentPosition, Player},
}, },
screens::GameplayElement, screens::Screen,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@ -33,6 +33,6 @@ pub fn spawn_player(
Mesh3d(meshes.add(generate_pill_mesh(player_radius, player_height / 2.))), Mesh3d(meshes.add(generate_pill_mesh(player_radius, player_height / 2.))),
MeshMaterial3d(materials.add(blue_material())), MeshMaterial3d(materials.add(blue_material())),
Transform::from_xyz(start_pos.x, y_offset, start_pos.y), Transform::from_xyz(start_pos.x, y_offset, start_pos.y),
GameplayElement, StateScoped(Screen::Gameplay),
)); ));
} }

View File

@ -1,13 +0,0 @@
use bevy::prelude::*;
use crate::{player::components::Player, screens::Screen};
pub fn toggle_player(mut query: Query<&mut Visibility, With<Player>>, state: Res<State<Screen>>) {
for mut visibility in query.iter_mut() {
*visibility = match *state.get() {
Screen::Gameplay => Visibility::Visible,
Screen::Pause => Visibility::Hidden,
_ => *visibility,
}
}
}

View File

@ -1,62 +1,29 @@
//! The screen state for the main gameplay. //! The screen state for the main gameplay.
use crate::{ use crate::player::spawn_player_command;
hint::spawn_hint_command, maze::spawn_level_command, player::spawn_player_command, use crate::screens::Screen;
screens::Screen, stats::spawn_stats_command, use crate::{hint::spawn_hint_command, maze::spawn_level_command};
};
use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use bevy::{input::common_conditions::input_just_pressed, prelude::*};
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_resource::<GameplayInitialized>();
app.add_systems( app.add_systems(
OnEnter(Screen::Gameplay), OnEnter(Screen::Gameplay),
( (
spawn_level_command, spawn_level_command,
spawn_player_command, spawn_player_command,
spawn_hint_command, spawn_hint_command,
spawn_stats_command,
) )
.chain() .chain(),
.run_if(not(resource_exists::<GameplayInitialized>)),
); );
app.add_systems(OnEnter(Screen::Gameplay), |mut commands: Commands| {
commands.insert_resource(GameplayInitialized(true));
});
app.add_systems(Update, cleanup_game.run_if(state_changed::<Screen>));
app.add_systems(OnEnter(Screen::Title), reset_gameplay_state);
app.add_systems( app.add_systems(
Update, Update,
pause_game.run_if(in_state(Screen::Gameplay).and(input_just_pressed(KeyCode::Escape))), return_to_title_screen
.run_if(in_state(Screen::Gameplay).and(input_just_pressed(KeyCode::Escape))),
); );
} }
fn pause_game(mut next_screen: ResMut<NextState<Screen>>) { fn return_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Pause); next_screen.set(Screen::Title);
}
fn reset_gameplay_state(mut commands: Commands) {
commands.remove_resource::<GameplayInitialized>();
}
#[derive(Debug, Default, Reflect, Resource, DerefMut, Deref)]
#[reflect(Resource)]
pub struct GameplayInitialized(bool);
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub struct GameplayElement;
fn cleanup_game(
mut commands: Commands,
query: Query<Entity, With<GameplayElement>>,
state: Res<State<Screen>>,
) {
if !matches!(*state.get(), Screen::Gameplay | Screen::Pause) {
for entity in query.iter() {
commands.entity(entity).despawn_recursive();
}
}
} }

View File

@ -2,12 +2,10 @@
mod gameplay; mod gameplay;
mod loading; mod loading;
mod pause;
mod splash; mod splash;
mod title; mod title;
use bevy::prelude::*; use bevy::prelude::*;
pub use gameplay::{GameplayElement, GameplayInitialized};
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_state::<Screen>(); app.init_state::<Screen>();
@ -18,7 +16,6 @@ pub(super) fn plugin(app: &mut App) {
loading::plugin, loading::plugin,
splash::plugin, splash::plugin,
title::plugin, title::plugin,
pause::plugin,
)); ));
} }
@ -31,5 +28,4 @@ pub enum Screen {
Loading, Loading,
Title, Title,
Gameplay, Gameplay,
Pause,
} }

View File

@ -1,57 +0,0 @@
use bevy::{input::common_conditions::input_just_pressed, prelude::*};
use crate::theme::{
events::OnPress,
palette::rose_pine::RosePineDawn,
prelude::ColorScheme,
widgets::{Containers, Widgets},
};
use super::Screen;
pub(super) fn plugin(app: &mut App) {
app.add_systems(OnEnter(Screen::Pause), spawn_pause_overlay);
app.add_systems(
Update,
return_to_game.run_if(in_state(Screen::Pause).and(input_just_pressed(KeyCode::Escape))),
);
}
fn spawn_pause_overlay(mut commands: Commands) {
commands
.ui_root()
.insert((
StateScoped(Screen::Pause),
BackgroundColor(RosePineDawn::Muted.to_color().with_alpha(0.5)),
))
.with_children(|parent| {
parent
.spawn(Node {
bottom: Val::Px(100.),
..default()
})
.with_children(|parent| {
parent.header("Paused");
});
parent.button("Continue").observe(return_to_game_trigger);
parent
.button("Exit")
.observe(return_to_title_screen_trigger);
});
}
fn return_to_game_trigger(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Gameplay);
}
fn return_to_title_screen_trigger(
_trigger: Trigger<OnPress>,
mut next_screen: ResMut<NextState<Screen>>,
) {
next_screen.set(Screen::Title);
}
fn return_to_game(mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Gameplay);
}

View File

@ -1,21 +0,0 @@
use bevy::prelude::*;
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
pub struct FloorDisplay;
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
pub struct HighestFloorDisplay;
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
pub struct ScoreDisplay;
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
pub struct FloorTimerDisplay;
#[derive(Debug, Clone, Reflect, Component)]
#[reflect(Component)]
pub struct TotalTimerDisplay;

View File

@ -1,22 +0,0 @@
use bevy::prelude::*;
pub trait StatsContainer {
fn ui_stats(&mut self) -> EntityCommands;
}
impl StatsContainer for Commands<'_, '_> {
fn ui_stats(&mut self) -> EntityCommands {
self.spawn((
Name::new("Stats Root"),
Node {
position_type: PositionType::Absolute,
top: Val::Px(10.),
right: Val::Px(10.),
row_gap: Val::Px(8.),
align_items: AlignItems::End,
flex_direction: FlexDirection::Column,
..default()
},
))
}
}

View File

@ -1,18 +0,0 @@
pub mod components;
pub mod container;
pub mod resources;
mod systems;
use bevy::{ecs::system::RunSystemOnce, prelude::*};
use resources::{FloorTimer, Score, TotalTimer};
pub(super) fn plugin(app: &mut App) {
app.init_resource::<Score>()
.init_resource::<TotalTimer>()
.init_resource::<FloorTimer>()
.add_plugins(systems::plugin);
}
pub fn spawn_stats_command(world: &mut World) {
let _ = world.run_system_once(systems::spawn::spawn_stats);
}

View File

@ -1,31 +0,0 @@
use std::time::Duration;
use bevy::prelude::*;
#[derive(Debug, Default, Reflect, Resource, Deref, DerefMut)]
#[reflect(Resource)]
pub struct Score(pub usize);
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
#[reflect(Resource)]
pub struct TotalTimer(pub Timer);
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
#[reflect(Resource)]
pub struct FloorTimer(pub Timer);
impl Default for TotalTimer {
fn default() -> Self {
Self(init_timer())
}
}
impl Default for FloorTimer {
fn default() -> Self {
Self(init_timer())
}
}
fn init_timer() -> Timer {
Timer::new(Duration::MAX, TimerMode::Once)
}

View File

@ -1,28 +0,0 @@
pub fn format_duration_adaptive(seconds: f32) -> String {
let total_millis = (seconds * 1000.0) as u64;
let millis = total_millis % 1000;
let total_seconds = total_millis / 1000;
let seconds = total_seconds % 60;
let total_minutes = total_seconds / 60;
let minutes = total_minutes % 60;
let total_hours = total_minutes / 60;
let hours = total_hours % 24;
let days = total_hours / 24;
let mut result = String::new();
if days > 0 {
result.push_str(&format!("{}d ", days));
}
if hours > 0 || days > 0 {
result.push_str(&format!("{:02}:", hours));
}
if minutes > 0 || hours > 0 || days > 0 {
result.push_str(&format!("{:02}:", minutes));
}
// Always show at least seconds and milliseconds
result.push_str(&format!("{:02}.{:03}", seconds, millis));
result
}

View File

@ -1,39 +0,0 @@
use bevy::prelude::*;
use crate::{
floor::{
components::{CurrentFloor, Floor},
resources::HighestFloor,
},
stats::components::{FloorDisplay, HighestFloorDisplay},
};
pub fn update_floor_display(
floor_query: Query<&Floor, With<CurrentFloor>>,
mut text_query: Query<&mut Text, With<FloorDisplay>>,
) {
let Ok(floor) = floor_query.get_single() else {
return;
};
let Ok(mut text) = text_query.get_single_mut() else {
return;
};
text.0 = format!("Floor: {}", floor.0);
}
pub fn update_highest_floor_display(
hightes_floor: Res<HighestFloor>,
mut text_query: Query<&mut Text, With<HighestFloorDisplay>>,
) {
if !hightes_floor.is_changed() {
return;
}
let Ok(mut text) = text_query.get_single_mut() else {
return;
};
text.0 = format!("Highest Floor: {}", hightes_floor.0);
}

View File

@ -1,33 +0,0 @@
use bevy::prelude::*;
use crate::{
floor::resources::HighestFloor,
stats::{components::FloorTimerDisplay, resources::FloorTimer},
};
use super::common::format_duration_adaptive;
pub fn update_floor_timer(
mut floor_timer: ResMut<FloorTimer>,
time: Res<Time>,
hightes_floor: Res<HighestFloor>,
) {
floor_timer.tick(time.delta());
if hightes_floor.is_changed() {
floor_timer.0.reset();
}
}
pub fn update_floor_timer_display(
mut text_query: Query<&mut Text, With<FloorTimerDisplay>>,
floor_timer: Res<FloorTimer>,
) {
let Ok(mut text) = text_query.get_single_mut() else {
return;
};
text.0 = format!(
"Floor Timer: {}",
format_duration_adaptive(floor_timer.0.elapsed_secs())
);
}

View File

@ -1,38 +0,0 @@
mod common;
mod floor;
mod floor_timer;
mod reset;
mod score;
pub mod spawn;
mod total_timer;
use bevy::prelude::*;
use floor::{update_floor_display, update_highest_floor_display};
use floor_timer::{update_floor_timer, update_floor_timer_display};
use reset::reset_timers;
use score::{update_score, update_score_display};
use total_timer::{update_total_timer, update_total_timer_display};
use crate::screens::{GameplayInitialized, Screen};
pub(super) fn plugin(app: &mut App) {
app.add_systems(
OnEnter(Screen::Gameplay),
reset_timers.run_if(not(resource_exists::<GameplayInitialized>)),
);
app.add_systems(
Update,
(
(
update_score.before(update_floor_timer),
update_score_display,
)
.chain(),
(update_floor_timer, update_floor_timer_display).chain(),
(update_total_timer, update_total_timer_display).chain(),
update_floor_display,
update_highest_floor_display,
)
.run_if(in_state(Screen::Gameplay)),
);
}

View File

@ -1,13 +0,0 @@
use bevy::prelude::*;
use crate::stats::resources::{FloorTimer, Score, TotalTimer};
pub fn reset_timers(
mut floor_timer: ResMut<FloorTimer>,
mut total_timer: ResMut<TotalTimer>,
mut score: ResMut<Score>,
) {
floor_timer.reset();
total_timer.reset();
score.0 = 0;
}

View File

@ -1,188 +0,0 @@
use bevy::prelude::*;
use crate::{
constants::{
BASE_FLOOR_SCORE, BASE_PERFECT_TIME, FLOOR_PROGRESSION_MULTIPLIER, MIN_TIME_MULTIPLIER,
TIME_BONUS_MULTIPLIER, TIME_INCREASE_FACTOR,
},
floor::resources::HighestFloor,
stats::{
components::ScoreDisplay,
resources::{FloorTimer, Score},
},
};
pub fn update_score(
mut score: ResMut<Score>,
hightes_floor: Res<HighestFloor>,
floor_timer: Res<FloorTimer>,
) {
if !hightes_floor.is_changed() || hightes_floor.is_added() {
return;
}
score.0 += calculate_score(
hightes_floor.0.saturating_sub(1),
floor_timer.elapsed_secs(),
);
}
pub fn update_score_display(
mut text_query: Query<&mut Text, With<ScoreDisplay>>,
score: Res<Score>,
) {
let Ok(mut text) = text_query.get_single_mut() else {
return;
};
text.0 = format!("Score: {}", score.0);
}
fn calculate_score(floor_number: u8, completion_time: f32) -> usize {
let perfect_time = calculate_perfect_time(floor_number);
// Floor progression using exponential scaling for better high-floor rewards
let floor_multiplier = (1.0 + floor_number as f32).powf(FLOOR_PROGRESSION_MULTIPLIER);
let base_score = BASE_FLOOR_SCORE as f32 * floor_multiplier;
// Time bonus calculation
// Perfect time or better gets maximum bonus
// Longer times get diminishing returns but never below minimum
let time_multiplier = if completion_time <= perfect_time {
// Bonus for being faster than perfect time
let speed_ratio = perfect_time / completion_time;
speed_ratio * TIME_BONUS_MULTIPLIER
} else {
// Penalty for being slower than perfect time, with smooth degradation
let overtime_ratio = completion_time / perfect_time;
let time_factor = 1.0 / overtime_ratio;
time_factor.max(MIN_TIME_MULTIPLIER) * TIME_BONUS_MULTIPLIER
};
(base_score * time_multiplier) as usize
}
/// Perfect time increases with floor number
fn calculate_perfect_time(floor_number: u8) -> f32 {
BASE_PERFECT_TIME * (floor_number as f32 - 1.).mul_add(TIME_INCREASE_FACTOR, 1.)
}
#[cfg(test)]
mod tests {
use claims::*;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rstest::*;
use super::*;
#[fixture]
fn floors() -> Vec<u8> {
(1..=100).collect()
}
#[fixture]
fn times() -> Vec<f32> {
vec![
BASE_PERFECT_TIME * 0.5, // Much faster than perfect
BASE_PERFECT_TIME * 0.8, // Faster than perfect
BASE_PERFECT_TIME, // Perfect time
BASE_PERFECT_TIME * 1.5, // Slower than perfect
BASE_PERFECT_TIME * 2.0, // Much slower
]
}
#[rstest]
#[case(1, BASE_PERFECT_TIME)]
#[case(2, BASE_PERFECT_TIME * (1.0 + TIME_INCREASE_FACTOR))]
#[case(5, BASE_PERFECT_TIME * 4.0f32.mul_add(TIME_INCREASE_FACTOR, 1.))]
fn specific_perfect_times(#[case] floor: u8, #[case] expected_time: f32) {
let calculated_time = calculate_perfect_time(floor);
assert_le!(
(calculated_time - expected_time).abs(),
0.001,
"Perfect time calculation mismatch"
);
}
#[rstest]
fn score_progression(floors: Vec<u8>, times: Vec<f32>) {
let floor_scores = floors
.par_iter()
.map(|floor| {
let scores = times
.par_iter()
.map(|&time| (*floor, time, calculate_score(*floor, time)))
.collect::<Vec<_>>();
(*floor, scores)
})
.collect::<Vec<_>>();
for (floor, scores) in floor_scores {
scores.windows(2).for_each(|window| {
let (_, time1, score1) = window[0];
let (_, time2, score2) = window[1];
if time1 < time2 {
assert_gt!(
score1,
score2,
"Floor {}: Faster time ({}) should give higher score than slower time ({})",
floor,
time1,
time2
);
}
})
}
}
#[rstest]
fn perfect_time_progression(floors: Vec<u8>) {
let perfect_scores = floors
.par_iter()
.map(|&floor| {
let perfect_time = calculate_perfect_time(floor);
(floor, calculate_score(floor, perfect_time))
})
.collect::<Vec<_>>();
perfect_scores.windows(2).for_each(|window| {
let (floor1, score1) = window[0];
let (floor2, score2) = window[1];
assert_gt!(
score2,
score1,
"Floor {} perfect score ({}) should be higher than floor {} perfect score ({})",
floor2,
score2,
floor1,
score1
);
})
}
#[rstest]
fn minimum_score_guarantee(floors: Vec<u8>) {
let very_slow_time = BASE_PERFECT_TIME * 10.0;
// Test minimum scores in parallel
let min_scores = floors
.par_iter()
.map(|&floor| calculate_score(floor, very_slow_time))
.collect::<Vec<_>>();
// Verify minimum scores
min_scores.windows(2).for_each(|window| {
assert_gt!(
window[1],
window[0],
"Higher floor should give better minimum score"
);
});
// Verify all scores are above zero
min_scores.iter().for_each(|&score| {
assert_gt!(score, 0, "Score should never be zero");
});
}
}

View File

@ -1,25 +0,0 @@
use bevy::prelude::*;
use crate::{
screens::GameplayElement,
stats::{
components::{
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, ScoreDisplay, TotalTimerDisplay,
},
container::StatsContainer,
},
theme::widgets::Widgets,
};
pub fn spawn_stats(mut commands: Commands) {
commands
.ui_stats()
.insert(GameplayElement)
.with_children(|parent| {
parent.stats("Floor: 1", FloorDisplay);
parent.stats("Highest Floor: 1", HighestFloorDisplay);
parent.stats("Score: 0", ScoreDisplay);
parent.stats("Floor Timer", FloorTimerDisplay);
parent.stats("Total Timer", TotalTimerDisplay);
});
}

View File

@ -1,23 +0,0 @@
use bevy::prelude::*;
use crate::stats::{components::TotalTimerDisplay, resources::TotalTimer};
use super::common::format_duration_adaptive;
pub fn update_total_timer(mut total_timer: ResMut<TotalTimer>, time: Res<Time>) {
total_timer.tick(time.delta());
}
pub fn update_total_timer_display(
mut text_query: Query<&mut Text, With<TotalTimerDisplay>>,
total_timer: Res<TotalTimer>,
) {
let Ok(mut text) = text_query.get_single_mut() else {
return;
};
text.0 = format!(
"Total Timer: {}",
format_duration_adaptive(total_timer.0.elapsed_secs())
);
}

View File

@ -8,7 +8,7 @@ pub mod components;
pub mod events; pub mod events;
pub mod palette; pub mod palette;
mod systems; mod systems;
pub mod widgets; mod widgets;
#[allow(unused_imports)] #[allow(unused_imports)]
pub mod prelude { pub mod prelude {

View File

@ -19,8 +19,6 @@ pub trait Widgets {
/// Spawn a simple text label. /// Spawn a simple text label.
fn label(&mut self, text: impl Into<String>) -> EntityCommands; fn label(&mut self, text: impl Into<String>) -> EntityCommands;
fn stats(&mut self, text: impl Into<String>, bundle: impl Bundle) -> EntityCommands;
} }
impl<T: SpawnUi> Widgets for T { impl<T: SpawnUi> Widgets for T {
@ -109,21 +107,6 @@ impl<T: SpawnUi> Widgets for T {
)); ));
entity entity
} }
fn stats(&mut self, text: impl Into<String>, bundle: impl Bundle) -> EntityCommands {
let text = text.into();
let entity = self.spawn_ui((
Name::new(text.clone()),
Text(text),
TextFont {
font_size: 24.0,
..default()
},
bundle,
TextColor(RosePineDawn::Text.to_color()),
));
entity
}
} }
/// An extension trait for spawning UI containers. /// An extension trait for spawning UI containers.