mirror of
https://github.com/kristoferssolo/maze-ascension.git
synced 2026-01-11 19:16:10 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed77d18e1e | |||
| 5dd932a6fe | |||
| d820b19988 | |||
| f08bd72038 | |||
| 4864ecca93 | |||
| 7fa567d522 | |||
| 7a4bcd81f9 | |||
| 5d50daf768 | |||
| e7bdb37093 | |||
| c8e968e76e | |||
| 099d163325 | |||
| 4398620ac8 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -3157,7 +3157,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maze-ascension"
|
name = "maze-ascension"
|
||||||
version = "1.1.0"
|
version = "1.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy",
|
"bevy",
|
||||||
|
|||||||
@ -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.0"
|
version = "1.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use bevy::{input::mouse::MouseWheel, prelude::*};
|
use bevy::{input::mouse::MouseWheel, prelude::*};
|
||||||
|
|
||||||
use crate::constants::{BASE_ZOOM_SPEED, MAX_ZOOM, MIN_ZOOM};
|
use crate::constants::{BASE_ZOOM_SPEED, MAX_ZOOM, MIN_ZOOM, SCROLL_MODIFIER};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_systems(Update, camera_zoom);
|
app.add_systems(Update, camera_zoom);
|
||||||
@ -53,7 +53,7 @@ fn camera_zoom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ev in scrool_evr.read() {
|
for ev in scrool_evr.read() {
|
||||||
zoom_delta += ev.y * adjusted_zoom_speed;
|
zoom_delta += ev.y * adjusted_zoom_speed * SCROLL_MODIFIER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if zoom_delta != 0.0 {
|
if zoom_delta != 0.0 {
|
||||||
|
|||||||
@ -5,15 +5,22 @@ 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
|
// Base score constants
|
||||||
pub const BASE_FLOOR_SCORE: usize = 1000;
|
pub const BASE_FLOOR_SCORE: usize = 100;
|
||||||
pub const BASE_TIME_SCORE: usize = 100;
|
|
||||||
|
|
||||||
// Floor progression constants
|
// Floor progression constants
|
||||||
pub const FLOOR_DIFFICULTY_MULTIPLIER: f32 = 1.2; // Higher floors are exponentially harder
|
pub const FLOOR_PROGRESSION_MULTIPLIER: f32 = 1.2;
|
||||||
pub const MIN_TIME_MULTIPLIER: f32 = 0.1; // Minimum score multiplier for time
|
pub const MIN_TIME_MULTIPLIER: f32 = 0.2; // Minimum score multiplier for time
|
||||||
pub const TIME_REFERENCE_SECONDS: f32 = 60.0; // Reference time for score calculation
|
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
|
// Constants for camera control
|
||||||
|
|
||||||
pub const BASE_ZOOM_SPEED: f32 = 10.0;
|
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 MIN_ZOOM: f32 = 50.0;
|
||||||
pub const MAX_ZOOM: f32 = 2500.0;
|
pub const MAX_ZOOM: f32 = 2500.0;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Reflect, Component, Deref, DerefMut)]
|
#[derive(Debug, Reflect, Component, Deref, DerefMut, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Floor(pub u8);
|
pub struct Floor(pub u8);
|
||||||
|
|
||||||
|
|||||||
19
src/floor/systems/hide.rs
Normal file
19
src/floor/systems/hide.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
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;
|
||||||
|
|
||||||
@ -16,6 +18,7 @@ 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)),
|
||||||
|
|||||||
@ -74,7 +74,7 @@ pub fn handle_floor_transition_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for event in event_reader.read() {
|
for event in event_reader.read() {
|
||||||
let Some((current_entity, current_floor)) = current_query.get_single().ok() else {
|
let Ok((current_entity, current_floor)) = current_query.get_single() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,13 @@ 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;
|
||||||
|
|
||||||
pub(super) fn plugin(_app: &mut App) {}
|
use crate::screens::Screen;
|
||||||
|
|
||||||
|
pub(super) fn plugin(app: &mut App) {
|
||||||
|
app.add_systems(Update, toggle_walls.run_if(state_changed::<Screen>));
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use crate::{
|
|||||||
components::{HexMaze, MazeConfig, Tile, Wall},
|
components::{HexMaze, MazeConfig, Tile, Wall},
|
||||||
resources::GlobalMazeConfig,
|
resources::GlobalMazeConfig,
|
||||||
},
|
},
|
||||||
screens::Screen,
|
screens::GameplayElement,
|
||||||
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,
|
||||||
StateScoped(Screen::Gameplay),
|
GameplayElement,
|
||||||
))
|
))
|
||||||
.insert_if(CurrentFloor, || floor == 1) // Only floor 1 gets CurrentFloor
|
.insert_if(CurrentFloor, || floor == 1) // Only floor 1 gets CurrentFloor
|
||||||
.id();
|
.id();
|
||||||
|
|||||||
13
src/maze/systems/toggle_pause.rs
Normal file
13
src/maze/systems/toggle_pause.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ 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};
|
||||||
@ -11,6 +12,7 @@ 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;
|
||||||
@ -30,4 +32,5 @@ 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>));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::Screen,
|
screens::GameplayElement,
|
||||||
};
|
};
|
||||||
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),
|
||||||
StateScoped(Screen::Gameplay),
|
GameplayElement,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/player/systems/toggle_pause.rs
Normal file
13
src/player/systems/toggle_pause.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ use crate::{
|
|||||||
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),
|
||||||
(
|
(
|
||||||
@ -16,16 +17,46 @@ pub(super) fn plugin(app: &mut App) {
|
|||||||
spawn_hint_command,
|
spawn_hint_command,
|
||||||
spawn_stats_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,
|
||||||
return_to_title_screen
|
pause_game.run_if(in_state(Screen::Gameplay).and(input_just_pressed(KeyCode::Escape))),
|
||||||
.run_if(in_state(Screen::Gameplay).and(input_just_pressed(KeyCode::Escape))),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
fn pause_game(mut next_screen: ResMut<NextState<Screen>>) {
|
||||||
next_screen.set(Screen::Title);
|
next_screen.set(Screen::Pause);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
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>();
|
||||||
@ -16,6 +18,7 @@ pub(super) fn plugin(app: &mut App) {
|
|||||||
loading::plugin,
|
loading::plugin,
|
||||||
splash::plugin,
|
splash::plugin,
|
||||||
title::plugin,
|
title::plugin,
|
||||||
|
pause::plugin,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,4 +31,5 @@ pub enum Screen {
|
|||||||
Loading,
|
Loading,
|
||||||
Title,
|
Title,
|
||||||
Gameplay,
|
Gameplay,
|
||||||
|
Pause,
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/screens/pause.rs
Normal file
57
src/screens/pause.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -13,19 +13,26 @@ use reset::reset_timers;
|
|||||||
use score::{update_score, update_score_display};
|
use score::{update_score, update_score_display};
|
||||||
use total_timer::{update_total_timer, update_total_timer_display};
|
use total_timer::{update_total_timer, update_total_timer_display};
|
||||||
|
|
||||||
use crate::screens::Screen;
|
use crate::screens::{GameplayInitialized, Screen};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_systems(OnEnter(Screen::Gameplay), reset_timers)
|
app.add_systems(
|
||||||
.add_systems(
|
OnEnter(Screen::Gameplay),
|
||||||
Update,
|
reset_timers.run_if(not(resource_exists::<GameplayInitialized>)),
|
||||||
|
);
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
(
|
(
|
||||||
(update_score, update_score_display).chain(),
|
update_score.before(update_floor_timer),
|
||||||
(update_floor_timer, update_floor_timer_display).chain(),
|
update_score_display,
|
||||||
(update_total_timer, update_total_timer_display).chain(),
|
|
||||||
update_floor_display,
|
|
||||||
update_highest_floor_display,
|
|
||||||
)
|
)
|
||||||
.run_if(in_state(Screen::Gameplay)),
|
.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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,8 @@ use bevy::prelude::*;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{
|
constants::{
|
||||||
BASE_FLOOR_SCORE, FLOOR_DIFFICULTY_MULTIPLIER, MIN_TIME_MULTIPLIER, TIME_REFERENCE_SECONDS,
|
BASE_FLOOR_SCORE, BASE_PERFECT_TIME, FLOOR_PROGRESSION_MULTIPLIER, MIN_TIME_MULTIPLIER,
|
||||||
|
TIME_BONUS_MULTIPLIER, TIME_INCREASE_FACTOR,
|
||||||
},
|
},
|
||||||
floor::resources::HighestFloor,
|
floor::resources::HighestFloor,
|
||||||
stats::{
|
stats::{
|
||||||
@ -20,7 +21,7 @@ pub fn update_score(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
score.0 = calculate_score(
|
score.0 += calculate_score(
|
||||||
hightes_floor.0.saturating_sub(1),
|
hightes_floor.0.saturating_sub(1),
|
||||||
floor_timer.elapsed_secs(),
|
floor_timer.elapsed_secs(),
|
||||||
);
|
);
|
||||||
@ -38,13 +39,150 @@ pub fn update_score_display(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_score(floor_number: u8, completion_time: f32) -> usize {
|
fn calculate_score(floor_number: u8, completion_time: f32) -> usize {
|
||||||
// Calculate base floor score with exponential scaling
|
let perfect_time = calculate_perfect_time(floor_number);
|
||||||
let floor_multiplier = (floor_number as f32).powf(FLOOR_DIFFICULTY_MULTIPLIER);
|
|
||||||
|
// 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;
|
let base_score = BASE_FLOOR_SCORE as f32 * floor_multiplier;
|
||||||
|
|
||||||
// Calculate time multiplier (decreases as time increases)
|
// Time bonus calculation
|
||||||
let time_factor = 1. / (1. + (completion_time / TIME_REFERENCE_SECONDS));
|
// Perfect time or better gets maximum bonus
|
||||||
let time_multiplier = time_factor.max(MIN_TIME_MULTIPLIER);
|
// 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
|
(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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
screens::Screen,
|
screens::GameplayElement,
|
||||||
stats::{
|
stats::{
|
||||||
components::{
|
components::{
|
||||||
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, ScoreDisplay, TotalTimerDisplay,
|
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, ScoreDisplay, TotalTimerDisplay,
|
||||||
@ -14,7 +14,7 @@ use crate::{
|
|||||||
pub fn spawn_stats(mut commands: Commands) {
|
pub fn spawn_stats(mut commands: Commands) {
|
||||||
commands
|
commands
|
||||||
.ui_stats()
|
.ui_stats()
|
||||||
.insert(StateScoped(Screen::Gameplay))
|
.insert(GameplayElement)
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent.stats("Floor: 1", FloorDisplay);
|
parent.stats("Floor: 1", FloorDisplay);
|
||||||
parent.stats("Highest Floor: 1", HighestFloorDisplay);
|
parent.stats("Highest Floor: 1", HighestFloorDisplay);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user