feat(score): add score calculator

This commit is contained in:
Kristofers Solo 2025-01-17 00:41:16 +02:00
parent d2dd57bcff
commit 58276ea8f7
13 changed files with 223 additions and 38 deletions

View File

@ -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

View File

@ -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;

View File

@ -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);
}

View File

@ -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)
}

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())
);
}

View File

@ -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)),
);
}

View File

@ -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
}

View File

@ -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);
});
}

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

@ -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