mod calendar;
pub use calendar::{Calendar, GregorianCalendar, Month, Epoch, CustomCalendar, CustomCalendarBuilder};
use bevy::prelude::*;
use chrono::{Duration, NaiveDateTime, Timelike, Utc};
use std::sync::Arc;
#[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, calendar: &dyn Calendar) -> u32 {
match self {
ClockInterval::Second => 1,
ClockInterval::Minute => 60,
ClockInterval::Hour => calendar.seconds_per_hour(),
ClockInterval::Day => calendar.seconds_per_day(),
ClockInterval::Week => calendar.seconds_per_week(),
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, Clone)]
pub struct InGameClock {
pub elapsed_seconds: f64,
pub speed: f32,
pub paused: bool,
pub start_datetime: NaiveDateTime,
calendar: Arc<dyn Calendar>,
}
impl std::fmt::Debug for InGameClock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InGameClock")
.field("elapsed_seconds", &self.elapsed_seconds)
.field("speed", &self.speed)
.field("paused", &self.paused)
.field("start_datetime", &self.start_datetime)
.field("calendar", &"<Calendar>")
.finish()
}
}
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,
calendar: Arc::new(GregorianCalendar),
}
}
}
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,
calendar: Arc::new(GregorianCalendar),
}
}
pub fn with_calendar(mut self, calendar: impl Calendar + 'static) -> Self {
self.calendar = Arc::new(calendar);
self
}
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 {
let calendar_seconds_per_day = self.calendar.seconds_per_day() as f32;
self.speed = calendar_seconds_per_day / 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_elapsed_seconds(&mut self, seconds: f64) {
self.elapsed_seconds = seconds;
}
pub fn set_day_duration(&mut self, real_seconds_per_day: f32) {
let calendar_seconds_per_day = self.calendar.seconds_per_day() as f32;
self.speed = calendar_seconds_per_day / real_seconds_per_day;
}
pub fn day_duration(&self) -> f32 {
let calendar_seconds_per_day = self.calendar.seconds_per_day() as f32;
calendar_seconds_per_day / 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) {
self.calendar.get_date(self.elapsed_seconds, self.start_datetime)
}
pub fn current_time(&self) -> (u32, u32, u32) {
self.calendar.get_time(self.elapsed_seconds, self.start_datetime)
}
pub fn format_date(&self, format: Option<&str>) -> String {
self.calendar.format_date(self.elapsed_seconds, self.start_datetime, format)
}
pub fn format_time(&self, format: Option<&str>) -> String {
self.calendar.format_time(self.elapsed_seconds, self.start_datetime, format)
}
pub fn format_datetime(&self, format: Option<&str>) -> String {
self.calendar.format_datetime(self.elapsed_seconds, self.start_datetime, format)
}
pub fn calendar(&self) -> &Arc<dyn Calendar> {
&self.calendar
}
}
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(clock.calendar().as_ref()) 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_custom_calendar_intervals() {
let custom_calendar = CustomCalendar::builder()
.minutes_per_hour(60)
.hours_per_day(20)
.month(Month::new("Month1", 20, 0))
.weekdays(vec![
"Day1".to_string(),
"Day2".to_string(),
"Day3".to_string(),
"Day4".to_string(),
"Day5".to_string(),
])
.leap_years("false")
.epoch(Epoch::new("Test Epoch", 0))
.build();
assert_eq!(ClockInterval::Second.as_seconds(&custom_calendar), 1);
assert_eq!(ClockInterval::Minute.as_seconds(&custom_calendar), 60);
assert_eq!(ClockInterval::Hour.as_seconds(&custom_calendar), 3600); assert_eq!(ClockInterval::Day.as_seconds(&custom_calendar), 72000); assert_eq!(ClockInterval::Week.as_seconds(&custom_calendar), 360000); assert_eq!(ClockInterval::Custom(90).as_seconds(&custom_calendar), 90);
}
#[test]
fn test_clock_interval_as_seconds() {
let gregorian = GregorianCalendar;
assert_eq!(ClockInterval::Second.as_seconds(&gregorian), 1);
assert_eq!(ClockInterval::Minute.as_seconds(&gregorian), 60);
assert_eq!(ClockInterval::Hour.as_seconds(&gregorian), 3600);
assert_eq!(ClockInterval::Day.as_seconds(&gregorian), 86400);
assert_eq!(ClockInterval::Week.as_seconds(&gregorian), 604800);
assert_eq!(ClockInterval::Custom(90).as_seconds(&gregorian), 90);
}
#[test]
fn test_custom_calendar_builder_integration_with_clock() {
let calendar = CustomCalendar::builder()
.minutes_per_hour(20)
.hours_per_day(8)
.month(Month::new("Month1", 30, 0))
.weekday("Day1")
.weekday("Day2")
.weekday("Day3")
.weekday("Day4")
.weekday("Day5")
.leap_years("false")
.epoch(Epoch::new("Test Epoch", 0))
.build();
let clock = InGameClock::new()
.with_calendar(calendar)
.with_day_duration(60.0);
let (year, month, day) = clock.current_date();
assert_eq!(year, 0);
assert_eq!(month, 1);
assert_eq!(day, 1);
}
}