diff --git a/src/gfx.rs b/src/gfx.rs index 9cdcb4c..06ac132 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -94,6 +94,9 @@ pub const I_CATAPULT: u32 = 210; // catapult tool pub const T_SNOW: u32 = 211; // 4 variants: snow on the land above ground (winter) pub const T_PUMPKIN: u32 = 215; // a carved pumpkin (Halloween) pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glowing spores +pub const T_GIFT: u32 = 220; // a wrapped present (Christmas) +pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas) +pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter) pub struct Gfx { pub atlas: Texture2D, @@ -1256,6 +1259,38 @@ fn gen_atlas() -> Image { Some(c(240, 128, 30)) } }); + // Christmas: a present in red paper with a golden ribbon, fir needles with baubles + cv.cell(T_GIFT, |x, y| { + if x == 7 || x == 8 || y == 7 || y == 8 { + Some(c(250, 210, 80)) + } else if (x + y) % 5 == 0 { + Some(c(200, 30, 40)) + } else { + Some(c(176, 24, 34)) + } + }); + cv.cell(T_FIR, |x, y| { + let r = hash(x, y, 470); + if r % 29 == 0 { + Some([c(240, 60, 60), c(250, 210, 80), c(90, 160, 255)][(r / 29 % 3) as usize]) + } else if (x + 2 * y) % 5 == 0 { + Some(c(20, 70, 40)) + } else { + Some(shade((34, 104, 56), (rnd(x, y, 471) * 16.0) as i32 - 8)) + } + }); + // Easter: painted eggs, stripes and zigzags in spring colours + for v in 0..2u32 { + cv.cell(T_EGG + v, |x, y| { + let cols = if v == 0 { [c(140, 200, 250), c(250, 230, 120), c(255, 255, 255)] } else { [c(250, 160, 200), c(150, 220, 140), c(255, 250, 230)] }; + let zig = (y + (x % 4 - 2).abs()) % 8; + Some(match zig { + 0 | 1 => cols[1], + 4 => cols[2], + _ => cols[0], + }) + }); + } // grass above ground for v in 0..4u32 { cv.cell(T_GRASS + v, |x, y| { diff --git a/src/holiday.rs b/src/holiday.rs new file mode 100644 index 0000000..31227d7 --- /dev/null +++ b/src/holiday.rs @@ -0,0 +1,109 @@ +// 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 + } +} + +// today's holiday (worked out once); 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 = const { std::cell::Cell::new(255) }; + } + 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 = 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); + } +} diff --git a/src/main.rs b/src/main.rs index f73a7fd..34570a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod fog; mod fortress; mod game; mod gfx; +mod holiday; mod i18n; mod lobby; mod map; diff --git a/src/menu.rs b/src/menu.rs index 2cd28e5..1daed45 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -351,6 +351,113 @@ const CLAUDE_COL: Color = Color { r: 0xd9 as f32 / 255.0, g: 0x77 as f32 / 255.0 const QWEN_COL: Color = Color { r: 0x7b as f32 / 255.0, g: 0x6c as f32 / 255.0, b: 0xf6 as f32 / 255.0, a: 1.0 }; const OPENCODE_COL: Color = Color { r: 0.86, g: 0.86, b: 0.88, a: 1.0 }; +// ---- holiday decorations around the title (see crate::holiday) +const BUNNY: [&str; 10] = [ + ".#...#.", + ".#...#.", + ".#...#.", + ".##.##.", + ".#####.", + "##.#.##", + "#######", + ".#####.", + "#######", + ".##.##.", +]; +const BUNNY_PINK: [&str; 10] = [ + ".......", + ".......", + ".#...#.", + ".......", + ".......", + ".......", + "...#...", + ".......", + ".......", + ".......", +]; +const EGG: [&str; 8] = ["..##..", ".####.", ".####.", "######", "######", "######", ".####.", "..##.."]; +const EGG_BAND: [&str; 8] = ["......", "......", "......", "######", "......", "#.##.#", "......", "......"]; +const PUMPKIN: [&str; 7] = ["...#....", "...##...", ".######.", "########", "########", "########", ".######."]; +const PUMPKIN_STEM: [&str; 7] = ["...#....", "...##...", "........", "........", "........", "........", "........"]; +const PUMPKIN_FACE: [&str; 7] = ["........", "........", "........", ".##..##.", "........", ".######.", "..#..#.."]; +const BAT_UP: [&str; 4] = ["#.......#", "##.#.#.##", "#########", "...#.#..."]; +const BAT_DOWN: [&str; 4] = ["...#.#...", "#########", "##.....##", "#.......#"]; + +// the title's decoration for the current holiday: string lights (Christmas), bunnies and eggs +// (Easter), pumpkins and bats (Halloween); (x, y, w, h) is the title text +fn draw_holiday_title(x: f32, y: f32, w: f32, h: f32) { + let px = (SMALL() * 2.0).round().max(2.0); + let t = get_time() as f32; + match crate::holiday::current() { + crate::holiday::CHRISTMAS => { + // a wire sagging between nails above the letters, bulbs hanging from it, blinking + let (x0, x1) = (x - px * 4.0, x + w + px * 4.0); + let spans = 5; + let span = (x1 - x0) / spans as f32; + let wire_y = |wx: f32| { + let f = ((wx - x0) / span).fract(); + y - px * 3.0 + (f * std::f32::consts::PI).sin() * px * 5.0 + }; + let wire = Color::new(0.12, 0.2, 0.12, 1.0); + let mut wx = x0; + while wx < x1 { + draw_rectangle(wx.floor(), wire_y(wx).floor(), px, px * 0.5, wire); + wx += px * 0.5; + } + let cols = [Color::new(1.0, 0.25, 0.25, 1.0), Color::new(1.0, 0.85, 0.2, 1.0), Color::new(0.3, 0.9, 0.35, 1.0), Color::new(0.35, 0.6, 1.0, 1.0)]; + let bulbs = ((x1 - x0) / (px * 6.0)) as i32; + for k in 1..bulbs { + let bx = (x0 + k as f32 * px * 6.0).floor(); + let by = wire_y(bx).floor() + px * 0.5; + let on = ((t * 2.0) as i32 + k) % 3 != 0; + let c = cols[k as usize % 4]; + let c = if on { c } else { Color::new(c.r * 0.35, c.g * 0.35, c.b * 0.35, 1.0) }; + draw_rectangle(bx, by, px, px * 0.5, wire); + draw_rectangle(bx - px * 0.5, by + px * 0.5, px * 2.0, px * 2.0, c); + if on { + draw_rectangle(bx - px * 1.5, by - px * 0.5, px * 4.0, px * 4.0, Color::new(c.r, c.g, c.b, 0.18)); + } + } + } + crate::holiday::EASTER => { + let white = Color::new(0.95, 0.95, 0.92, 1.0); + let pink = Color::new(1.0, 0.6, 0.75, 1.0); + let by = y + h - 10.0 * px; + for (k, bx) in [x - px * 10.0, x + w + px * 3.0].into_iter().enumerate() { + // a little hop now and then + let hop = ((t * 1.5 + k as f32 * 1.7).sin().max(0.0).powi(8) * px * 3.0).floor(); + draw_mark(&BUNNY, bx, by - hop, px, white); + draw_mark(&BUNNY_PINK, bx, by - hop, px, pink); + } + let eggs = [(0.12, Color::new(0.55, 0.78, 1.0, 1.0), Color::new(1.0, 0.9, 0.4, 1.0)), (0.32, Color::new(1.0, 0.62, 0.8, 1.0), Color::new(0.6, 0.86, 0.55, 1.0)), (0.55, Color::new(0.98, 0.9, 0.45, 1.0), Color::new(0.95, 0.45, 0.45, 1.0)), (0.78, Color::new(0.65, 0.9, 0.65, 1.0), Color::new(0.75, 0.6, 1.0, 1.0))]; + for (f, body, band) in eggs { + let ex = (x + w * f).floor(); + let ey = (y - px * 9.0).floor(); + draw_mark(&EGG, ex, ey, px, body); + draw_mark(&EGG_BAND, ex, ey, px, band); + } + } + crate::holiday::HALLOWEEN => { + let by = y + h - 7.0 * px; + for bx in [x - px * 11.0, x + w + px * 3.0] { + draw_mark(&PUMPKIN, bx, by, px, Color::new(0.95, 0.5, 0.12, 1.0)); + draw_mark(&PUMPKIN_STEM, bx, by, px, Color::new(0.3, 0.55, 0.2, 1.0)); + let glow = 0.75 + 0.25 * (t * 5.0).sin(); + draw_mark(&PUMPKIN_FACE, bx, by, px, Color::new(1.0, 0.85 * glow, 0.25, 1.0)); + } + for k in 0..3 { + let f = k as f32 * 0.33 + 0.12; + let bx = (x + w * f + (t * 0.7 + k as f32).sin() * px * 6.0).floor(); + let by = (y - px * 8.0 + (t * 1.3 + k as f32 * 2.0).sin() * px * 2.0).floor(); + let up = ((t * 6.0) as i32 + k) % 2 == 0; + draw_mark(if up { &BAT_UP } else { &BAT_DOWN }, bx, by, px, Color::new(0.12, 0.08, 0.16, 1.0)); + } + } + _ => {} + } +} + fn draw_mark(mark: &[&str], x: f32, y: f32, px: f32, c: Color) { for (r, row) in mark.iter().enumerate() { for (k, ch) in row.chars().enumerate() { @@ -1121,6 +1228,7 @@ impl Menu { let ty = (sh * 0.12 / CH()).floor() * CH(); gfx.text(title, grid_center(sw, gfx.measure(title, big)) + big, ty + big, big, Color::new(0.0, 0.0, 0.0, 0.8)); gfx.text(title, grid_center(sw, gfx.measure(title, big)), ty, big, FRAME_HI); + draw_holiday_title(grid_center(sw, gfx.measure(title, big)), ty, gfx.measure(title, big), GH * big); let sub = tr("menu.subtitle"); gfx.text(&sub, grid_center(sw, gfx.measure(&sub, S())), ty + GH * big + CH(), S(), TXT); let mut items = Vec::new(); diff --git a/src/render3d.rs b/src/render3d.rs index b3b7f3d..b60874b 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -509,7 +509,8 @@ impl MB { } } -// the season of the real date: 0 none, 1 Halloween (20 Oct - 5 Nov), 2 winter (Dec - Feb) +// the season of the real date: 0 none, 2 winter (Dec - Feb: snow above ground); the holidays +// are in crate::holiday pub fn season() -> u8 { thread_local! { static SEASON: std::cell::Cell = const { std::cell::Cell::new(255) }; @@ -518,7 +519,6 @@ pub fn season() -> u8 { if s.get() == 255 { let (_, m, d, _, _) = crate::cloud::civil(macroquad::miniquad::date::now() as i64); s.set(match (m, d) { - (10, 20..=31) | (11, 1..=5) => 1, (12, _) | (1, _) | (2, _) => 2, _ => 0, }); @@ -1007,13 +1007,36 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T } // room decor let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), lava_at(glow, xf + 0.5, zf + 0.5)), 1.0); - // Halloween: now and then a glowing pumpkin on the dungeon's plain floor - if season() == 1 && t.kind == Kind::Floor && t.owner == Owner::Player { + // holidays: now and then something on the dungeon's plain floor - a glowing + // pumpkin (Halloween), a little fir tree with a star and a present (Christmas), + // painted eggs (Easter) + let holiday = crate::holiday::current(); + if holiday != crate::holiday::NONE && t.kind == Kind::Floor && t.owner == Owner::Player { let hsh = (x as u32).wrapping_mul(2246822519) ^ (z as u32).wrapping_mul(3266489917); if (hsh >> 7) % 37 == 0 { let (cx, cz) = (xf + 0.3 + ((hsh >> 3) % 4) as f32 * 0.1, zf + 0.35); let pc = col(Vec3::splat(1.4), 1.0); - mb.boxy(vec3(cx - 0.14, 0.0, cz - 0.14), vec3(cx + 0.14, 0.24, cz + 0.14), T_PUMPKIN, pc, pc); + match holiday { + crate::holiday::HALLOWEEN => mb.boxy(vec3(cx - 0.14, 0.0, cz - 0.14), vec3(cx + 0.14, 0.24, cz + 0.14), T_PUMPKIN, pc, pc), + crate::holiday::CHRISTMAS => { + let fc = col(Vec3::splat(1.2), 1.0); + for (w, y0, y1) in [(0.22f32, 0.06f32, 0.26f32), (0.16, 0.24, 0.42), (0.1, 0.4, 0.56)] { + mb.boxy(vec3(cx - w, y0, cz - w), vec3(cx + w, y1, cz + w), crate::gfx::T_FIR, fc, fc); + } + mb.boxy(vec3(cx - 0.03, 0.0, cz - 0.03), vec3(cx + 0.03, 0.08, cz + 0.03), T_BARK, fc, fc); + mb.boxy(vec3(cx - 0.035, 0.56, cz - 0.035), vec3(cx + 0.035, 0.63, cz + 0.035), crate::gfx::T_GIFT, pc, pc); + let gx = cx + 0.26; + mb.boxy(vec3(gx - 0.08, 0.0, cz + 0.02), vec3(gx + 0.08, 0.13, cz + 0.18), crate::gfx::T_GIFT, pc, pc); + } + _ => { + for k in 0..3u32 { + let (ex, ez) = (cx + (k as f32 - 1.0) * 0.16, cz + ((hsh >> (k * 3)) % 3) as f32 * 0.08 - 0.08); + let egg = crate::gfx::T_EGG + (hsh >> (k + 11)) % 2; + mb.boxy(vec3(ex - 0.045, 0.0, ez - 0.035), vec3(ex + 0.045, 0.12, ez + 0.035), egg, pc, pc); + mb.boxy(vec3(ex - 0.03, 0.12, ez - 0.025), vec3(ex + 0.03, 0.15, ez + 0.025), egg, pc, pc); + } + } + } } } if t.plant() {