mirror of
https://github.com/kristoferssolo/maze-ascension.git
synced 2025-10-21 19:20:34 +00:00
feat(score): add score calculator
This commit is contained in:
parent
d2dd57bcff
commit
58276ea8f7
@ -4,5 +4,11 @@ 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";
|
||||||
|
|
||||||
pub const FLOOR_SCORE_MULTIPLIER: f32 = 100.;
|
// Base score constants
|
||||||
pub const TIME_SCORE_MULTIPLIER: f32 = 10.0;
|
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
|
||||||
|
|||||||
@ -4,6 +4,22 @@ use bevy::prelude::*;
|
|||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Score(pub usize);
|
pub struct Score(pub usize);
|
||||||
|
|
||||||
#[derive(Debug, Reflect, Component)]
|
#[derive(Debug, Clone, Reflect, Component)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct StatsText;
|
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;
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod container;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
pub mod stats;
|
|
||||||
mod systems;
|
mod systems;
|
||||||
|
|
||||||
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
||||||
use components::Score;
|
use components::Score;
|
||||||
use resources::{FloorTimer, GameTimer};
|
use resources::{FloorTimer, TotalTimer};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.register_type::<Score>()
|
app.register_type::<Score>()
|
||||||
.init_resource::<GameTimer>()
|
.init_resource::<TotalTimer>()
|
||||||
.init_resource::<FloorTimer>()
|
.init_resource::<FloorTimer>()
|
||||||
.insert_resource(FloorTimer(Timer::from_seconds(0.0, TimerMode::Once)))
|
|
||||||
.add_plugins(systems::plugin);
|
.add_plugins(systems::plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,27 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
pub struct GameTimer(pub Timer);
|
pub struct TotalTimer(pub Timer);
|
||||||
|
|
||||||
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
pub struct FloorTimer(pub Timer);
|
pub struct FloorTimer(pub Timer);
|
||||||
|
|
||||||
impl Default for GameTimer {
|
impl Default for TotalTimer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Timer::from_seconds(0.0, TimerMode::Once))
|
Self(init_timer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FloorTimer {
|
impl Default for FloorTimer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Timer::from_seconds(0.0, TimerMode::Once))
|
Self(init_timer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_timer() -> Timer {
|
||||||
|
Timer::new(Duration::MAX, TimerMode::Once)
|
||||||
|
}
|
||||||
|
|||||||
28
src/stats/systems/common.rs
Normal file
28
src/stats/systems/common.rs
Normal 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
|
||||||
|
}
|
||||||
39
src/stats/systems/floor.rs
Normal file
39
src/stats/systems/floor.rs
Normal 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);
|
||||||
|
}
|
||||||
33
src/stats/systems/floor_timer.rs
Normal file
33
src/stats/systems/floor_timer.rs
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,16 +1,28 @@
|
|||||||
|
mod common;
|
||||||
|
mod floor;
|
||||||
|
mod floor_timer;
|
||||||
mod score;
|
mod score;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
mod total_timer;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use floor::{update_floor_display, update_highest_floor_display};
|
||||||
|
use floor_timer::{update_floor_timer, update_floor_timer_display};
|
||||||
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 crate::screens::Screen;
|
use crate::screens::Screen;
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(update_score, update_score_display)
|
(
|
||||||
.chain()
|
(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)),
|
.run_if(in_state(Screen::Gameplay)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,55 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{FLOOR_SCORE_MULTIPLIER, TIME_SCORE_MULTIPLIER},
|
constants::{
|
||||||
floor::components::{CurrentFloor, Floor},
|
BASE_FLOOR_SCORE, FLOOR_DIFFICULTY_MULTIPLIER, MIN_TIME_MULTIPLIER, TIME_REFERENCE_SECONDS,
|
||||||
stats::{components::Score, resources::GameTimer},
|
},
|
||||||
|
floor::resources::HighestFloor,
|
||||||
|
stats::{
|
||||||
|
components::{Score, ScoreDisplay},
|
||||||
|
resources::FloorTimer,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn update_score(
|
pub fn update_score(
|
||||||
mut score_query: Query<&mut Score>,
|
mut score_query: Query<&mut Score>,
|
||||||
mut game_timer: ResMut<GameTimer>,
|
hightes_floor: Res<HighestFloor>,
|
||||||
floor_query: Query<&Floor, With<CurrentFloor>>,
|
floor_timer: Res<FloorTimer>,
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
) {
|
||||||
game_timer.tick(time.delta());
|
if !hightes_floor.is_changed() || hightes_floor.is_added() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(mut score) = score_query.get_single_mut() else {
|
let Ok(mut score) = score_query.get_single_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(current_floor) = floor_query.get_single() else {
|
score.0 = calculate_score(hightes_floor.0, floor_timer.elapsed_secs());
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let time_score = game_timer.elapsed_secs() * TIME_SCORE_MULTIPLIER;
|
|
||||||
let floor_score = current_floor.0 as f32 * FLOOR_SCORE_MULTIPLIER;
|
|
||||||
score.0 = (time_score + floor_score) as usize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_score_display(score_query: Query<&Score>, mut text_query: Query<&mut Text>) {
|
pub fn update_score_display(
|
||||||
|
score_query: Query<&Score>,
|
||||||
|
mut text_query: Query<&mut Text, With<ScoreDisplay>>,
|
||||||
|
) {
|
||||||
let Ok(score) = score_query.get_single() else {
|
let Ok(score) = score_query.get_single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(mut text) = text_query.get_single_mut() else {
|
let Ok(mut text) = text_query.get_single_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
text.0 = format!("Score: {}", score.0);
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
stats::{components::Score, stats::StatsContainer},
|
stats::{
|
||||||
|
components::{
|
||||||
|
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, Score, ScoreDisplay,
|
||||||
|
TotalTimerDisplay,
|
||||||
|
},
|
||||||
|
container::StatsContainer,
|
||||||
|
},
|
||||||
theme::widgets::Widgets,
|
theme::widgets::Widgets,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn setup(mut commands: Commands) {
|
pub fn setup(mut commands: Commands) {
|
||||||
commands.spawn((Name::new("Score"), Score(0)));
|
commands.spawn((Name::new("Score"), Score(0)));
|
||||||
|
|
||||||
commands.ui_stats().with_children(|parent| {
|
commands.ui_stats().with_children(|parent| {
|
||||||
parent.stats("Floor", "0");
|
parent.stats("Floor: 1", FloorDisplay);
|
||||||
parent.stats("Score", "0");
|
parent.stats("Highest Floor: 1", HighestFloorDisplay);
|
||||||
parent.stats("Floor timer", "00:00");
|
parent.stats("Score: 0", ScoreDisplay);
|
||||||
parent.stats("Game timer", "00:00");
|
parent.stats("Floor Timer", FloorTimerDisplay);
|
||||||
|
parent.stats("Total Timer", TotalTimerDisplay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/stats/systems/total_timer.rs
Normal file
23
src/stats/systems/total_timer.rs
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ 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>, value: 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 {
|
||||||
@ -110,15 +110,16 @@ impl<T: SpawnUi> Widgets for T {
|
|||||||
entity
|
entity
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stats(&mut self, text: impl Into<String>, value: impl Into<String>) -> EntityCommands {
|
fn stats(&mut self, text: impl Into<String>, bundle: impl Bundle) -> EntityCommands {
|
||||||
let text = text.into();
|
let text = text.into();
|
||||||
let entity = self.spawn_ui((
|
let entity = self.spawn_ui((
|
||||||
Name::new(text.clone()),
|
Name::new(text.clone()),
|
||||||
Text(format!("{text}: {}", value.into())),
|
Text(text),
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 24.0,
|
font_size: 24.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
bundle,
|
||||||
TextColor(RosePineDawn::Foam.to_color()),
|
TextColor(RosePineDawn::Foam.to_color()),
|
||||||
));
|
));
|
||||||
entity
|
entity
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user