animations
This commit is contained in:
@@ -18,6 +18,7 @@ pub(crate) const MOVE_COST: f64 = 0.25;
|
||||
/// Each element of the array passed to the `scroll()` Rhai function becomes a
|
||||
/// `ScrollLine`: a plain string becomes [`Text`](ScrollLine::Text); a two-element
|
||||
/// array `[choice, display]` becomes a [`Choice`](ScrollLine::Choice).
|
||||
#[derive(Clone)]
|
||||
pub enum ScrollLine {
|
||||
/// Plain text, word-wrapped to the scroll window width.
|
||||
Text(String),
|
||||
|
||||
+27
-16
@@ -20,13 +20,18 @@ pub use crate::action::ScrollLine;
|
||||
|
||||
/// An active scroll overlay opened by a scripted object via `scroll()`.
|
||||
///
|
||||
/// While a `Scroll` is present on [`GameState`], the front-end should pause ticks
|
||||
/// and display the overlay. Closed via [`GameState::close_scroll`].
|
||||
/// While a `Scroll` is present on [`GameState`], the front-end should display
|
||||
/// the overlay and pause ticks. When the player selects a choice (or dismisses),
|
||||
/// the front-end sets [`choice`](Scroll::choice); [`GameState::tick`] calls
|
||||
/// [`GameState::handle_scroll`] at the start of each tick to dispatch and clear it.
|
||||
pub struct Scroll {
|
||||
/// The object whose `scroll()` call opened this overlay.
|
||||
pub source: ObjectId,
|
||||
/// The lines of content to display.
|
||||
pub lines: Vec<ScrollLine>,
|
||||
/// The choice key the player selected, or `None` if dismissed without a choice.
|
||||
/// Set by the front-end; consumed by [`GameState::handle_scroll`].
|
||||
pub choice: Option<String>,
|
||||
}
|
||||
|
||||
/// An active speech bubble emitted by a scripted object via `say()`.
|
||||
@@ -59,7 +64,7 @@ pub struct GameState {
|
||||
/// Active speech bubbles from `say()` calls, displayed by the front-end over the board.
|
||||
pub speech_bubbles: Vec<SpeechBubble>,
|
||||
/// An active scroll overlay opened by `scroll()`. `Some` while the overlay is
|
||||
/// visible; the front-end pauses ticks and closes it via [`close_scroll`](GameState::close_scroll).
|
||||
/// visible; the front-end pauses ticks and sets [`Scroll::choice`] before resuming.
|
||||
pub active_scroll: Option<Scroll>,
|
||||
/// Remaining seconds for the board-entry transition animation, set to `1.0`
|
||||
/// by [`enter_board`](GameState::enter_board). Front-ends tick this down and
|
||||
@@ -160,10 +165,14 @@ impl GameState {
|
||||
}
|
||||
|
||||
/// Advances real-time game state by `dt` (the elapsed time since the last tick).
|
||||
/// Called once per frame by the front-end's game loop. Counts down object
|
||||
/// cooldowns, drives every scripted object's `tick(dt)` hook (pumping each queue),
|
||||
/// then resolves the actions that were promoted onto the board queue.
|
||||
/// Called once per frame by the front-end's game loop. First processes any active
|
||||
/// scroll (dispatching the player's choice if set), then drives object tick hooks
|
||||
/// and resolves queued actions.
|
||||
pub fn tick(&mut self, dt: Duration) {
|
||||
// Process any pending scroll choice before running scripts; ticks are
|
||||
// suppressed by the front-end while a scroll overlay is visible, so
|
||||
// this runs exactly once per player interaction with a scroll.
|
||||
self.handle_scroll();
|
||||
let secs = dt.as_secs_f64();
|
||||
self.scripts.run_tick(secs);
|
||||
// Expire speech bubbles before resolving new actions so a fresh say()
|
||||
@@ -268,7 +277,7 @@ impl GameState {
|
||||
}
|
||||
// Later scrolls overwrite earlier ones from the same tick.
|
||||
Action::Scroll(lines) => {
|
||||
new_scroll = Some(Scroll { source: ba.source, lines });
|
||||
new_scroll = Some(Scroll { source: ba.source, lines, choice: None });
|
||||
}
|
||||
Action::Teleport { x, y } => {
|
||||
if !board.in_bounds((x, y)) {
|
||||
@@ -311,16 +320,18 @@ impl GameState {
|
||||
self.drain_errors();
|
||||
}
|
||||
|
||||
/// Closes the active scroll overlay, optionally dispatching a named choice
|
||||
/// back to the object that opened it via `send`.
|
||||
/// Consumes the active scroll, dispatching the player's choice (if any) back
|
||||
/// to the source object via `send`.
|
||||
///
|
||||
/// Call from the front-end when the player dismisses the scroll or selects a
|
||||
/// choice. If `choice` is `Some`, the source object receives a `send` call with
|
||||
/// that string as the function name (e.g. `"eat"` triggers `fn eat()`).
|
||||
pub fn close_scroll(&mut self, choice: Option<&str>) {
|
||||
let source = self.active_scroll.take().map(|s| s.source);
|
||||
if let (Some(src), Some(ch)) = (source, choice) {
|
||||
self.scripts.run_send(src, ch, None);
|
||||
/// Called automatically by [`tick`](GameState::tick) as its first step. The
|
||||
/// front-end sets [`Scroll::choice`] before resuming ticks; if no choice was
|
||||
/// made (player dismissed), `choice` stays `None` and the scroll is cleared
|
||||
/// without dispatching.
|
||||
pub fn handle_scroll(&mut self) {
|
||||
if let Some(scroll) = self.active_scroll.take()
|
||||
&& let Some(choice) = scroll.choice
|
||||
{
|
||||
self.scripts.run_send(scroll.source, &choice, None);
|
||||
self.drain_errors();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ fn scroll_opens_on_player_bump() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_scroll_without_choice_clears_it() {
|
||||
fn handle_scroll_without_choice_clears_it() {
|
||||
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", r#"fn bump(id) { scroll(["Hello"]); }"#)]));
|
||||
game.run_init();
|
||||
@@ -272,14 +272,15 @@ fn close_scroll_without_choice_clears_it() {
|
||||
game.tick(Duration::from_millis(16));
|
||||
assert!(game.active_scroll.is_some());
|
||||
|
||||
game.close_scroll(None);
|
||||
// No choice set — next tick clears the scroll without dispatching.
|
||||
game.tick(Duration::from_millis(16));
|
||||
assert!(game.active_scroll.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_scroll_with_choice_dispatches_send_to_source() {
|
||||
// Closing with a choice string fires send() on the object that opened the
|
||||
// scroll; fn eat() logging "eaten" confirms the dispatch arrived.
|
||||
fn handle_scroll_with_choice_dispatches_send_to_source() {
|
||||
// Setting choice on the scroll before a tick fires send() on the source object;
|
||||
// fn eat() logging "eaten" confirms the dispatch arrived.
|
||||
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", r#"
|
||||
fn bump(id) { scroll(["Muffin?", ["eat", "Eat it"]]); }
|
||||
@@ -290,9 +291,9 @@ fn close_scroll_with_choice_dispatches_send_to_source() {
|
||||
game.tick(Duration::from_millis(16));
|
||||
assert!(game.active_scroll.is_some());
|
||||
|
||||
game.close_scroll(Some("eat"));
|
||||
assert!(game.active_scroll.is_none());
|
||||
// eat() queues Action::Log onto the board queue; a tick resolves it.
|
||||
// Set the choice, then tick — handle_scroll dispatches "eat" and resolve picks up the log.
|
||||
game.active_scroll.as_mut().unwrap().choice = Some("eat".to_string());
|
||||
game.tick(Duration::from_millis(16));
|
||||
assert!(game.active_scroll.is_none());
|
||||
assert!(log_texts(&game).iter().any(|t| t == "eaten"), "eat() should have logged");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user