use std::cell::Cell;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Padding, Paragraph};
use tui_scrollview::{ScrollView, ScrollViewState, ScrollbarVisibility};
use crate::party::{ALL_PARTIES, PartyEntry};
use crate::state::State;
use crate::tui::action::{Action, Route, StoreRoute};
use crate::tui::views::{MessageType, View, ViewResult};
use crate::tui::widgets::ShimmerBlock;
const ITEM_HEIGHT: u16 = 4;
const SCROLL_PADDING: u16 = ITEM_HEIGHT;
struct PartyListItem {
party: &'static PartyEntry,
unlocked: bool,
affordable: bool,
selected: bool,
tick: u32,
}
impl PartyListItem {
fn new(
party: &'static PartyEntry,
unlocked: bool,
affordable: bool,
selected: bool,
tick: u32,
) -> Self {
Self {
party,
unlocked,
affordable,
selected,
tick,
}
}
}
impl Widget for PartyListItem {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner = if self.selected {
let block = ShimmerBlock::new(self.tick);
let inner = block.inner(area).inner(Margin::new(1, 0));
block.render(area, buf);
inner
} else {
let block = Block::default()
.borders(Borders::ALL)
.padding(Padding::horizontal(1))
.border_style(Style::default().gray());
let inner = block.inner(area);
block.render(area, buf);
inner
};
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
let top_chunks =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).split(chunks[0]);
let title_text = Text::from(self.party.info.name).reset().bold();
title_text.render(top_chunks[0], buf);
let price_style = if self.unlocked {
Style::default().fg(Color::DarkGray)
} else if self.affordable {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::Red)
};
let price_str = if self.unlocked {
"✓ Owned".to_string()
} else {
format!("{} P", self.party.info.cost)
};
let price_text = Text::from(price_str)
.style(price_style)
.alignment(Alignment::Right);
price_text.render(top_chunks[1], buf);
let desc_text = Text::from(self.party.info.description).reset();
desc_text.render(chunks[1], buf);
}
}
#[derive(Default)]
pub struct UpgradesView {
selection: usize,
scroll_state: ScrollViewState,
viewport_height: Cell<u16>,
}
impl UpgradesView {
fn selected_party(&self) -> Option<&'static PartyEntry> {
ALL_PARTIES.get(self.selection).copied()
}
const fn item_count(&self) -> usize {
ALL_PARTIES.len()
}
fn update_scroll(&mut self) {
let viewport_height = self.viewport_height.get();
let selected_top = self.selection as u16 * ITEM_HEIGHT;
let selected_bottom = selected_top + ITEM_HEIGHT;
let current_offset = self.scroll_state.offset().y;
let viewport_bottom = current_offset + viewport_height;
if selected_bottom + SCROLL_PADDING > viewport_bottom {
let new_offset = (selected_bottom + SCROLL_PADDING).saturating_sub(viewport_height);
self.scroll_state.set_offset(Position::new(0, new_offset));
}
else if selected_top < current_offset + SCROLL_PADDING {
let new_offset = selected_top.saturating_sub(SCROLL_PADDING);
self.scroll_state.set_offset(Position::new(0, new_offset));
}
}
}
impl View for UpgradesView {
fn render(&self, frame: &mut Frame, area: Rect, state: &State, tick: u32) {
self.viewport_height.set(area.height);
let chunks = Layout::vertical([Constraint::Length(2), Constraint::Fill(1)]).split(area);
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().dark_gray());
let header = Paragraph::new("Party Upgrades")
.alignment(Alignment::Center)
.style(Style::default().fg(Color::Reset))
.block(block);
frame.render_widget(header, chunks[0]);
let content_area = chunks[1].inner(Margin::new(1, 0));
let content_width = content_area.width.saturating_sub(1); let content_height = self.item_count() as u16 * ITEM_HEIGHT;
let mut scroll_view = ScrollView::new(Size::new(content_width, content_height))
.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
for (i, &party) in ALL_PARTIES.iter().enumerate() {
let affordable = state.party_points >= party.info.cost;
let selected = self.selection == i;
let unlocked = state.is_party_unlocked(party.info.id);
let item = PartyListItem::new(party, unlocked, affordable, selected, tick);
let item_rect = Rect::new(0, i as u16 * ITEM_HEIGHT, content_width, ITEM_HEIGHT);
scroll_view.render_widget(item, item_rect);
}
frame.render_stateful_widget(scroll_view, content_area, &mut self.scroll_state.clone());
}
fn handle(&mut self, action: Action, state: &mut State) -> ViewResult {
match action {
Action::Up => {
let count = self.item_count();
self.selection = (self.selection + count - 1) % count;
self.update_scroll();
ViewResult::Redraw
}
Action::Down => {
self.selection = (self.selection + 1) % self.item_count();
self.update_scroll();
ViewResult::Redraw
}
Action::Select => {
if let Some(party) = self.selected_party() {
if state.is_party_unlocked(party.info.id) {
ViewResult::Message(
MessageType::Normal,
format!("You already own {}.", party.info.name),
)
} else {
let cost = party.info.cost;
if state.party_points >= cost {
state.party_points -= cost;
state.unlock_party(party.info.id);
ViewResult::Message(
MessageType::Success,
format!("Unlocked {}!", party.info.name),
)
} else {
ViewResult::Message(
MessageType::Error,
format!("You need {} more points.", cost - state.party_points),
)
}
}
} else {
ViewResult::None
}
}
Action::Back => ViewResult::Navigate(Route::Store(StoreRoute::Grid)),
_ => ViewResult::None,
}
}
fn key_hints(&self) -> Vec<(&'static str, &'static str)> {
vec![
("↑↓", "select"),
("enter", "buy"),
("esc", "back"),
("q", "quit"),
]
}
}