//! Ambient traffic on the arterials: purely visual, no simulation beyond position. use crate::render::style::StylePack; use crate::render::viewport::{Overlay, tile_to_screen}; use citygen::rng::hash64; use citygen::{City, Dir, Hue, Pos, Rect as TileRect, StratumKind, Terrain}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use std::collections::HashSet; pub const MAX_CARS: usize = 24; const MAX_AGE: u32 = 600; #[derive(Clone, Copy, Debug)] pub struct Car { pub pos: Pos, pub dir: Dir, pub hue: Hue, pub age: u32, } #[derive(Default)] pub struct Traffic { pub cars: Vec, arterial: HashSet, arterial_key: Option<(u64, u32, u32)>, } impl Traffic { fn ensure_arterials(&mut self, city: &City) { let key = (city.seed, city.size.w, city.size.h); if self.arterial_key == Some(key) { return; } self.arterial.clear(); for path in &city.arterials { for p in path { for q in TileRect::new(p.x - 1, p.y - 1, 4, 4).iter() { self.arterial.insert(q); } } } self.arterial_key = Some(key); self.cars.clear(); } fn drivable(&self, city: &City, p: Pos) -> bool { self.arterial.contains(&p) && city .tile(StratumKind::Street, p) .is_some_and(|t| t.terrain == Terrain::Road && t.feature.is_none()) } /// Advances cars and spawns new ones at the edge of `view` (in tiles). Only the Street /// has traffic. pub fn update(&mut self, city: &City, stratum: StratumKind, view: TileRect, tick: u64) { if stratum != StratumKind::Street { return; } self.ensure_arterials(city); if tick.is_multiple_of(2) { let cars = std::mem::take(&mut self.cars); let mut kept = Vec::with_capacity(cars.len()); for mut car in cars { car.age += 1; if car.age > MAX_AGE { continue; } let ahead = car.pos.step(car.dir); if self.drivable(city, ahead) { car.pos = ahead; kept.push(car); continue; } let turns = [turn_left(car.dir), turn_right(car.dir), car.dir.opposite()]; if let Some(d) = turns .into_iter() .find(|d| self.drivable(city, car.pos.step(*d))) { car.dir = d; car.pos = car.pos.step(d); kept.push(car); } } self.cars = kept; } if tick.is_multiple_of(10) && self.cars.len() < MAX_CARS { let edge: Vec = view .border() .filter(|p| self.drivable(city, *p)) .filter(|p| self.cars.iter().all(|c| c.pos != *p)) .collect(); let h = hash64(tick, city.seed); // No arterial crosses the view edge: pick a drivable tile inside instead. let inside = (0..64).find_map(|i| { let hh = hash64(h, i); let p = Pos::new( view.x + (hh % view.w.max(1) as u64) as i32, view.y + ((hh >> 20) % view.h.max(1) as u64) as i32, ); (self.drivable(city, p) && self.cars.iter().all(|c| c.pos != p)).then_some(p) }); let pick = if edge.is_empty() { inside } else { Some(edge[(h % edge.len() as u64) as usize]) }; if let Some(pos) = pick { let order = [Dir::N, Dir::E, Dir::S, Dir::W]; let start = ((h >> 8) % 4) as usize; let dir = (0..4) .map(|i| order[(start + i) % 4]) .find(|d| self.drivable(city, pos.step(*d))); if let Some(dir) = dir { let hues = [Hue::White, Hue::Yellow, Hue::Red, Hue::Cyan]; self.cars.push(Car { pos, dir, hue: hues[((h >> 16) % 4) as usize], age: 0, }); } } } } } fn turn_left(d: Dir) -> Dir { match d { Dir::N => Dir::W, Dir::W => Dir::S, Dir::S => Dir::E, Dir::E => Dir::N, other => other, } } fn turn_right(d: Dir) -> Dir { match d { Dir::N => Dir::E, Dir::E => Dir::S, Dir::S => Dir::W, Dir::W => Dir::N, other => other, } } impl Overlay for Traffic { fn draw( &self, _city: &City, stratum: StratumKind, origin: Pos, area: Rect, _tick: u64, buf: &mut Buffer, style: &StylePack, ) { if stratum != StratumKind::Street { return; } for car in &self.cars { let color = style.hue(car.hue); if let Some((sx, sy)) = tile_to_screen(area, origin, car.pos.step(car.dir)) { let c = &mut buf[(sx, sy)]; let bg = match c.bg { ratatui::style::Color::Rgb(r, g, b) => crate::render::Rgb(r, g, b), _ => crate::render::Rgb(0, 0, 0), }; c.set_bg(bg.mix(color, 0.3).to_color()); } if let Some((sx, sy)) = tile_to_screen(area, origin, car.pos) { buf[(sx, sy)].set_char('▬').set_fg(color.to_color()); } } } } #[cfg(test)] mod tests { use super::*; use citygen::{GenParams, Generator, Rect, SizePreset, StratumKind, Terrain}; #[test] fn traffic_stays_on_roads() { let c = Generator::run(GenParams { seed: 3, size: SizePreset::Custom { w: 120, h: 60 }, districts: 5, ..Default::default() }) .unwrap(); let mut tr = Traffic::default(); for tick in 0..400 { tr.update(&c, StratumKind::Street, Rect::new(0, 0, 120, 60), tick); } assert!(!tr.cars.is_empty()); assert!(tr.cars.len() <= MAX_CARS); for car in &tr.cars { assert_eq!( c.tile(StratumKind::Street, car.pos).unwrap().terrain, Terrain::Road ); } tr.update(&c, StratumKind::Skyways, Rect::new(0, 0, 120, 60), 401); } }