use palette::Hsl;
use ratatui::{
prelude::*,
symbols::border,
widgets::{Block, Borders, Paragraph},
};
use crate::{
pack::PackItem,
party::{ALL_PARTIES, palette::ALL_PALETTES},
state::State,
tui::{
action::{Action, Route},
views::{View, ViewResult},
widgets::palette_preview,
},
};
enum PackItemState {
Opened,
Unopened,
}
const ITEM_HEIGHT: u16 = 5;
const ITEM_WIDTH: u16 = 10;
const ROW_SPACE_VERT: u16 = 1;
const ROW_SPACE_HORI: u16 = 6;
pub fn item_preview(item: &PackItem) -> Vec<Line<'static>> {
match item {
PackItem::PaletteUnlock { palette_id, .. } => {
let palette = ALL_PALETTES
.iter()
.find(|p| p.id() == *palette_id)
.expect("palette should exist");
["".into(), palette_preview(palette, false), "".into()].into()
}
PackItem::PointBundle { rarity, .. } => match rarity {
crate::pack::Rarity::Common => ["".into(), "●●".yellow().into(), "".into()].into(),
crate::pack::Rarity::Rare => [
"".yellow().into(),
"●●●●".yellow().into(),
"".yellow().into(),
]
.into(),
crate::pack::Rarity::Epic => [
"●●".yellow().into(),
"●●●●".yellow().into(),
"●●".yellow().into(),
]
.into(),
crate::pack::Rarity::Legendary => [
"●●●●".yellow().into(),
"●●●●●●".yellow().into(),
"●●●●".yellow().into(),
]
.into(),
},
PackItem::GameToken { .. } => [
"┌────┐".reset().bold().into(),
"│▔▔▔▔│".reset().bold().into(),
"└────┘".reset().bold().into(),
]
.into(),
}
}
#[derive(Default)]
pub struct PackRevealView {
items: Vec<(PackItem, PackItemState)>,
selected: Option<usize>,
}
impl PackRevealView {
pub fn set_items(&mut self, items: Vec<PackItem>) {
self.items = items
.into_iter()
.map(|i| (i, PackItemState::Unopened))
.collect();
}
fn reset(&mut self) {
self.items = Vec::new();
self.selected = None;
}
fn first_row_len(&self) -> usize {
self.items.len().div_ceil(2)
}
}
impl View for PackRevealView {
fn render(&self, frame: &mut Frame, area: Rect, _state: &State, tick: u32) {
let split_idx = self.first_row_len();
let first_row_items = &self
.items
.iter()
.enumerate()
.take(split_idx)
.collect::<Vec<_>>();
let second_row_items = &self
.items
.iter()
.enumerate()
.skip(split_idx)
.collect::<Vec<_>>();
let [_, first_row, _, middle, _, second_row, _] = area.layout(&Layout::vertical([
Constraint::Fill(2),
Constraint::Length(ITEM_HEIGHT),
Constraint::Length(ROW_SPACE_VERT),
Constraint::Length(1),
Constraint::Length(ROW_SPACE_VERT),
Constraint::Length(ITEM_HEIGHT),
Constraint::Fill(2),
]));
let selected_item = self.selected.and_then(|selected| {
first_row_items
.iter()
.chain(second_row_items)
.find(|(idx, _)| *idx == selected)
});
if let Some((_, (item, state))) = selected_item {
let middle_text = match (item, state) {
(
PackItem::PaletteUnlock {
party_id,
palette_id,
..
},
PackItemState::Opened,
) => {
let party = ALL_PARTIES
.iter()
.find(|p| p.info.id == *party_id)
.expect("party should exist");
let palette = ALL_PALETTES
.iter()
.find(|p| p.id() == *palette_id)
.expect("palette should exist");
Line::from(vec![
"You unlocked the ".dim(),
palette.name().bold(),
" palette for the ".dim(),
party.info.name.bold(),
"!".dim(),
])
}
(PackItem::PointBundle { points, .. }, PackItemState::Opened) => Line::from(vec![
"You got ".into(),
points.yellow(),
" P".yellow(),
"!".into(),
]),
(PackItem::GameToken { game, .. }, PackItemState::Opened) => Line::from(vec![
"You got a token to play ".into(),
game.name().cyan(),
"!".into(),
]),
(_, PackItemState::Unopened) => "Press enter to reveal!".dim().into(),
};
frame.render_widget(Text::from(middle_text.centered()), middle);
} else {
frame.render_widget(
Text::from("(Use the arrow keys to navigate.)".dark_gray()).centered(),
middle,
);
}
for (row, items) in [(first_row, first_row_items), (second_row, second_row_items)] {
let mut constraints = vec![Constraint::Fill(2)];
for _ in 0..items.len().saturating_sub(1) {
constraints.extend([
Constraint::Length(ITEM_WIDTH),
Constraint::Length(ROW_SPACE_HORI),
]);
}
constraints.extend([Constraint::Length(ITEM_WIDTH), Constraint::Fill(2)]);
let layout = Layout::horizontal(constraints).split(row);
for (rect, (idx, (item, state))) in layout.iter().skip(1).step_by(2).zip(items) {
let selected = self.selected.is_some_and(|n| n == *idx);
let opened = matches!(state, PackItemState::Opened);
let block = if selected {
let (hue, sat, mut lum) = item.rarity().color();
if !opened {
lum += 0.1 * f32::cos(tick as f32 / 10.);
}
Block::default()
.borders(Borders::ALL)
.border_set(border::PROPORTIONAL_TALL)
.style(Style::default().fg(Color::from_hsl(Hsl::new(hue, sat, lum))))
} else {
Block::default()
.borders(Borders::ALL)
.border_set(border::PROPORTIONAL_TALL)
.style(Style::default().dark_gray())
};
let [_, middle, _] = rect.layout(&Layout::vertical([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
]));
let text = if opened {
item_preview(item)
} else {
["".into(), "??".into(), "".into()].into()
};
let text = Paragraph::new(text).centered();
frame.render_widget(block, *rect);
frame.render_widget(text, middle);
}
}
}
fn handle(&mut self, action: Action, _state: &mut State) -> ViewResult {
match action {
Action::Left => {
self.selected = if let Some(selected) = self.selected {
Some(selected.saturating_sub(1))
} else {
Some(self.items.len() - 1)
};
ViewResult::Redraw
}
Action::Right => {
self.selected = if let Some(selected) = self.selected {
Some((selected + 1).min(self.items.len() - 1))
} else {
Some(0)
};
ViewResult::Redraw
}
Action::Up => {
if let Some(selected) = self.selected {
if selected >= self.first_row_len() {
self.selected = Some(selected.saturating_sub(self.first_row_len()))
}
} else {
self.selected = Some(self.items.len())
};
ViewResult::Redraw
}
Action::Down => {
if let Some(selected) = self.selected {
if selected < self.first_row_len() {
self.selected =
Some((selected + self.first_row_len()).min(self.items.len() - 1))
}
} else {
self.selected = Some(0)
};
ViewResult::Redraw
}
Action::Select => {
let selected_item = self.selected.and_then(|idx| self.items.get_mut(idx));
if let Some((item, state)) = selected_item
&& matches!(state, PackItemState::Unopened)
{
*state = PackItemState::Opened;
if let PackItem::PointBundle { points, .. } = item {
ViewResult::RevealPoints(*points)
} else if let PackItem::GameToken { .. } = item {
ViewResult::RevealGame
} else {
ViewResult::Redraw
}
} else {
ViewResult::None
}
}
Action::Back => {
self.reset();
ViewResult::Navigate(Route::Packs)
}
_ => ViewResult::None,
}
}
fn key_hints(&self) -> Vec<(&'static str, &'static str)> {
vec![
("↑↓←→", "select"),
("enter", "reveal"),
("esc", "back"),
("q", "quit"),
]
}
}