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 TITLE: &str = "Maze Ascension: The Labyrinth of Echoes";
|
||||
|
||||
pub const FLOOR_SCORE_MULTIPLIER: f32 = 100.;
|
||||
pub const TIME_SCORE_MULTIPLIER: f32 = 10.0;
|
||||
// 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
|
||||
|
||||
@ -4,6 +4,22 @@ use bevy::prelude::*;
|
||||
#[reflect(Component)]
|
||||
pub struct Score(pub usize);
|
||||
|
||||
#[derive(Debug, Reflect, Component)]
|
||||
#[derive(Debug, Clone, 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 container;
|
||||
pub mod resources;
|
||||
pub mod stats;
|
||||
mod systems;
|
||||
|
||||
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
||||
use components::Score;
|
||||
use resources::{FloorTimer, GameTimer};
|
||||
use resources::{FloorTimer, TotalTimer};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<Score>()
|
||||
.init_resource::<GameTimer>()
|
||||
.init_resource::<TotalTimer>()
|
||||
.init_resource::<FloorTimer>()
|
||||
.insert_resource(FloorTimer(Timer::from_seconds(0.0, TimerMode::Once)))
|
||||
.add_plugins(systems::plugin);
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +1,27 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
||||
#[reflect(Resource)]
|
||||
pub struct GameTimer(pub Timer);
|
||||
pub struct TotalTimer(pub Timer);
|
||||
|
||||
#[derive(Debug, Reflect, Resource, Deref, DerefMut)]
|
||||
#[reflect(Resource)]
|
||||
pub struct FloorTimer(pub Timer);
|
||||
|
||||
impl Default for GameTimer {
|
||||
impl Default for TotalTimer {
|
||||
fn default() -> Self {
|
||||
Self(Timer::from_seconds(0.0, TimerMode::Once))
|
||||
Self(init_timer())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FloorTimer {
|
||||
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;
|
||||
pub mod setup;
|
||||
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 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(
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,39 +1,55 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::{FLOOR_SCORE_MULTIPLIER, TIME_SCORE_MULTIPLIER},
|
||||
floor::components::{CurrentFloor, Floor},
|
||||
stats::{components::Score, resources::GameTimer},
|
||||
constants::{
|
||||
BASE_FLOOR_SCORE, FLOOR_DIFFICULTY_MULTIPLIER, MIN_TIME_MULTIPLIER, TIME_REFERENCE_SECONDS,
|
||||
},
|
||||
floor::resources::HighestFloor,
|
||||
stats::{
|
||||
components::{Score, ScoreDisplay},
|
||||
resources::FloorTimer,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn update_score(
|
||||
mut score_query: Query<&mut Score>,
|
||||
mut game_timer: ResMut<GameTimer>,
|
||||
floor_query: Query<&Floor, With<CurrentFloor>>,
|
||||
time: Res<Time>,
|
||||
hightes_floor: Res<HighestFloor>,
|
||||
floor_timer: Res<FloorTimer>,
|
||||
) {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(current_floor) = floor_query.get_single() else {
|
||||
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;
|
||||
score.0 = calculate_score(hightes_floor.0, floor_timer.elapsed_secs());
|
||||
}
|
||||
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
stats::{components::Score, stats::StatsContainer},
|
||||
stats::{
|
||||
components::{
|
||||
FloorDisplay, FloorTimerDisplay, HighestFloorDisplay, Score, ScoreDisplay,
|
||||
TotalTimerDisplay,
|
||||
},
|
||||
container::StatsContainer,
|
||||
},
|
||||
theme::widgets::Widgets,
|
||||
};
|
||||
|
||||
pub fn setup(mut commands: Commands) {
|
||||
commands.spawn((Name::new("Score"), Score(0)));
|
||||
|
||||
commands.ui_stats().with_children(|parent| {
|
||||
parent.stats("Floor", "0");
|
||||
parent.stats("Score", "0");
|
||||
parent.stats("Floor timer", "00:00");
|
||||
parent.stats("Game timer", "00:00");
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
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 {
|
||||
@ -110,15 +110,16 @@ impl<T: SpawnUi> Widgets for T {
|
||||
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 entity = self.spawn_ui((
|
||||
Name::new(text.clone()),
|
||||
Text(format!("{text}: {}", value.into())),
|
||||
Text(text),
|
||||
TextFont {
|
||||
font_size: 24.0,
|
||||
..default()
|
||||
},
|
||||
bundle,
|
||||
TextColor(RosePineDawn::Foam.to_color()),
|
||||
));
|
||||
entity
|
||||
|
||||
Loading…
Reference in New Issue
Block a user