Files
dungeoncraft/src/holiday.rs
T

125 lines
4.1 KiB
Rust

// Holidays of the real date: the dungeon and the title menu dress up for them (pumpkins,
// fir trees and presents, painted eggs; see render3d's room decor and menu's draw_title).
// Every holiday runs from about two weeks before its day to a few days after it.
pub const NONE: u8 = 0;
pub const HALLOWEEN: u8 = 1;
pub const CHRISTMAS: u8 = 2;
pub const EASTER: u8 = 3;
// days since 1970-01-01 of a date (proleptic Gregorian)
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let mp = (m + 9) % 12;
let doy = (153 * mp + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
// Easter Sunday of a year (month, day): the anonymous Gregorian algorithm
pub fn easter(y: i64) -> (i64, i64) {
let a = y % 19;
let b = y / 100;
let c = y % 100;
let d = b / 4;
let e = b % 4;
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let i = c / 4;
let k = c % 4;
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let n = h + l - 7 * m + 114;
(n / 31, n % 31 + 1)
}
// the holiday on a date: Halloween 17 Oct - 3 Nov, Christmas 10 Dec - 1 Jan, Easter from two
// weeks before Easter Sunday to the Wednesday after it
pub fn holiday_on(y: i64, m: i64, d: i64) -> u8 {
match (m, d) {
(10, 17..=31) | (11, 1..=3) => return HALLOWEEN,
(12, 10..=31) | (1, 1) => return CHRISTMAS,
_ => {}
}
let (em, ed) = easter(y);
let off = days_from_civil(y, m, d) - days_from_civil(y, em, ed);
if (-14..=3).contains(&off) {
EASTER
} else {
NONE
}
}
thread_local! {
// the debug menu's choice: 0 by the date, else the holiday + 1
static OVERRIDE: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
}
// the debug menu's holiday setting (Settings::debug_holiday: 0 by the date, 1 none, 2 Halloween,
// 3 Christmas, 4 Easter)
pub fn set_override(v: u8) {
OVERRIDE.with(|o| o.set(v.min(4)));
}
// today's holiday (worked out once) unless the debug menu picks one; on the desktop
// DUNGEONCRAFT_HOLIDAY=halloween / christmas / easter / none shows one at any time
pub fn current() -> u8 {
thread_local! {
static HOLIDAY: std::cell::Cell<u8> = const { std::cell::Cell::new(255) };
}
let forced = OVERRIDE.with(|o| o.get());
if forced > 0 {
return forced - 1;
}
HOLIDAY.with(|h| {
if h.get() == 255 {
#[cfg(not(target_arch = "wasm32"))]
let forced = std::env::var("DUNGEONCRAFT_HOLIDAY").ok().and_then(|v| match v.to_lowercase().as_str() {
"halloween" => Some(HALLOWEEN),
"christmas" | "xmas" => Some(CHRISTMAS),
"easter" => Some(EASTER),
"none" => Some(NONE),
_ => None,
});
#[cfg(target_arch = "wasm32")]
let forced: Option<u8> = None;
let (y, m, d, _, _) = crate::cloud::civil(macroquad::miniquad::date::now() as i64);
h.set(forced.unwrap_or_else(|| holiday_on(y, m, d)));
}
h.get()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easter_dates() {
assert_eq!(easter(2024), (3, 31));
assert_eq!(easter(2025), (4, 20));
assert_eq!(easter(2026), (4, 5));
assert_eq!(easter(2027), (3, 28));
}
#[test]
fn holiday_windows() {
assert_eq!(holiday_on(2026, 10, 16), NONE);
assert_eq!(holiday_on(2026, 10, 17), HALLOWEEN);
assert_eq!(holiday_on(2026, 11, 3), HALLOWEEN);
assert_eq!(holiday_on(2026, 11, 7), NONE);
assert_eq!(holiday_on(2026, 12, 9), NONE);
assert_eq!(holiday_on(2026, 12, 10), CHRISTMAS);
assert_eq!(holiday_on(2027, 1, 1), CHRISTMAS);
assert_eq!(holiday_on(2027, 1, 2), NONE);
// Easter 2026: 5 April
assert_eq!(holiday_on(2026, 3, 21), NONE);
assert_eq!(holiday_on(2026, 3, 22), EASTER);
assert_eq!(holiday_on(2026, 4, 8), EASTER);
assert_eq!(holiday_on(2026, 4, 9), NONE);
}
}