use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use crate::combat::reckoning::*;
use crate::ui::theme;
#[derive(Debug)]
pub struct ReckoningUi {
pub action_cursor: usize,
pub log: Vec<ReckoningLogEntry>,
}
#[derive(Debug, Clone)]
pub struct ReckoningLogEntry {
pub text: String,
pub style: Style,
}
impl ReckoningUi {
pub fn new() -> Self {
Self { action_cursor: 0, log: Vec::new() }
}
pub fn push_action(&mut self, action: &ReckoningAction) {
self.log.push(ReckoningLogEntry {
text: format!(" {}", action.description),
style: Style::default().fg(Color::White),
});
let e = &action.effects;
let changes = [
("Credibility", e.credibility),
("Crowd", e.crowd_nerve),
("Witness", e.witness_integrity),
("Evidence", e.evidence_continuity),
("Procedure", e.procedural_control),
];
let significant: Vec<String> = changes.iter()
.filter(|(_, v)| v.abs() >= 5)
.map(|(name, v)| {
if *v > 0 { format!("{} +{}", name, v) }
else { format!("{} {}", name, v) }
})
.collect();
if !significant.is_empty() {
self.log.push(ReckoningLogEntry {
text: format!(" {}", significant.join(" | ")),
style: Style::default().fg(Color::Rgb(180, 180, 140)),
});
}
if action.action_type == ReckoningActionType::EliDefiningAct {
self.log.push(ReckoningLogEntry {
text: " ** The room goes silent. **".to_string(),
style: Style::default().fg(Color::Rgb(200, 180, 140)).add_modifier(Modifier::BOLD),
});
}
while self.log.len() > 8 { self.log.remove(0); }
}
}
#[derive(Debug, Clone)]
pub struct ReckoningMenuItem {
pub actor: String,
pub label: String,
pub action_type: ReckoningActionType,
pub bar_effects: String,
pub is_eli_act: bool,
pub available: bool,
pub lock_reason: Option<String>,
}
pub fn render_reckoning(
frame: &mut Frame,
area: Rect,
state: &ReckoningState,
ui: &ReckoningUi,
actions: &[ReckoningMenuItem],
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Length(7), Constraint::Length(1), Constraint::Min(3), Constraint::Length(1), Constraint::Min(5), ])
.split(area);
render_reckoning_title(frame, chunks[0], state);
render_five_bars(frame, chunks[1], state);
render_separator(frame, chunks[2]);
render_reckoning_log(frame, chunks[3], ui);
render_separator(frame, chunks[4]);
render_reckoning_actions(frame, chunks[5], actions, ui, state);
}
fn render_reckoning_title(frame: &mut Frame, area: Rect, state: &ReckoningState) {
let phase_str = match state.phase {
ReckoningPhase::Opening => "OPENING",
ReckoningPhase::Presentation => "PRESENTATION",
ReckoningPhase::Counterstrike => "COUNTERSTRIKE",
ReckoningPhase::EliAct => "ELI'S ACT",
ReckoningPhase::Verdict => "VERDICT",
};
let phase_color = match state.phase {
ReckoningPhase::Opening => Color::White,
ReckoningPhase::Presentation => Color::Rgb(200, 180, 140),
ReckoningPhase::Counterstrike => Color::Red,
ReckoningPhase::EliAct => Color::Rgb(200, 180, 140),
ReckoningPhase::Verdict => Color::Green,
};
let line = Line::from(vec![
Span::styled(
" PUBLIC RECKONING \u{2014} Deadwater Trial",
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_five_bars(frame: &mut Frame, area: Rect, state: &ReckoningState) {
let bars = [
("Room Credibility ", state.room_credibility),
("Crowd Nerve ", state.crowd_nerve),
("Witness Integrity ", state.witness_integrity),
("Evidence Continuity", state.evidence_continuity),
("Procedural Control ", state.procedural_control),
];
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::DarkGray))
.title(Span::styled(" The Room ", Style::default().fg(Color::Rgb(200, 180, 140))));
let inner = block.inner(area);
frame.render_widget(block, area);
let lines: Vec<Line> = bars.iter().map(|(label, value)| {
let color = reckoning_bar_color(*value);
let bar = crate::ui::screens::combat::bar_chars(*value, 100, 20);
let danger = *value <= 15;
let mark = if danger { " \u{26a0}" } else { "" };
Line::from(vec![
Span::styled(format!(" {}", label), Style::default().fg(Color::White)),
Span::styled(bar, Style::default().fg(color)),
Span::styled(
format!(" {}/100{}", value, mark),
Style::default().fg(color),
),
])
}).collect();
frame.render_widget(Paragraph::new(lines), inner);
}
fn reckoning_bar_color(value: i32) -> Color {
if value >= 60 {
Color::Rgb(140, 180, 140) } else if value >= 30 {
Color::Rgb(200, 180, 100) } else if value > 10 {
Color::Rgb(200, 100, 80) } else {
Color::Rgb(180, 60, 60) }
}
fn render_reckoning_log(frame: &mut Frame, area: Rect, ui: &ReckoningUi) {
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);
}
fn render_reckoning_actions(
frame: &mut Frame,
area: Rect,
actions: &[ReckoningMenuItem],
ui: &ReckoningUi,
state: &ReckoningState,
) {
let title_text = match state.phase {
ReckoningPhase::Opening => " The room listens. ",
ReckoningPhase::Presentation => " Present your case. ",
ReckoningPhase::Counterstrike => " They're pushing back. ",
ReckoningPhase::EliAct => " One choice remains. ",
ReckoningPhase::Verdict => " The room decides. ",
};
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::Rgb(200, 180, 140)))
.title(Span::styled(title_text, Style::default().fg(Color::Rgb(200, 180, 140))));
let items: Vec<ListItem> = actions.iter().enumerate().map(|(i, action)| {
let selected = i == ui.action_cursor;
let prefix = if selected { " > " } else { " " };
if !action.available {
let reason = action.lock_reason.as_deref().unwrap_or("[Not yet]");
return ListItem::new(Line::from(vec![
Span::styled(
format!("{}{:<10}\u{2014} {}", prefix, action.actor.to_uppercase(), action.label),
theme::locked_style(),
),
Span::styled(format!(" {}", reason), theme::lock_reason_style()),
]));
}
if action.is_eli_act {
let style = if selected {
Style::default().fg(Color::Rgb(220, 200, 160)).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(200, 180, 140))
};
return ListItem::new(Line::from(vec![
Span::styled(
format!("{}{:<10}\u{2014} {}", prefix, action.actor.to_uppercase(), action.label),
style,
),
Span::styled(
" ** DEFINING ACT **",
Style::default().fg(Color::Rgb(220, 200, 160)).add_modifier(Modifier::BOLD),
),
]));
}
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} {:<28}", prefix, action.actor.to_uppercase(), action.label),
style,
),
Span::styled(action.bar_effects.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);
}