animations

This commit is contained in:
2026-06-15 09:15:30 -05:00
parent 77eba1a09c
commit 32aef1ea08
9 changed files with 394 additions and 313 deletions
+49 -46
View File
@@ -3,28 +3,26 @@ use std::rc::Rc;
use std::time::Duration;
use kiln_core::Board;
use kiln_core::game::GameState;
use crate::animation::Animation;
use crate::log::LogState;
use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayState};
use crate::scroll_overlay::{ScrollCloseAnimation, ScrollOpenAnimation, ScrollOverlayState};
use crate::transition::TransitionAnimation;
/// 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.
/// View-only UI state for the log panel, scroll overlay, and board animations.
/// Presentation-only state; lives in the front-end, not on [`GameState`].
pub(crate) struct Ui {
/// State for the log panel (open/closed, height, scroll position).
pub(crate) log: LogState,
/// Scroll position, bounds, and animation state for an active scroll overlay.
/// Scroll offset and bounds for the fully-open scroll overlay.
pub(crate) overlay: ScrollOverlayState,
/// 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,
/// The currently running animation, if any. All input is suppressed while `Some`.
pub(crate) active_animation: Option<Box<dyn Animation>>,
}
impl Default for Ui {
@@ -33,8 +31,7 @@ impl Default for Ui {
log: LogState::new(),
overlay: ScrollOverlayState::default(),
pending_transition: None,
transition: None,
post_transition_wait: false,
active_animation: None,
}
}
}
@@ -48,48 +45,54 @@ impl Ui {
/// Advances all time-driven UI state by `dt` and drives the game tick.
///
/// 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.
/// While any animation is running, `game.tick` is suppressed. When an
/// animation finishes, [`input_mode_after`](Animation::input_mode_after) is
/// consulted: returning to [`Board`](crate::input::InputMode::Board) mode
/// immediately calls [`handle_scroll`](GameState::handle_scroll) so the
/// closed overlay is cleared before the next draw rather than flickering open
/// for one extra frame.
pub(crate) fn tick(&mut self, dt: Duration, game: &mut GameState) {
// 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;
// Advance the active animation; on completion, dispatch any mode-change effect.
if let Some(ref mut anim) = self.active_animation {
if anim.update(dt.as_secs_f32()) {
let mode = anim.input_mode_after();
self.active_animation = None;
// Returning to Board mode: eagerly close any scroll so it isn't
// rendered as open on the next frame before the game tick runs.
if matches!(mode, crate::input::InputMode::Board) {
game.handle_scroll();
}
}
return; // no game tick during the wipe
return; // no game tick during any animation (even when animation just ended)
}
// 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.
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));
}
// Normal path: tick the game, then check if a scroll appeared.
game.tick(dt);
if let Some(scroll) = &game.active_scroll {
let lines = scroll.lines.clone();
self.active_animation = Some(Box::new(ScrollOpenAnimation::new(lines)));
}
}
/// 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));
/// Sets the player's scroll choice and starts the close animation.
///
/// `choice` is `None` when the player dismisses without selecting; it is
/// stored on the active scroll and dispatched by the engine on the next tick.
pub(crate) fn begin_close(&mut self, choice: Option<String>, game: &mut GameState) {
if let Some(scroll) = game.active_scroll.as_mut() {
scroll.choice = choice;
}
let lines = game.active_scroll.as_ref()
.map(|s| s.lines.clone())
.unwrap_or_default();
self.active_animation = Some(Box::new(ScrollCloseAnimation::new(lines)));
}
/// Initialises the board-wipe transition animation once the draw area is known.
///
/// Called from `draw_board_area` when [`pending_transition`](Ui::pending_transition)
/// is set, so the [`TransitionAnimation`] is sized to the exact inner board area.
pub(crate) fn start_transition(&mut self, old: &Board, new: &Board, area: ratatui::layout::Rect) {
self.active_animation = Some(Box::new(TransitionAnimation::new(old, new, area)));
}
}