2026-06-13 18:43:29 -05:00
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::rc::Rc;
|
2026-06-11 21:45:50 -05:00
|
|
|
use std::time::Duration;
|
2026-06-13 18:43:29 -05:00
|
|
|
use kiln_core::Board;
|
2026-06-11 21:45:50 -05:00
|
|
|
use kiln_core::game::GameState;
|
2026-06-11 21:55:53 -05:00
|
|
|
use crate::log::LogState;
|
2026-06-11 21:45:50 -05:00
|
|
|
use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayState};
|
2026-06-13 18:43:29 -05:00
|
|
|
use crate::transition::TransitionAnimation;
|
2026-06-11 21:45:50 -05:00
|
|
|
|
2026-06-13 18:43:29 -05:00
|
|
|
/// A pair of board `Rc`s (old board, new board) queued for transition pre-rendering.
|
|
|
|
|
type PendingTransition = Option<(Rc<RefCell<Board>>, Rc<RefCell<Board>>)>;
|
|
|
|
|
|
|
|
|
|
/// View-only UI state for the log panel, scroll overlay, and board transitions.
|
|
|
|
|
/// Presentation-only state; lives in the front-end, not on [`GameState`].
|
2026-06-11 21:45:50 -05:00
|
|
|
pub(crate) struct Ui {
|
2026-06-11 21:55:53 -05:00
|
|
|
/// State for the log panel (open/closed, height, scroll position).
|
|
|
|
|
pub(crate) log: LogState,
|
2026-06-11 21:45:50 -05:00
|
|
|
/// Scroll position, bounds, and animation state for an active scroll overlay.
|
|
|
|
|
pub(crate) overlay: ScrollOverlayState,
|
2026-06-13 18:43:29 -05:00
|
|
|
/// Old and new board Rcs waiting to be pre-rendered on the next `draw()` call.
|
|
|
|
|
/// Populated when a portal transition is detected; consumed in `draw()`.
|
|
|
|
|
pub(crate) pending_transition: PendingTransition,
|
|
|
|
|
/// Active board-wipe animation. While `Some`, ticks and input are suppressed.
|
|
|
|
|
pub(crate) transition: Option<TransitionAnimation>,
|
|
|
|
|
/// Set when the wipe completes; puts the game in [`InputMode::Frozen`] until the
|
|
|
|
|
/// player presses any key.
|
|
|
|
|
pub(crate) post_transition_wait: bool,
|
2026-06-11 21:45:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Ui {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
2026-06-11 21:55:53 -05:00
|
|
|
log: LogState::new(),
|
2026-06-11 21:45:50 -05:00
|
|
|
overlay: ScrollOverlayState::default(),
|
2026-06-13 18:43:29 -05:00
|
|
|
pending_transition: None,
|
|
|
|
|
transition: None,
|
|
|
|
|
post_transition_wait: false,
|
2026-06-11 21:45:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Ui {
|
|
|
|
|
/// Scrolls the overlay by `delta` lines, clamped to the valid range.
|
|
|
|
|
pub(crate) fn scroll_lines(&mut self, delta: i32) {
|
|
|
|
|
self.overlay.offset = (self.overlay.offset as i32 + delta)
|
|
|
|
|
.clamp(0, self.overlay.max_scroll as i32) as u16;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Advances all time-driven UI state by `dt` and drives the game tick.
|
|
|
|
|
///
|
2026-06-13 18:43:29 -05:00
|
|
|
/// Board-wipe transition and post-transition freeze take priority: while either
|
|
|
|
|
/// is active the game does not tick and no scroll overlay is processed. Once the
|
|
|
|
|
/// wipe completes, `post_transition_wait` is set so input stays frozen until the
|
|
|
|
|
/// player presses any key. Normal scroll-overlay handling resumes after that.
|
2026-06-11 21:45:50 -05:00
|
|
|
pub(crate) fn tick(&mut self, dt: Duration, game: &mut GameState) {
|
2026-06-13 18:43:29 -05:00
|
|
|
// Advance the wipe animation; transition to frozen state when done.
|
|
|
|
|
if let Some(ref mut tr) = self.transition {
|
|
|
|
|
if tr.update(dt.as_secs_f32()) {
|
|
|
|
|
self.transition = None;
|
|
|
|
|
self.post_transition_wait = true;
|
|
|
|
|
}
|
|
|
|
|
return; // no game tick during the wipe
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Game stays frozen until the player presses any key.
|
|
|
|
|
if self.post_transition_wait {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal path: advance the scroll overlay and tick the game.
|
2026-06-11 21:45:50 -05:00
|
|
|
self.overlay.advance_anim(dt.as_secs_f32());
|
|
|
|
|
if self.overlay.close_finished() {
|
|
|
|
|
game.close_scroll(self.overlay.take_choice().as_deref());
|
|
|
|
|
self.overlay = ScrollOverlayState::default();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if game.active_scroll.is_none() {
|
|
|
|
|
game.tick(dt);
|
|
|
|
|
if game.active_scroll.is_some() && self.overlay.anim.is_none() {
|
|
|
|
|
self.overlay.anim = Some(ScrollAnimState::Opening(0.0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Starts the scroll-close animation with the optional player choice.
|
|
|
|
|
/// Uses the current opening progress if the overlay was still animating in.
|
|
|
|
|
pub(crate) fn begin_close(&mut self, choice: Option<String>) {
|
|
|
|
|
let progress = match self.overlay.anim {
|
|
|
|
|
Some(ScrollAnimState::Opening(p)) => p.min(1.0),
|
|
|
|
|
_ => 1.0,
|
|
|
|
|
};
|
|
|
|
|
self.overlay.pending_choice = choice;
|
|
|
|
|
self.overlay.anim = Some(ScrollAnimState::Closing(progress));
|
|
|
|
|
}
|
2026-06-11 21:55:53 -05:00
|
|
|
}
|