use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph};
use crate::combat::crowd::*;
use crate::ui::theme;
#[derive(Debug)]
pub struct CrowdUi {
pub action_cursor: usize,
pub target_cursor: usize,
pub log: Vec<CrowdLogEntry>,
}
#[derive(Debug, Clone)]
pub struct CrowdLogEntry {
pub text: String,
pub style: Style,
}
impl CrowdUi {
pub fn new() -> Self {
Self { action_cursor: 0, target_cursor: 0, log: Vec::new() }
}
pub fn push_result(&mut self, result: &CrowdActionResult) {
self.log.push(CrowdLogEntry {
text: format!(" {}", result.description),
style: Style::default().fg(Color::White),
});
if let Some(ref name) = result.ringleader_broken {
self.log.push(CrowdLogEntry {
text: format!(" {} BREAKS. The crowd feels it.", name),
style: Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD),
});
}
if result.surge_delayed {
self.log.push(CrowdLogEntry {
text: " The surge is delayed. The line holds.".to_string(),
style: Style::default().fg(Color::Yellow),
});
}
while self.log.len() > 6 { self.log.remove(0); }
}
}
pub fn render_crowd(
frame: &mut Frame,
area: Rect,
crowd: &CrowdState,
ui: &CrowdUi,
actions: &[CrowdMenuItem],
) {
let ringleader_count = crowd.ringleaders.len() as u16;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Length(4), Constraint::Length(1), Constraint::Length(ringleader_count + 2), Constraint::Length(1), Constraint::Min(2), Constraint::Length(1), Constraint::Min(5), ])
.split(area);
render_crowd_title(frame, chunks[0], crowd);
render_crowd_dashboard(frame, chunks[1], crowd);
render_separator(frame, chunks[2]);
render_ringleaders(frame, chunks[3], crowd, ui.target_cursor);
render_separator(frame, chunks[4]);
render_crowd_log(frame, chunks[5], ui);
render_separator(frame, chunks[6]);
render_crowd_actions(frame, chunks[7], actions, ui);
}
fn render_crowd_title(frame: &mut Frame, area: Rect, crowd: &CrowdState) {
let phase_str = match crowd.phase {
CrowdPhase::Tense => "TENSE",
CrowdPhase::Surging => "SURGING",
CrowdPhase::Calming => "CALMING",
CrowdPhase::Broken => "BROKEN",
CrowdPhase::Dispersed => "DISPERSED",
};
let phase_color = match crowd.phase {
CrowdPhase::Tense => Color::Yellow,
CrowdPhase::Surging => Color::Red,
CrowdPhase::Calming => Color::Green,
CrowdPhase::Broken => Color::Magenta,
CrowdPhase::Dispersed => Color::Rgb(100, 180, 100),
};
let line = Line::from(vec![
Span::styled(
format!(" CROWD PRESSURE \u{2014} Turn {}", crowd.turn),
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
format!("Phase: {}", phase_str),
Style::default().fg(phase_color).add_modifier(Modifier::BOLD),
),
]);
frame.render_widget(Paragraph::new(vec![line, Line::from("")]), area);
}
fn render_crowd_dashboard(frame: &mut Frame, area: Rect, crowd: &CrowdState) {
let bar = crate::ui::screens::combat::bar_chars(crowd.collective_nerve, crowd.max_nerve, 24);
let nerve_color = theme::gauge_color(crowd.collective_nerve, crowd.max_nerve);
let momentum_display = if crowd.momentum > 0 {
format!("\u{25ba}\u{25ba} +{} (calming)", crowd.momentum)
} else if crowd.momentum < 0 {
format!("\u{25c4}\u{25c4} {} (escalating)", crowd.momentum)
} else {
"-- 0 (stalled)".to_string()
};
let momentum_color = if crowd.momentum > 0 {
Color::Green
} else if crowd.momentum < 0 {
Color::Red
} else {
Color::Yellow
};
let surge_color = if crowd.surge_countdown <= 2 {
Color::Red
} else {
Color::Yellow
};
let surge_bar = crate::ui::screens::combat::bar_chars(
crowd.surge_countdown as i32,
8, 8,
);
let lines = vec![
Line::from(vec![
Span::styled(" Collective Nerve ", Style::default().fg(Color::White)),
Span::styled(bar, Style::default().fg(nerve_color)),
Span::styled(
format!(" {}/{}", crowd.collective_nerve, crowd.max_nerve),
Style::default().fg(nerve_color),
),
]),
Line::from(vec![
Span::styled(" Momentum ", Style::default().fg(Color::White)),
Span::styled(momentum_display, Style::default().fg(momentum_color)),
]),
Line::from(vec![
Span::styled(" Surge Countdown ", Style::default().fg(Color::White)),
Span::styled(surge_bar, Style::default().fg(surge_color)),
Span::styled(
format!(" {} turns", crowd.surge_countdown),
Style::default().fg(surge_color),
),
]),
];
frame.render_widget(Paragraph::new(lines), area);
}
fn render_ringleaders(frame: &mut Frame, area: Rect, crowd: &CrowdState, target_cursor: usize) {
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::DarkGray))
.title(Span::styled(" Ringleaders ", theme::dim_style()));
let inner = block.inner(area);
frame.render_widget(block, area);
let lines: Vec<Line> = crowd.ringleaders.iter().enumerate().map(|(i, rl)| {
let is_target = i == target_cursor;
let prefix = if is_target { " \u{25b6} " } else { " " };
if rl.broken {
Line::from(vec![
Span::styled(prefix, theme::dim_style()),
Span::styled(
format!("{:<14} BROKEN", rl.name),
Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC),
),
])
} else {
let nerve_bar = crate::ui::screens::combat::bar_chars(rl.nerve, 20, 6);
let nerve_color = theme::gauge_color(rl.nerve, 20);
Line::from(vec![
Span::styled(prefix, if is_target { Style::default().fg(Color::Yellow) } else { Style::default() }),
Span::styled(format!("{:<14} ", rl.name), Style::default().fg(Color::White)),
Span::styled("NRV ", Style::default().fg(nerve_color)),
Span::styled(nerve_bar, Style::default().fg(nerve_color)),
Span::styled(
format!(" influence: {}", rl.influence),
theme::dim_style(),
),
])
}
}).collect();
frame.render_widget(Paragraph::new(lines), inner);
}
fn render_crowd_log(frame: &mut Frame, area: Rect, ui: &CrowdUi) {
let max = area.height as usize;
let start = ui.log.len().saturating_sub(max);
let lines: Vec<Line> = ui.log[start..].iter().map(|e| {
Line::from(Span::styled(e.text.clone(), e.style))
}).collect();
frame.render_widget(Paragraph::new(lines), area);
}
#[derive(Debug, Clone)]
pub struct CrowdMenuItem {
pub actor: String,
pub label: String,
pub action_type: CrowdActionType,
pub effect_hint: String,
pub needs_target: bool,
}
fn render_crowd_actions(
frame: &mut Frame,
area: Rect,
actions: &[CrowdMenuItem],
ui: &CrowdUi,
) {
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::Rgb(180, 80, 60))) .title(Span::styled(" The room is waiting. ", Style::default().fg(Color::Rgb(180, 80, 60))));
let items: Vec<ListItem> = actions.iter().enumerate().map(|(i, action)| {
let selected = i == ui.action_cursor;
let prefix = if selected { " > " } else { " " };
let style = if selected {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
ListItem::new(Line::from(vec![
Span::styled(
format!("{}{:<10}\u{2014} {:<24}",
prefix,
action.actor.to_uppercase(),
action.label,
),
style,
),
Span::styled(action.effect_hint.clone(), theme::dim_style()),
]))
}).collect();
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
fn render_separator(frame: &mut Frame, area: Rect) {
let sep = Paragraph::new(Line::from(Span::styled(
"\u{2500}".repeat(area.width as usize),
Style::default().fg(Color::Rgb(60, 60, 60)),
)));
frame.render_widget(sep, area);
}