mod base;
mod big_text;
mod breakdown;
pub mod compositor;
mod context;
mod exclamation;
mod fireworks;
pub(crate) mod palette;
mod quotes;
pub(crate) mod stats;
mod style;
use anyhow::Result;
pub use context::RenderContext;
pub use palette::Palette;
pub use fireworks::FIREWORKS_PARTY;
use crate::{party::palette::ALL_PALETTES, state::PaletteSelection};
pub struct PartyInfo {
pub cost: u64,
pub description: &'static str,
pub id: &'static str,
pub name: &'static str,
pub supports_color: bool,
}
pub trait FullscreenPartyRenderer {
fn z_index(&self) -> u32;
fn update(&mut self, dt: std::time::Duration) -> bool;
fn render(&mut self, buf: &mut String);
}
pub enum PartyRenderer {
Inline {
render: fn(&RenderContext<'_>, &Palette) -> bool,
},
Fullscreen {
create: fn(u16, u16, &'static Palette) -> Box<dyn FullscreenPartyRenderer>,
},
}
pub struct PartyEntry {
pub info: PartyInfo,
pub renderer: PartyRenderer,
}
pub static ALL_PARTIES: &[&PartyEntry] = &[
&base::BASE_PARTY,
&breakdown::BREAKDOWN_PARTY,
&stats::STATS_PARTY,
&exclamation::EXCLAMATION_PARTY,
&big_text::BIG_TEXT_PARTY,
"es::QUOTES_PARTY,
&fireworks::FIREWORKS_PARTY,
];
fn random_pick<T>(items: &[T]) -> &T {
use rand::prelude::IndexedRandom;
items
.choose(&mut rand::rng())
.expect("list must be nonempty")
}
pub fn display(ctx: &RenderContext) {
let enabled_parties = ALL_PARTIES
.iter()
.filter(|party| ctx.state.is_party_enabled(party.info.id))
.copied();
let _ = render_fullscreen_parties(ctx, enabled_parties.clone());
render_inline_parties(ctx, enabled_parties);
}
fn render_inline_parties(
ctx: &RenderContext<'_>,
enabled_parties: impl Iterator<Item = &'static PartyEntry>,
) {
println!();
for party in enabled_parties {
let PartyRenderer::Inline { render } = party.renderer else {
continue;
};
let palette = get_palette(party, ctx);
let printed_text = render(ctx, palette);
if printed_text {
println!();
}
}
}
fn render_fullscreen_parties(
ctx: &RenderContext<'_>,
enabled_parties: impl Iterator<Item = &'static PartyEntry>,
) -> Result<()> {
let (cols, rows) = crossterm::terminal::size()?;
let parties: Vec<Box<dyn FullscreenPartyRenderer>> = enabled_parties
.filter_map(|party| {
let PartyRenderer::Fullscreen { create } = party.renderer else {
return None;
};
let palette = get_palette(party, ctx);
let renderer = create(cols, rows, palette);
Some(renderer)
})
.collect();
if !parties.is_empty() {
compositor::run(parties)?;
}
Ok(())
}
fn get_palette(party: &PartyEntry, ctx: &RenderContext<'_>) -> &'static Palette {
let palette_selection = ctx.state.selected_palette(party.info.id);
let palette_id = match palette_selection {
Some(PaletteSelection::Random) => {
let unlocked_palettes = ctx
.state
.unlocked_palettes(party.info.id)
.map(|set| set.iter().collect::<Vec<_>>())
.unwrap_or_default();
if unlocked_palettes.is_empty() {
Palette::WHITE_ANSI.id().to_string()
} else {
random_pick(&unlocked_palettes).to_string()
}
}
Some(PaletteSelection::Specific(color_name)) => color_name.to_string(),
None => Palette::WHITE_ANSI.id().to_string(),
};
ALL_PALETTES
.iter()
.find(|&&c| c.id() == palette_id)
.unwrap_or(&&Palette::WHITE_ANSI)
}