Holidays: Christmas and Easter beside Halloween, two weeks before to a few days after; decorations in the dungeon and around the title

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 22:40:27 +01:00
1 parent c23c4d28ec
commit 8f670554c7
5 files changed
+281 -5

No files matched your search

+35
View File
@@ -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| {
+109
View File
@@ -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<u8> = 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<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);
}
}
+1
View File
@@ -19,6 +19,7 @@ mod fog;
mod fortress;
mod game;
mod gfx;
mod holiday;
mod i18n;
mod lobby;
mod map;
+108
View File
@@ -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();
+28 -5
View File
@@ -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<u8> = 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() {