mod lighting;
mod time_of_day;
pub(super) use lighting::{
paint_ceiling_pool, paint_clock, paint_corridor_runner, paint_floor_lamp_halo,
paint_neon_panel, paint_shadow, Ellipse,
};
pub(super) use time_of_day::{
daylight_floor_overlay, dim_floor_overlay, set_weather_override, sun_on_wall, sunset_strength,
time_of_day_look, weather_light, weather_state, TimeOfDayLook, WallSide, Weather,
};
use std::time::SystemTime;
use pixtuoid_core::sprite::{Rgb, RgbBuffer};
use super::ambient::SunbeamColumn;
use super::epoch_ms;
use super::palette::{blend, blend_rgb, lerp_rgb};
fn local_hour_frac(now: std::time::SystemTime) -> f32 {
use chrono::Timelike;
let unix_now = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let local = chrono::DateTime::<chrono::Local>::from(std::time::UNIX_EPOCH + unix_now);
local.hour() as f32 + local.minute() as f32 / 60.0
}
use crate::tui::layout::{Layout, ELEVATOR_W};
use crate::tui::theme::Theme;
const WINDOW_W: u16 = 22;
const WINDOW_GAP: u16 = 3;
const SPILL_DEPTH: u16 = 12;
const LIGHTNING_PERIOD_MS: u64 = 15000;
const LIGHTNING_FLASH_MS: u64 = 90;
fn lightning_envelope(since_strike_ms: u64) -> f32 {
match since_strike_ms {
0..=24 => 1.0, 25..=39 => 0.15, 40..=69 => 0.55, _ => 0.0,
}
}
fn strike_offset(bucket: u64) -> u64 {
let mut h = bucket.wrapping_add(0x9e37_79b9_7f4a_7c15);
h = (h ^ (h >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
h = (h ^ (h >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
h ^= h >> 31;
h % (LIGHTNING_PERIOD_MS - LIGHTNING_FLASH_MS)
}
fn lightning_flash_level(now: SystemTime) -> f32 {
let elapsed_ms = epoch_ms(now);
let bucket = elapsed_ms / LIGHTNING_PERIOD_MS;
let phase = elapsed_ms % LIGHTNING_PERIOD_MS;
match phase.checked_sub(strike_offset(bucket)) {
Some(since) if since < LIGHTNING_FLASH_MS => lightning_envelope(since),
_ => 0.0,
}
}
pub(super) fn paint_lightning_flash(buf: &mut RgbBuffer, now: SystemTime, weather: Weather) {
if weather != Weather::Storm {
return;
}
let level = lightning_flash_level(now);
if level <= 0.0 {
return;
}
let alpha = 0.20 * level;
for y in 0..buf.height {
for x in 0..buf.width {
let cur = buf.get(x, y);
buf.put(
x,
y,
blend_rgb(
cur,
Rgb {
r: 255,
g: 255,
b: 255,
},
alpha,
),
);
}
}
}
pub(super) fn weather_floor_tint(w: Weather) -> Rgb {
match w {
Weather::Clear => Rgb {
r: 255,
g: 252,
b: 240,
},
Weather::Rain => Rgb {
r: 190,
g: 200,
b: 220,
},
Weather::Storm => Rgb {
r: 140,
g: 145,
b: 165,
},
Weather::Snow => Rgb {
r: 220,
g: 230,
b: 250,
},
Weather::Fog => Rgb {
r: 228,
g: 229,
b: 233,
},
Weather::Overcast => Rgb {
r: 210,
g: 210,
b: 215,
},
Weather::Windy => Rgb {
r: 248,
g: 248,
b: 245,
},
Weather::Smog => Rgb {
r: 215,
g: 200,
b: 165,
},
}
}
fn skyline_haze(w: Weather) -> Option<(Rgb, f32)> {
match w {
Weather::Fog => Some((
Rgb {
r: 226,
g: 228,
b: 233,
},
0.55,
)),
Weather::Storm => Some((
Rgb {
r: 120,
g: 126,
b: 142,
},
0.38,
)),
Weather::Rain => Some((
Rgb {
r: 168,
g: 178,
b: 198,
},
0.20,
)),
Weather::Smog => Some((
Rgb {
r: 150,
g: 138,
b: 110,
},
0.22,
)),
Weather::Overcast => Some((
Rgb {
r: 196,
g: 199,
b: 206,
},
0.12,
)),
_ => None,
}
}
pub(in crate::tui::pixel_painter) fn window_spill_columns(layout: &Layout) -> Vec<SunbeamColumn> {
let top_wall_h = layout
.top_margin
.saturating_sub(pixtuoid_core::layout::WALL_BAND_TO_TOP_MARGIN);
let skip = layout.door.map(|d| (d.x, d.x + ELEVATOR_W));
let mut out = Vec::new();
let mut x = 3u16;
while x + WINDOW_W + 2 <= layout.buf_w {
let overlaps_door = skip.is_some_and(|(dx0, dx1)| x < dx1 && x + WINDOW_W > dx0);
if !overlaps_door {
out.push(SunbeamColumn {
x: x + WINDOW_W / 2,
top_y: top_wall_h,
depth: SPILL_DEPTH,
});
}
x += WINDOW_W + WINDOW_GAP;
}
out
}
#[allow(clippy::too_many_arguments)]
pub(super) fn paint_floor_and_walls(
buf: &mut RgbBuffer,
buf_w: u16,
buf_h: u16,
now: SystemTime,
look: &TimeOfDayLook,
top_wall_h: u16,
skip_window_x_range: Option<(u16, u16)>,
theme: &Theme,
altitude: f32,
) {
let window_frame = theme.surface.window_frame;
let carpet_base = theme.surface.carpet_base;
let carpet_light = theme.surface.carpet_light;
let carpet_dark = theme.surface.carpet_dark;
let wall = theme.surface.wall;
let wall_trim_color = theme.surface.wall_trim;
let weather = weather_state(now);
let tint = weather_floor_tint(weather);
for y in 0..buf_h {
for x in 0..buf_w {
let hash = (x as u32)
.wrapping_mul(73)
.wrapping_add((y as u32).wrapping_mul(151))
^ ((x as u32).wrapping_mul(11) ^ (y as u32).wrapping_mul(37));
let color = match hash % 17 {
0 | 1 => carpet_light,
2 | 3 => carpet_dark,
_ => carpet_base,
};
buf.put(x, y, blend_rgb(color, tint, 0.15));
}
}
for y in 0..top_wall_h.min(buf_h) {
for x in 0..buf_w {
buf.put(x, y, wall);
}
}
let window_y: u16 = 1;
let window_h: u16 = top_wall_h.saturating_sub(2).max(8);
let mut x = 3u16;
let mut idx: u32 = 0;
while x + WINDOW_W + 2 <= buf_w {
let overlaps_door =
skip_window_x_range.is_some_and(|(dx0, dx1)| x < dx1 && x + WINDOW_W > dx0);
if !overlaps_door {
paint_floor_to_ceiling_window(
buf,
x,
window_y,
WINDOW_W,
window_h,
window_frame,
look,
idx as u16,
now,
theme,
weather,
altitude,
);
if look.spill_strength > 0.0 {
paint_window_light_spill(
buf,
x,
WINDOW_W,
top_wall_h,
look.spill_strength,
look.spill_slant,
theme,
);
}
}
x += WINDOW_W + WINDOW_GAP;
idx += 1;
}
let trim_y = top_wall_h.saturating_sub(1);
if trim_y < buf_h {
for x in 0..buf_w {
buf.put(x, trim_y, wall_trim_color);
}
}
}
fn city_dot_lit(window_idx: u16, dx: u16, dy: u16) -> bool {
let mut h = (window_idx as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
h ^= (dx as u64).wrapping_mul(0xc6a4_a793_5bd1_e995);
h ^= (dy as u64).wrapping_mul(0x1656_67b1_9e37_79b9);
h ^= h >> 17;
(h % 100) < 75
}
fn city_dot_twinkle(window_idx: u16, dx: u16, dy: u16, now: SystemTime) -> bool {
let now_ms = epoch_ms(now);
let dot_seed = (window_idx as u64).wrapping_mul(31)
^ (dx as u64).wrapping_mul(131)
^ (dy as u64).wrapping_mul(521);
let cycle_ms = 6000 + (dot_seed % 8000);
let phase = now_ms / cycle_ms;
let hash = dot_seed
.wrapping_add(phase)
.wrapping_mul(0x9e37_79b9_7f4a_7c15);
(hash % 10) < 7
}
fn paint_window_light_spill(
buf: &mut RgbBuffer,
window_x: u16,
window_w: u16,
top_y: u16,
intensity: f32,
slant_per_row: f32,
theme: &Theme,
) {
let warm = theme.lighting.sun_spill;
let fade_start = 0.32 * intensity;
for dy in 0..SPILL_DEPTH {
let widen = (dy / 2).min(3);
let shift = (slant_per_row * dy as f32).round() as i32;
let base_x = (window_x as i32 + shift).max(0) as u16;
let start_x = base_x.saturating_sub(widen);
let end_x = (base_x + window_w + widen).min(buf.width);
let y = top_y + dy;
if y >= buf.height {
break;
}
let strength = fade_start * (1.0 - dy as f32 / SPILL_DEPTH as f32);
for x in start_x..end_x {
let cur = buf.get(x, y);
buf.put(x, y, blend_rgb(cur, warm, strength));
}
}
}
#[derive(Clone, Copy)]
enum Particle {
Streak {
len_base: u16,
len_mod: u64,
alpha_base: f32,
alpha_falloff: f32,
drift: bool,
},
Flake,
}
struct StreakSpec {
count: u64,
seed_mult: u64,
sx_mult: u64,
speed_base: u64,
speed_span: u64,
color: Rgb,
particle: Particle,
}
#[derive(Clone, Copy)]
struct GlassRect {
x0: u16,
y0: u16,
w: u16,
h: u16,
}
fn paint_streaks(
buf: &mut RgbBuffer,
spec: &StreakSpec,
window_idx: u16,
glass: GlassRect,
elapsed_ms: u64,
) {
let GlassRect {
x0: glass_x0,
y0: glass_y0,
w: gw,
h: gh,
} = glass;
for i in 0..spec.count {
let seed = window_idx as u64 * spec.seed_mult + i;
let sx = (seed.wrapping_mul(spec.sx_mult) % gw as u64) as u16;
let speed = spec.speed_base + (seed.wrapping_mul(0x4f6c_dd1d) % spec.speed_span);
let offset = seed.wrapping_mul(0x85eb_ca6b) % (gh as u64).max(1);
let phase = (elapsed_ms / speed + offset) % gh as u64;
match spec.particle {
Particle::Streak {
len_base,
len_mod,
alpha_base,
alpha_falloff,
drift,
} => {
let len = len_base + (seed % len_mod) as u16;
for dy in 0..len {
let dx = if drift { dy / 2 } else { 0 };
let px = glass_x0 + (sx + dx) % gw;
let py = glass_y0 + ((phase as u16 + dy) % gh);
if px < buf.width && py < buf.height {
let alpha = alpha_base - (dy as f32 / len as f32) * alpha_falloff;
let cur = buf.get(px, py);
buf.put(px, py, blend_rgb(cur, spec.color, alpha));
}
}
}
Particle::Flake => {
let wiggle = if (elapsed_ms / 400 + seed.wrapping_mul(0x9e37)) % 2 == 0 {
0
} else {
1
};
let px = glass_x0 + (sx + wiggle) % gw;
let py = glass_y0 + phase as u16;
if px < buf.width && py < buf.height {
buf.put(px, py, spec.color);
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn paint_floor_to_ceiling_window(
buf: &mut RgbBuffer,
x: u16,
y: u16,
w: u16,
h: u16,
frame: Rgb,
look: &TimeOfDayLook,
window_idx: u16,
now: SystemTime,
theme: &Theme,
weather: Weather,
altitude: f32,
) {
let building_dark = theme.office.building_dark;
let building_light = theme.office.building_light;
let cw = theme.office.city_lit_windows;
let dark_window = theme.office.city_dark_window;
let lit_strength = look.darkness.max(0.12).clamp(0.0, 1.0);
let lit_colors: [Rgb; 3] = [
lerp_rgb(dark_window, cw[0], lit_strength),
lerp_rgb(dark_window, cw[1], lit_strength),
lerp_rgb(dark_window, cw[2], lit_strength),
];
let building = lerp_rgb(building_light, building_dark, look.darkness);
const SKYLINE_PATTERN: &[u8] = &[8, 14, 11, 15, 6, 13, 9, 12, 7, 15, 10, 13];
const PATTERN_MAX: u16 = 15;
let glass_h = h.saturating_sub(2);
let alt_shrink = (glass_h as f32 * 0.3 * altitude) as u16;
let min_bh = (glass_h / 5).saturating_sub(alt_shrink).max(2);
let max_bh = (glass_h * 50 / 100)
.saturating_sub(alt_shrink)
.max(min_bh + 3);
let bh_range = max_bh.saturating_sub(min_bh);
let sky_norm = (glass_h as f32) * 0.7;
let sky_row: Vec<Rgb> = (0..glass_h)
.map(|gy| {
let sky_t = (gy as f32 / sky_norm).min(1.0);
lerp_rgb(look.glass_b, look.glass_a, sky_t)
})
.collect();
for dy in 0..h {
for dx in 0..w {
let px = x + dx;
let py = y + dy;
if px >= buf.width || py >= buf.height {
continue;
}
let on_edge = dx == 0 || dx == w - 1 || dy == 0 || dy == h - 1;
let on_mullion = dx == w / 2 || dy == h * 7 / 10;
if on_edge || on_mullion {
buf.put(px, py, frame);
continue;
}
let glass_dx = dx - 1;
let glass_dy = dy - 1;
let pat_idx = ((glass_dx + window_idx * 3) % SKYLINE_PATTERN.len() as u16) as usize;
let pat = SKYLINE_PATTERN[pat_idx] as u16;
let building_h = min_bh + (pat * bh_range) / PATTERN_MAX;
let in_building = glass_dy >= glass_h.saturating_sub(building_h);
if in_building {
let bldg_y = glass_dy - (glass_h - building_h);
let on_grid = glass_dx % 2 == 1 && bldg_y % 2 == 1;
let lit_base = on_grid && city_dot_lit(window_idx, glass_dx, bldg_y);
if lit_base && city_dot_twinkle(window_idx, glass_dx, bldg_y, now) {
let dot_color = match (glass_dx.wrapping_add(bldg_y)) % 5 {
0 => lit_colors[1],
1 => lit_colors[2],
_ => lit_colors[0],
};
buf.put(px, py, dot_color);
} else {
buf.put(px, py, building);
}
} else {
buf.put(px, py, sky_row[glass_dy as usize]);
}
}
}
if let Some((haze, alpha)) = skyline_haze(weather) {
for dy in 1..h.saturating_sub(1) {
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(px, py, blend_rgb(cur, haze, alpha));
}
}
}
}
let elapsed_ms = epoch_ms(now);
match weather {
Weather::Rain => paint_streaks(
buf,
&StreakSpec {
count: 4,
seed_mult: 7,
sx_mult: 0x9e37_79b9,
speed_base: 60,
speed_span: 50,
color: Rgb {
r: 210,
g: 220,
b: 240,
},
particle: Particle::Streak {
len_base: 3,
len_mod: 2,
alpha_base: 0.35,
alpha_falloff: 0.15,
drift: false,
},
},
window_idx,
GlassRect {
x0: x + 1,
y0: y + 1,
w: w.saturating_sub(2),
h: h.saturating_sub(2),
},
elapsed_ms,
),
Weather::Storm => {
paint_streaks(
buf,
&StreakSpec {
count: 6,
seed_mult: 7,
sx_mult: 0x9e37_79b9,
speed_base: 40,
speed_span: 40,
color: Rgb {
r: 210,
g: 220,
b: 245,
},
particle: Particle::Streak {
len_base: 4,
len_mod: 3,
alpha_base: 0.6,
alpha_falloff: 0.3,
drift: false,
},
},
window_idx,
GlassRect {
x0: x + 1,
y0: y + 1,
w: w.saturating_sub(2),
h: h.saturating_sub(2),
},
elapsed_ms,
);
let level = lightning_flash_level(now);
if level > 0.0 {
let alpha = 0.6 * level;
for dy in 1..h.saturating_sub(1) {
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(
px,
py,
blend_rgb(
cur,
Rgb {
r: 255,
g: 255,
b: 255,
},
alpha,
),
);
}
}
}
}
}
Weather::Snow => paint_streaks(
buf,
&StreakSpec {
count: 3,
seed_mult: 11,
sx_mult: 0x517c_c1b7,
speed_base: 150,
speed_span: 100,
color: Rgb {
r: 240,
g: 240,
b: 250,
},
particle: Particle::Flake,
},
window_idx,
GlassRect {
x0: x + 1,
y0: y + 1,
w: w.saturating_sub(2),
h: h.saturating_sub(2),
},
elapsed_ms,
),
Weather::Fog => {
for dy in 1..h.saturating_sub(1) {
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(
px,
py,
blend_rgb(
cur,
Rgb {
r: 160,
g: 165,
b: 175,
},
0.25,
),
);
}
}
}
}
Weather::Overcast => {
for dy in 1..h.saturating_sub(1) {
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(
px,
py,
blend_rgb(
cur,
Rgb {
r: 100,
g: 105,
b: 110,
},
0.2,
),
);
}
}
}
}
Weather::Windy => paint_streaks(
buf,
&StreakSpec {
count: 5,
seed_mult: 7,
sx_mult: 0x9e37_79b9,
speed_base: 50,
speed_span: 40,
color: Rgb {
r: 210,
g: 220,
b: 240,
},
particle: Particle::Streak {
len_base: 3,
len_mod: 2,
alpha_base: 0.35,
alpha_falloff: 0.15,
drift: true,
},
},
window_idx,
GlassRect {
x0: x + 1,
y0: y + 1,
w: w.saturating_sub(2),
h: h.saturating_sub(2),
},
elapsed_ms,
),
Weather::Smog => {
for dy in 1..h.saturating_sub(1) {
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
buf.put(
px,
py,
blend_rgb(
cur,
Rgb {
r: 180,
g: 160,
b: 110,
},
0.30,
),
);
}
}
}
}
Weather::Clear => {}
}
let raw_sunset = sunset_strength(now);
let twilight_now = look.twilight;
let atmo = weather_light(weather);
let smog_boost = if matches!(weather, Weather::Smog) {
1.4
} else {
1.0
};
let sunset =
(raw_sunset * (1.0 - twilight_now * 0.8) * atmo.intensity * smog_boost).clamp(0.0, 1.0);
if sunset > 0.05 {
let min_building_h = (glass_h / 5).max(3);
for dy in 1..h.saturating_sub(1) {
let glass_dy = dy.saturating_sub(1);
if glass_dy >= glass_h.saturating_sub(min_building_h) {
continue;
}
for dx in 1..w.saturating_sub(1) {
let px = x + dx;
let py = y + dy;
if px < buf.width && py < buf.height {
let cur = buf.get(px, py);
let s = sunset * 0.35;
buf.put(
px,
py,
Rgb {
r: blend(cur.r, 255, s * 0.4),
g: blend(cur.g, 160, s * 0.25),
b: blend(cur.b, 60, s * 0.1),
},
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weather_floor_tint_differs_by_variant() {
let clear = weather_floor_tint(Weather::Clear);
let rain = weather_floor_tint(Weather::Rain);
let fog = weather_floor_tint(Weather::Fog);
assert_ne!(clear, rain, "rain biases floor cooler");
assert_ne!(clear, fog, "fog desaturates");
assert!(
rain.b >= rain.r,
"rain tint should be cool (blue >= red), got {:?}",
rain
);
}
#[test]
fn weather_floor_tint_clear_is_near_neutral() {
let clear = weather_floor_tint(Weather::Clear);
assert!(
clear.r > 200 && clear.g > 200 && clear.b > 200,
"clear should be a near-white slight-warm tint, got {:?}",
clear
);
}
#[test]
fn fog_floor_tint_is_brighter_than_overcast() {
let fog = weather_floor_tint(Weather::Fog);
let oc = weather_floor_tint(Weather::Overcast);
let lum = |c: Rgb| c.r as u16 + c.g as u16 + c.b as u16;
assert!(
lum(fog) > lum(oc),
"fog {fog:?} should outshine overcast {oc:?}"
);
}
#[test]
fn skyline_haze_obscures_fog_and_storm_only_when_expected() {
let fog = skyline_haze(Weather::Fog).expect("fog hazes").1;
let storm = skyline_haze(Weather::Storm).expect("storm hazes").1;
assert!(fog > storm, "fog should obscure more than storm");
assert!(
skyline_haze(Weather::Clear).is_none(),
"clear skyline is crisp"
);
assert!(
skyline_haze(Weather::Snow).is_none(),
"snow skyline is crisp"
);
}
#[test]
fn lightning_envelope_is_a_two_pulse_then_dark() {
assert_eq!(lightning_envelope(0), 1.0, "primary strike");
assert!(
lightning_envelope(30) < lightning_envelope(0),
"dim between flickers"
);
assert!(
lightning_envelope(50) > lightning_envelope(30),
"after-flash rebrightens"
);
assert_eq!(lightning_envelope(LIGHTNING_FLASH_MS), 0.0, "flash is over");
assert_eq!(lightning_envelope(5000), 0.0, "dark between strikes");
}
#[test]
fn lightning_flash_storm_only_and_mid_strike_only() {
use std::time::{Duration, UNIX_EPOCH};
let bucket = (0u64..)
.find(|&b| strike_offset(b) < 500)
.expect("a low-offset bucket exists");
let off = strike_offset(bucket);
let at = |ms: u64| UNIX_EPOCH + Duration::from_millis(bucket * LIGHTNING_PERIOD_MS + ms);
let mk = || {
RgbBuffer::filled(
8,
4,
Rgb {
r: 10,
g: 10,
b: 12,
},
)
};
let mut b = mk();
paint_lightning_flash(&mut b, at(off), Weather::Storm);
assert!(b.get(0, 0).r > 10, "storm strike should brighten the room");
let mut b = mk();
paint_lightning_flash(&mut b, at(off + 1000), Weather::Storm);
assert_eq!(
b.get(0, 0),
Rgb {
r: 10,
g: 10,
b: 12
},
"no flash between strikes"
);
let mut b = mk();
paint_lightning_flash(&mut b, at(off), Weather::Clear);
assert_eq!(
b.get(0, 0),
Rgb {
r: 10,
g: 10,
b: 12
},
"flash is storm-only"
);
}
#[test]
fn lightning_strikes_are_jittered_not_metronomic() {
let offsets: Vec<u64> = (0..24u64).map(strike_offset).collect();
let distinct = offsets
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
assert!(
distinct > 12,
"strike offsets should vary across buckets, got {offsets:?}"
);
assert!(offsets
.iter()
.all(|&o| o < LIGHTNING_PERIOD_MS - LIGHTNING_FLASH_MS));
}
#[test]
fn storm_window_bolt_brightens_glass_during_the_flash() {
use std::time::{Duration, UNIX_EPOCH};
let bucket = (0u64..)
.find(|&b| strike_offset(b) < 500)
.expect("a low-offset bucket exists");
let off = strike_offset(bucket);
let at = |ms: u64| UNIX_EPOCH + Duration::from_millis(bucket * LIGHTNING_PERIOD_MS + ms);
assert!(
lightning_flash_level(at(off)) > 0.0,
"flash at strike offset"
);
assert_eq!(
lightning_flash_level(at(off + 1000)),
0.0,
"quiet 1 s later"
);
let theme = crate::tui::theme::theme_by_name("normal").expect("theme");
let render_lum = |now: SystemTime| -> u64 {
let look = time_of_day_look(now, theme);
let mut buf = RgbBuffer::filled(40, 40, Rgb { r: 8, g: 8, b: 10 });
paint_floor_to_ceiling_window(
&mut buf,
0,
0,
WINDOW_W,
30,
theme.surface.window_frame,
&look,
0,
now,
theme,
Weather::Storm,
0.0,
);
let mut sum = 0u64;
for y in 1..29u16 {
for x in 1..(WINDOW_W - 1) {
let p = buf.get(x, y);
sum += p.r as u64 + p.g as u64 + p.b as u64;
}
}
sum
};
let flashing = render_lum(at(off));
let quiet = render_lum(at(off + 1000));
assert!(
flashing > quiet,
"the on-glass bolt must brighten the storm glass during the flash \
(flash={flashing}, quiet={quiet})"
);
}
#[test]
fn short_buffer_clamps_spill_and_window_without_panic() {
let theme = crate::tui::theme::theme_by_name("normal").expect("theme");
let top_wall_h = 18u16;
let buf_h = top_wall_h + 2;
let buf_w = 60u16;
let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(12 * 3600);
let look = TimeOfDayLook {
glass_a: theme.office.building_light,
glass_b: theme.office.building_dark,
spill_strength: 0.8,
spill_slant: 0.0,
darkness: 0.2,
twilight: 0.0,
};
let mut buf = RgbBuffer::filled(buf_w, buf_h, Rgb { r: 5, g: 5, b: 5 });
paint_floor_and_walls(
&mut buf, buf_w, buf_h, now, &look, top_wall_h, None, theme, 0.0,
);
assert_ne!(
buf.get(0, 0),
Rgb { r: 5, g: 5, b: 5 },
"the wall band should still paint in the in-bounds rows"
);
}
}