use bevy::prelude::*;
use chrono::{Datelike, Duration, NaiveDateTime, Timelike, Utc};
#[derive(Message, Debug, Clone)]
pub struct ClockIntervalEvent {
pub interval: ClockInterval,
pub count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClockInterval {
Second,
Minute,
Hour,
Day,
Week,
Custom(u32),
}
impl ClockInterval {
pub fn as_seconds(&self) -> u32 {
match self {
ClockInterval::Second => 1,
ClockInterval::Minute => 60,
ClockInterval::Hour => 3600,
ClockInterval::Day => 86400,
ClockInterval::Week => 604800,
ClockInterval::Custom(seconds) => *seconds,
}
}
}
pub struct InGameClockPlugin;
impl Plugin for InGameClockPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<InGameClock>()
.init_resource::<ClockIntervalTrackers>()
.add_message::<ClockIntervalEvent>()
.add_systems(Update, update_clock)
.add_systems(Update, check_intervals);
}
}
#[derive(Resource, Default)]
struct ClockIntervalTrackers {
trackers: Vec<IntervalTracker>,
}
struct IntervalTracker {
interval: ClockInterval,
last_trigger_seconds: f64,
count: u64,
}
#[derive(Resource, Debug, Clone)]
pub struct InGameClock {
pub elapsed_seconds: f64,
pub speed: f32,
pub paused: bool,
pub start_datetime: NaiveDateTime,
}
impl Default for InGameClock {
fn default() -> Self {
let now = Utc::now().naive_utc();
Self {
elapsed_seconds: 0.0,
speed: 1.0,
paused: false,
start_datetime: now,
}
}
}
impl InGameClock {
pub fn new() -> Self {
Self::default()
}
pub fn register_interval(world: &mut World, interval: ClockInterval) {
let mut trackers = world.resource_mut::<ClockIntervalTrackers>();
if !trackers.trackers.iter().any(|t| t.interval == interval) {
trackers.trackers.push(IntervalTracker {
interval,
last_trigger_seconds: 0.0,
count: 0,
});
}
}
pub fn with_start_datetime(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> Self {
let start_datetime = NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(year, month, day).unwrap(),
chrono::NaiveTime::from_hms_opt(hour, minute, second).unwrap(),
);
Self {
elapsed_seconds: 0.0,
speed: 1.0,
paused: false,
start_datetime,
}
}
pub fn with_speed(mut self, speed: f32) -> Self {
self.speed = speed;
self
}
pub fn with_day_duration(mut self, real_seconds_per_day: f32) -> Self {
self.speed = 86400.0 / real_seconds_per_day;
self
}
pub fn with_start(mut self, datetime: NaiveDateTime) -> Self {
self.start_datetime = datetime;
self
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn toggle_pause(&mut self) {
self.paused = !self.paused;
}
pub fn set_speed(&mut self, speed: f32) {
self.speed = speed;
}
pub fn set_day_duration(&mut self, real_seconds_per_day: f32) {
self.speed = 86400.0 / real_seconds_per_day;
}
pub fn day_duration(&self) -> f32 {
86400.0 / self.speed
}
pub fn current_datetime(&self) -> NaiveDateTime {
let duration = Duration::milliseconds((self.elapsed_seconds * 1000.0) as i64);
self.start_datetime + duration
}
pub fn as_hms(&self) -> (u32, u32, u32) {
let dt = self.current_datetime();
(dt.hour(), dt.minute(), dt.second())
}
pub fn current_date(&self) -> (i32, u32, u32) {
let dt = self.current_datetime();
(dt.year(), dt.month(), dt.day())
}
pub fn current_time(&self) -> (u32, u32, u32) {
self.as_hms()
}
pub fn format_date(&self, format: Option<&str>) -> String {
let dt = self.current_datetime();
let fmt = format.unwrap_or("%Y-%m-%d");
dt.format(fmt).to_string()
}
pub fn format_time(&self, format: Option<&str>) -> String {
let dt = self.current_datetime();
let fmt = format.unwrap_or("%H:%M:%S");
dt.format(fmt).to_string()
}
pub fn format_datetime(&self, format: Option<&str>) -> String {
let dt = self.current_datetime();
let fmt = format.unwrap_or("%Y-%m-%d %H:%M:%S");
dt.format(fmt).to_string()
}
}
fn update_clock(mut clock: ResMut<InGameClock>, time: Res<Time>) {
if !clock.paused {
clock.elapsed_seconds += time.delta_secs_f64() * clock.speed as f64;
}
}
fn check_intervals(
clock: Res<InGameClock>,
mut trackers: ResMut<ClockIntervalTrackers>,
mut events: MessageWriter<ClockIntervalEvent>,
) {
if clock.paused {
return;
}
for tracker in &mut trackers.trackers {
let interval_seconds = tracker.interval.as_seconds() as f64;
let current_intervals = (clock.elapsed_seconds / interval_seconds).floor() as u64;
let previous_intervals = (tracker.last_trigger_seconds / interval_seconds).floor() as u64;
for _ in previous_intervals..current_intervals {
tracker.count += 1;
events.write(ClockIntervalEvent {
interval: tracker.interval,
count: tracker.count,
});
}
tracker.last_trigger_seconds = clock.elapsed_seconds;
}
}
pub trait ClockCommands {
fn register_clock_interval(&mut self, interval: ClockInterval);
}
impl ClockCommands for Commands<'_, '_> {
fn register_clock_interval(&mut self, interval: ClockInterval) {
self.queue(move |world: &mut World| {
InGameClock::register_interval(world, interval);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clock_default() {
let clock = InGameClock::default();
assert_eq!(clock.elapsed_seconds, 0.0);
assert_eq!(clock.speed, 1.0);
assert!(!clock.paused);
}
#[test]
fn test_clock_with_speed() {
let clock = InGameClock::new().with_speed(2.0);
assert_eq!(clock.speed, 2.0);
}
#[test]
fn test_clock_pause() {
let mut clock = InGameClock::new();
assert!(!clock.paused);
clock.pause();
assert!(clock.paused);
clock.resume();
assert!(!clock.paused);
}
#[test]
fn test_clock_toggle_pause() {
let mut clock = InGameClock::new();
assert!(!clock.paused);
clock.toggle_pause();
assert!(clock.paused);
clock.toggle_pause();
assert!(!clock.paused);
}
#[test]
fn test_as_hms() {
let mut clock = InGameClock::with_start_datetime(2024, 1, 1, 0, 0, 0);
assert_eq!(clock.as_hms(), (0, 0, 0));
clock.elapsed_seconds = 3723.0;
assert_eq!(clock.as_hms(), (1, 2, 3));
clock.elapsed_seconds = 86400.0 + 3600.0; assert_eq!(clock.as_hms(), (1, 0, 0));
}
#[test]
fn test_with_start_datetime() {
let clock = InGameClock::with_start_datetime(2024, 1, 15, 10, 30, 45);
assert_eq!(clock.elapsed_seconds, 0.0);
let (year, month, day) = clock.current_date();
assert_eq!((year, month, day), (2024, 1, 15));
let (hour, minute, second) = clock.current_time();
assert_eq!((hour, minute, second), (10, 30, 45));
}
#[test]
fn test_current_datetime() {
let mut clock = InGameClock::with_start_datetime(2024, 1, 15, 10, 30, 45);
let (year, month, day) = clock.current_date();
let (hour, minute, second) = clock.current_time();
assert_eq!((year, month, day), (2024, 1, 15));
assert_eq!((hour, minute, second), (10, 30, 45));
clock.elapsed_seconds = 3600.0;
let (year, month, day) = clock.current_date();
let (hour, minute, second) = clock.current_time();
assert_eq!((year, month, day), (2024, 1, 15));
assert_eq!((hour, minute, second), (11, 30, 45));
clock.elapsed_seconds = 14.0 * 3600.0; let (year, month, day) = clock.current_date();
let (hour, minute, second) = clock.current_time();
assert_eq!((year, month, day), (2024, 1, 16));
assert_eq!((hour, minute, second), (0, 30, 45));
}
#[test]
fn test_format_date() {
let clock = InGameClock::with_start_datetime(2024, 3, 5, 0, 0, 0);
assert_eq!(clock.format_date(None), "2024-03-05");
assert_eq!(clock.format_date(Some("%d/%m/%Y")), "05/03/2024");
assert_eq!(clock.format_date(Some("%B %d, %Y")), "March 05, 2024");
}
#[test]
fn test_format_datetime() {
let clock = InGameClock::with_start_datetime(2024, 12, 31, 23, 59, 59);
assert_eq!(clock.format_datetime(None), "2024-12-31 23:59:59");
assert_eq!(clock.format_datetime(Some("%d/%m/%Y %H:%M")), "31/12/2024 23:59");
}
#[test]
fn test_format_time() {
let clock = InGameClock::with_start_datetime(2024, 6, 15, 14, 30, 45);
assert_eq!(clock.format_time(None), "14:30:45");
assert_eq!(clock.format_time(Some("%I:%M %p")), "02:30 PM");
assert_eq!(clock.format_time(Some("%H:%M")), "14:30");
}
#[test]
fn test_month_overflow() {
let mut clock = InGameClock::with_start_datetime(2024, 1, 31, 0, 0, 0);
clock.elapsed_seconds = 24.0 * 3600.0;
let (year, month, day) = clock.current_date();
assert_eq!((year, month, day), (2024, 2, 1));
}
#[test]
fn test_year_overflow() {
let mut clock = InGameClock::with_start_datetime(2024, 12, 31, 23, 0, 0);
clock.elapsed_seconds = 3600.0;
let (year, month, day) = clock.current_date();
let (hour, _, _) = clock.current_time();
assert_eq!((year, month, day), (2025, 1, 1));
assert_eq!(hour, 0);
}
#[test]
fn test_with_day_duration() {
let clock = InGameClock::new().with_day_duration(60.0);
assert_eq!(clock.speed, 1440.0);
let clock = InGameClock::new().with_day_duration(1200.0);
assert_eq!(clock.speed, 72.0);
}
#[test]
fn test_set_day_duration() {
let mut clock = InGameClock::new();
clock.set_day_duration(120.0);
assert_eq!(clock.speed, 720.0);
assert_eq!(clock.day_duration(), 120.0);
clock.set_day_duration(86400.0);
assert_eq!(clock.speed, 1.0);
assert_eq!(clock.day_duration(), 86400.0);
}
#[test]
fn test_day_duration_getter() {
let clock = InGameClock::new().with_speed(1440.0);
assert_eq!(clock.day_duration(), 60.0);
let clock = InGameClock::new().with_speed(1.0);
assert_eq!(clock.day_duration(), 86400.0);
}
#[test]
fn test_clock_interval_as_seconds() {
assert_eq!(ClockInterval::Second.as_seconds(), 1);
assert_eq!(ClockInterval::Minute.as_seconds(), 60);
assert_eq!(ClockInterval::Hour.as_seconds(), 3600);
assert_eq!(ClockInterval::Day.as_seconds(), 86400);
assert_eq!(ClockInterval::Week.as_seconds(), 604800);
assert_eq!(ClockInterval::Custom(90).as_seconds(), 90);
}
}