Merge pull request #35 from kristoferssolo/feture/score

This commit is contained in:
Kristofers Solo 2025-01-17 00:58:07 +02:00 committed by GitHub
commit d01e987b89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 420 additions and 23 deletions

58
Cargo.lock generated
View File

@ -1409,7 +1409,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash", "rustc-hash 1.1.0",
"shlex", "shlex",
"syn", "syn",
] ]
@ -1813,7 +1813,7 @@ dependencies = [
"log", "log",
"rangemap", "rangemap",
"rayon", "rayon",
"rustc-hash", "rustc-hash 1.1.0",
"rustybuzz", "rustybuzz",
"self_cell", "self_cell",
"swash", "swash",
@ -1925,6 +1925,18 @@ 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"
@ -2644,15 +2656,16 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "hexlab" name = "hexlab"
version = "0.5.3" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d2fbc6c41965686841aa5ea0e1af448730d0902274e49251c7d1fb7c78fffb9" checksum = "e247b21d6885136e4a910e1e6fac755d8dc56112c40ebf1062582f0644d62c62"
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",
] ]
@ -2894,6 +2907,15 @@ 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"
@ -3228,7 +3250,7 @@ dependencies = [
"indexmap", "indexmap",
"log", "log",
"pp-rs", "pp-rs",
"rustc-hash", "rustc-hash 1.1.0",
"spirv", "spirv",
"termcolor", "termcolor",
"thiserror 1.0.69", "thiserror 1.0.69",
@ -3249,7 +3271,7 @@ dependencies = [
"once_cell", "once_cell",
"regex", "regex",
"regex-syntax 0.8.5", "regex-syntax 0.8.5",
"rustc-hash", "rustc-hash 1.1.0",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
"unicode-ident", "unicode-ident",
@ -3763,6 +3785,20 @@ 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"
@ -4184,6 +4220,12 @@ 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"
@ -5224,7 +5266,7 @@ dependencies = [
"parking_lot", "parking_lot",
"profiling", "profiling",
"raw-window-handle", "raw-window-handle",
"rustc-hash", "rustc-hash 1.1.0",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wgpu-hal", "wgpu-hal",
@ -5266,7 +5308,7 @@ dependencies = [
"range-alloc", "range-alloc",
"raw-window-handle", "raw-window-handle",
"renderdoc-sys", "renderdoc-sys",
"rustc-hash", "rustc-hash 1.1.0",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wasm-bindgen", "wasm-bindgen",

View File

@ -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.5", features = ["bevy"] } hexlab = { version = "0.6", features = ["bevy", "pathfinding"] }
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

@ -8,7 +8,7 @@ native-dev:
# Run native release # Run native release
native-release: native-release:
RUSTC_WRAPPER=sccache cargo run --release --no-default-features RUSTC_WRAPPER=sccache cargo run --release --no-default-features
# Run web dev # Run web dev
web-dev: web-dev:

View File

@ -3,3 +3,12 @@ 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 = 1000;
pub const BASE_TIME_SCORE: usize = 100;
// Floor progression constants
pub const FLOOR_DIFFICULTY_MULTIPLIER: f32 = 1.2; // Higher floors are exponentially harder
pub const MIN_TIME_MULTIPLIER: f32 = 0.1; // Minimum score multiplier for time
pub const TIME_REFERENCE_SECONDS: f32 = 60.0; // Reference time for score calculation

View File

@ -3,9 +3,11 @@ 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.add_plugins(systems::plugin); app.register_type::<IdleTimer>()
.add_plugins(systems::plugin);
} }
pub fn spawn_hint_command(world: &mut World) { pub fn spawn_hint_command(world: &mut World) {

View File

@ -8,6 +8,7 @@ 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::{
@ -69,6 +70,7 @@ impl Plugin for AppPlugin {
floor::plugin, floor::plugin,
player::plugin, player::plugin,
hint::plugin, hint::plugin,
stats::plugin,
)); ));
// Enable dev tools for dev builds. // Enable dev tools for dev builds.

View File

@ -95,14 +95,6 @@ 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

@ -1,8 +1,9 @@
//! The screen state for the main gameplay. //! The screen state for the main gameplay.
use crate::player::spawn_player_command; use crate::{
use crate::screens::Screen; hint::spawn_hint_command, maze::spawn_level_command, player::spawn_player_command,
use crate::{hint::spawn_hint_command, maze::spawn_level_command}; screens::Screen, stats::spawn_stats_command,
};
use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use bevy::{input::common_conditions::input_just_pressed, prelude::*};
@ -13,6 +14,7 @@ pub(super) fn plugin(app: &mut App) {
spawn_level_command, spawn_level_command,
spawn_player_command, spawn_player_command,
spawn_hint_command, spawn_hint_command,
spawn_stats_command,
) )
.chain(), .chain(),
); );

21
src/stats/components.rs Normal file
View File

@ -0,0 +1,21 @@
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;

22
src/stats/container.rs Normal file
View File

@ -0,0 +1,22 @@
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()
},
))
}
}

18
src/stats/mod.rs Normal file
View File

@ -0,0 +1,18 @@
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);
}

31
src/stats/resources.rs Normal file
View File

@ -0,0 +1,31 @@
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

@ -0,0 +1,28 @@
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

@ -0,0 +1,39 @@
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

@ -0,0 +1,33 @@
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())
);
}

31
src/stats/systems/mod.rs Normal file
View File

@ -0,0 +1,31 @@
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::Screen;
pub(super) fn plugin(app: &mut App) {
app.add_systems(OnEnter(Screen::Gameplay), reset_timers)
.add_systems(
Update,
(
(update_score, 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

@ -0,0 +1,13 @@
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

@ -0,0 +1,47 @@
use bevy::prelude::*;
use crate::{
constants::{
BASE_FLOOR_SCORE, FLOOR_DIFFICULTY_MULTIPLIER, MIN_TIME_MULTIPLIER, TIME_REFERENCE_SECONDS,
},
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, 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 {
// Calculate base floor score with exponential scaling
let floor_multiplier = (floor_number as f32).powf(FLOOR_DIFFICULTY_MULTIPLIER);
let base_score = BASE_FLOOR_SCORE as f32 * floor_multiplier;
// Calculate time multiplier (decreases as time increases)
let time_factor = 1. / (1. + (completion_time / TIME_REFERENCE_SECONDS));
let time_multiplier = time_factor.max(MIN_TIME_MULTIPLIER);
(base_score * time_multiplier) as usize
}

View File

@ -0,0 +1,25 @@
use bevy::prelude::*;
use crate::{
screens::Screen,
stats::{
components::{
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, ScoreDisplay, TotalTimerDisplay,
},
container::StatsContainer,
},
theme::widgets::Widgets,
};
pub fn spawn_stats(mut commands: Commands) {
commands
.ui_stats()
.insert(StateScoped(Screen::Gameplay))
.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

@ -0,0 +1,23 @@
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;
mod widgets; pub mod widgets;
#[allow(unused_imports)] #[allow(unused_imports)]
pub mod prelude { pub mod prelude {

View File

@ -19,6 +19,8 @@ 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 {
@ -107,6 +109,21 @@ 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.