use std::f64::consts::PI;
use rand::RngExt;
use tixel::BrailleCanvas;
use crate::party::{FullscreenPartyRenderer, PartyEntry, PartyInfo, PartyRenderer};
use super::Palette;
pub static FIREWORKS_PARTY: PartyEntry = PartyEntry {
info: PartyInfo {
id: "fireworks",
name: "Fireworks Party",
description: "A full-screen fireworks display.",
cost: 10_000,
supports_color: true,
},
renderer: PartyRenderer::Fullscreen {
create: FireworksRenderer::create,
},
};
struct FireworksRenderer {
canvas: BrailleCanvas,
sim: FireworksSim,
palette: &'static Palette,
}
impl FireworksRenderer {
pub fn create(
width: u16,
height: u16,
palette: &'static Palette,
) -> Box<dyn FullscreenPartyRenderer> {
let canvas = BrailleCanvas::new((width as usize, height as usize), (0, 0));
let sim = FireworksSim::new(canvas.width() as f64, canvas.height() as f64);
Box::new(Self {
sim,
canvas,
palette,
})
}
}
impl FullscreenPartyRenderer for FireworksRenderer {
fn z_index(&self) -> u32 {
1
}
fn update(&mut self, dt: std::time::Duration) -> bool {
self.sim.update(dt.as_secs_f64())
}
fn render(&mut self, buf: &mut String) {
for p in self.sim.particles() {
self.canvas.set_f(
p.x,
self.canvas.height() as f64 - p.y,
self.palette.get_color(p.color_idx),
);
}
self.canvas.render_to(buf);
}
}
const GRAVITY: f64 = -60.;
const NUM_ROCKETS: usize = 10;
const VEL_X_VARIANCE: f64 = 10.;
const VEL_Y_VARIANCE_RATIO: f64 = 0.2;
const NUM_EXPLOSION_PARTICLES: usize = 200;
const EXPLOSION_VEL: f64 = 50.;
const EXPLOSION_VEL_RANGE: std::ops::Range<f64> = 0.2 * EXPLOSION_VEL..EXPLOSION_VEL;
pub struct Particle {
pub color_idx: usize,
pub x: f64,
pub y: f64,
pub vel_x: f64,
pub vel_y: f64,
}
impl Particle {
pub fn new(id: usize, x: f64, y: f64, vel_x: f64, vel_y: f64) -> Self {
Self {
color_idx: id,
x,
y,
vel_x,
vel_y,
}
}
pub fn update(&mut self, dt_secs: f64) {
self.vel_y += GRAVITY * dt_secs;
self.x += self.vel_x * dt_secs;
self.y += self.vel_y * dt_secs;
}
}
pub struct FireworksSim {
#[expect(dead_code)]
dims: (f64, f64),
rockets: Vec<(Particle, bool)>,
particles: Vec<Particle>,
}
impl FireworksSim {
pub fn new(width: f64, height: f64) -> Self {
let max_height = (2. / 3.) * height;
let vel_y_base = (-2. * max_height * GRAVITY).sqrt();
let mut rng = rand::rng();
let rockets = (0..NUM_ROCKETS)
.map(|n| {
let x = (2 * n + 1) as f64 * width / (2. * NUM_ROCKETS as f64);
let y = 0.;
let vel_x = rng.random_range(-VEL_X_VARIANCE..VEL_X_VARIANCE);
let vel_y = vel_y_base
* (1. + rng.random_range(-VEL_Y_VARIANCE_RATIO..VEL_Y_VARIANCE_RATIO));
(Particle::new(n, x, y, vel_x, vel_y), true)
})
.collect();
Self {
dims: (width, height),
rockets,
particles: vec![],
}
}
pub fn particles(&self) -> impl Iterator<Item = &Particle> {
self.rockets
.iter()
.filter_map(|(p, live)| live.then_some(p))
.chain(self.particles.iter())
}
pub fn update(&mut self, dt_secs: f64) -> bool {
let mut rng = rand::rng();
for (p, live) in &mut self.rockets {
if !*live {
continue;
}
p.update(dt_secs);
if p.vel_y < 0.0 {
*live = false;
for n in 0..NUM_EXPLOSION_PARTICLES {
let speed = rng.random_range(EXPLOSION_VEL_RANGE);
let angle = (n as f64 / NUM_EXPLOSION_PARTICLES as f64) * 2. * PI;
let vel_x = speed * f64::cos(angle);
let vel_y = speed * f64::sin(angle);
let new_p = Particle::new(p.color_idx, p.x, p.y, vel_x, vel_y);
self.particles.push(new_p);
}
}
}
let mut has_visible_particles = self.rockets.iter().any(|(_, live)| *live);
for p in &mut self.particles {
p.update(dt_secs);
if p.y > 0. {
has_visible_particles = true;
}
}
has_visible_particles
}
}