refactoring

This commit is contained in:
2026-06-11 21:31:37 -05:00
parent a1cce10432
commit 828771fb7d
6 changed files with 429 additions and 321 deletions
+27 -59
View File
@@ -9,7 +9,10 @@
mod cp437;
mod render;
mod scroll_overlay;
mod term;
mod utils;
mod speech;
use kiln_core::game::{GameState, ScrollLine};
use kiln_core::log::LogLine;
@@ -25,22 +28,14 @@ use ratatui::style::{Color, Style};
use ratatui::symbols::merge::MergeStrategy;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Wrap};
use render::{BoardWidget, logline_to_line};
use render::{BoardWidget};
use std::io;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use term::TerminalCaps;
/// Animation state for the scroll overlay window.
enum ScrollAnimState {
/// Window is opening: progress goes from 0.0 → 1.0 over 0.5 s.
Opening(f32),
/// Window is fully open.
Open,
/// Window is closing: progress from 1.0 → 0.0 over 0.5 s.
/// When it reaches 0 the `choice` (if any) is dispatched and the overlay clears.
Closing { progress: f32, choice: Option<String> },
}
use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayState, ScrollOverlayWidget};
use crate::speech::SpeechBubblesWidget;
use crate::utils::logline_to_line;
/// View-only UI state for the log panel and scroll overlay. This is presentation
/// state, so it lives in the front-end rather than on the engine's [`GameState`].
@@ -51,12 +46,8 @@ struct Ui {
log_height: u16,
/// Vertical scroll offset of the log panel, in lines from the top (newest).
scroll: u16,
/// Vertical scroll offset within an active scroll overlay.
scroll_offset: u16,
/// Maximum valid value for `scroll_offset`; updated each frame by the renderer.
scroll_max: u16,
/// Animation state for the scroll overlay; `None` when no overlay is active.
scroll_anim: Option<ScrollAnimState>,
/// Scroll position, bounds, and animation state for an active scroll overlay.
overlay: ScrollOverlayState,
}
impl Default for Ui {
@@ -65,9 +56,7 @@ impl Default for Ui {
log_open: false,
log_height: 10,
scroll: 0,
scroll_offset: 0,
scroll_max: 0,
scroll_anim: None,
overlay: ScrollOverlayState::default(),
}
}
}
@@ -75,19 +64,20 @@ impl Default for Ui {
impl Ui {
/// Scrolls the overlay by `delta` lines, clamped to the valid range.
fn scroll_lines(&mut self, delta: i32) {
self.scroll_offset = (self.scroll_offset as i32 + delta)
.clamp(0, self.scroll_max as i32) as u16;
self.overlay.offset = (self.overlay.offset as i32 + delta)
.clamp(0, self.overlay.max_scroll as i32) as u16;
}
}
/// Starts the scroll-close animation with the optional player choice.
/// Uses the current opening progress if the overlay was still animating in.
fn begin_close(ui: &mut Ui, choice: Option<String>) {
let progress = match &ui.scroll_anim {
let progress = match &ui.overlay.anim {
Some(ScrollAnimState::Opening(p)) => p.min(1.0),
_ => 1.0,
};
ui.scroll_anim = Some(ScrollAnimState::Closing { progress, choice });
ui.overlay.pending_choice = choice;
ui.overlay.anim = Some(ScrollAnimState::Closing(progress));
}
/// Returns the choice string for the given key character, or `None` if it doesn't
@@ -199,7 +189,7 @@ fn run(
// When a scroll overlay is active (and not already animating
// closed), intercept all keys for scroll navigation/choices.
let scroll_active = game.active_scroll.is_some()
&& !matches!(ui.scroll_anim, Some(ScrollAnimState::Closing { .. }));
&& !matches!(ui.overlay.anim, Some(ScrollAnimState::Closing(_) | ScrollAnimState::Done));
if scroll_active {
let scroll = game.active_scroll.as_ref().unwrap();
let has_choices = scroll.lines.iter()
@@ -241,7 +231,7 @@ fn run(
// wheel scrolls the log and drag resizes it.
Event::Mouse(m) => {
let scroll_active = game.active_scroll.is_some()
&& !matches!(ui.scroll_anim, Some(ScrollAnimState::Closing { .. }));
&& !matches!(ui.overlay.anim, Some(ScrollAnimState::Closing(_) | ScrollAnimState::Done));
if scroll_active {
match m.kind {
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
@@ -276,31 +266,19 @@ fn run(
// Advance scroll animation regardless of game pause state.
let dt_s = dt.as_secs_f32();
match &mut ui.scroll_anim {
Some(ScrollAnimState::Opening(p)) => {
*p += dt_s / 0.5;
if *p >= 1.0 { ui.scroll_anim = Some(ScrollAnimState::Open); }
}
Some(ScrollAnimState::Closing { progress, choice }) => {
*progress -= dt_s / 0.5;
if *progress <= 0.0 {
// Animation done — dispatch choice and clear the scroll.
let ch = choice.take();
game.close_scroll(ch.as_deref());
ui.scroll_anim = None;
ui.scroll_offset = 0;
ui.scroll_max = 0;
}
}
_ => {}
ui.overlay.advance_anim(dt_s);
if ui.overlay.close_finished() {
// Animation done — dispatch choice and clear the scroll.
game.close_scroll(ui.overlay.take_choice().as_deref());
ui.overlay = ScrollOverlayState::default();
}
// Tick the game only when no scroll overlay is blocking.
if game.active_scroll.is_none() {
game.tick(dt);
// Detect a scroll just opened by the tick; start its open animation.
if game.active_scroll.is_some() && ui.scroll_anim.is_none() {
ui.scroll_anim = Some(ScrollAnimState::Opening(0.0));
if game.active_scroll.is_some() && ui.overlay.anim.is_none() {
ui.overlay.anim = Some(ScrollAnimState::Opening(0.0));
}
}
}
@@ -319,9 +297,6 @@ fn scroll_log(ui: &mut Ui, game: &GameState, delta: i32) {
/// previewed in the board's bottom-left border after a `[l]og:` label.
/// A scroll overlay is drawn on top of everything when active.
///
/// TODO: `draw` mutates `ui.scroll_max` as a side-effect of rendering. This is a wart —
/// ideally render functions are pure and all state lives in the game loop. The max scroll
/// should be computed independently of drawing and stored before the draw call.
fn draw(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
// Borrow the board for the duration of the frame (no script runs during draw).
let board = game.board();
@@ -340,7 +315,7 @@ fn draw(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
let inner = block.inner(board_area);
frame.render_widget(block, board_area);
frame.render_widget(BoardWidget::new(board), inner);
render::draw_speech_bubbles(frame.buffer_mut(), inner, board, &game.speech_bubbles);
frame.render_widget(SpeechBubblesWidget::new(board, &game.speech_bubbles), inner);
// Newest message on top: reverse the log into ratatui lines.
let log_block = Block::bordered()
@@ -362,21 +337,14 @@ fn draw(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
let inner = block.inner(frame.area());
frame.render_widget(block, frame.area());
frame.render_widget(BoardWidget::new(board), inner);
render::draw_speech_bubbles(frame.buffer_mut(), inner, board, &game.speech_bubbles);
frame.render_widget(SpeechBubblesWidget::new(board, &game.speech_bubbles), inner);
}
// Scroll overlay: drawn last so it sits above all other content.
if let Some(scroll) = &game.active_scroll {
let anim = match &ui.scroll_anim {
Some(ScrollAnimState::Opening(p)) => p.clamp(0.0, 1.0),
Some(ScrollAnimState::Open) => 1.0,
Some(ScrollAnimState::Closing { progress, .. }) => progress.clamp(0.0, 1.0),
// Fallback: overlay exists but animation not set yet — show fully open.
None => 1.0,
};
// Capture area before the mutable buffer borrow to satisfy the borrow checker.
let area = frame.area();
ui.scroll_max = render::draw_scroll_overlay(frame.buffer_mut(), area, scroll, ui.scroll_offset, anim);
frame.render_stateful_widget(ScrollOverlayWidget::new(scroll), area, &mut ui.overlay);
}
}