Added scroll()
This commit is contained in:
@@ -29,6 +29,19 @@ use rhai::{Dynamic, ImmutableString, Module};
|
||||
/// How long a move occupies an object before it can act again, in seconds.
|
||||
pub(crate) const MOVE_COST: f64 = 0.25;
|
||||
|
||||
/// One line of content in a [`Action::Scroll`] overlay.
|
||||
///
|
||||
/// 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).
|
||||
pub enum ScrollLine {
|
||||
/// Plain text, word-wrapped to the scroll window width.
|
||||
Text(String),
|
||||
/// A selectable option: `choice` is the event name sent back to the source
|
||||
/// object via `send`, and `display` is the label shown to the player.
|
||||
Choice { choice: String, display: String },
|
||||
}
|
||||
|
||||
/// One deferred mutation emitted by a script, applied by [`crate::game::GameState`]
|
||||
/// after it is promoted onto the board queue.
|
||||
///
|
||||
@@ -52,6 +65,9 @@ pub(crate) enum Action {
|
||||
SetColor { fg: Option<Rgba8>, bg: Option<Rgba8> },
|
||||
/// Call `fn_name` on `target` object, optionally passing `arg`.
|
||||
Send { target: ObjectId, fn_name: String, arg: Option<ScriptArg> },
|
||||
/// Open a scrollable text overlay. Pauses game ticks while shown; a choice
|
||||
/// selection dispatches [`Send`](Action::Send) to the source object.
|
||||
Scroll(Vec<ScrollLine>),
|
||||
}
|
||||
|
||||
// ── Direction helpers ─────────────────────────────────────────────────────────
|
||||
@@ -128,6 +144,10 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||
m.insert("arg".into(), dyn_arg);
|
||||
}
|
||||
}
|
||||
// Scroll lines are not round-trippable via the queue map API; emit type only.
|
||||
Action::Scroll(_) => {
|
||||
ins_str(&mut m, "type", "Scroll");
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
@@ -202,6 +222,7 @@ impl TryFrom<rhai::Map> for Action {
|
||||
};
|
||||
Ok(Action::Send { target: target as ObjectId, fn_name, arg })
|
||||
}
|
||||
"Scroll" => Err("Scroll actions cannot be constructed from maps".to_string()),
|
||||
other => Err(format!("unknown action type: '{other}'")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,21 @@ use crate::utils::{Direction, ObjectId, ScriptArg, Solid};
|
||||
/// How long a `say()` speech bubble stays on screen, in seconds.
|
||||
pub const SAY_DURATION: f64 = 3.0;
|
||||
|
||||
// Re-export ScrollLine so kiln-tui can pattern-match scroll content without
|
||||
// accessing the private `action` module directly.
|
||||
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`].
|
||||
pub struct Scroll {
|
||||
/// The object whose `scroll()` call opened this overlay.
|
||||
pub source: ObjectId,
|
||||
/// The lines of content to display.
|
||||
pub lines: Vec<ScrollLine>,
|
||||
}
|
||||
|
||||
/// An active speech bubble emitted by a scripted object via `say()`.
|
||||
pub struct SpeechBubble {
|
||||
/// The object whose `say()` call created this bubble.
|
||||
@@ -38,6 +53,9 @@ pub struct GameState {
|
||||
scripts: ScriptHost,
|
||||
/// 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).
|
||||
pub active_scroll: Option<Scroll>,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
@@ -54,6 +72,7 @@ impl GameState {
|
||||
log: Vec::new(),
|
||||
scripts,
|
||||
speech_bubbles: Vec::new(),
|
||||
active_scroll: None,
|
||||
};
|
||||
// Surface any compile-time script errors collected during ScriptHost::new.
|
||||
state.drain_errors();
|
||||
@@ -120,6 +139,8 @@ impl GameState {
|
||||
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
|
||||
let mut sends: Vec<(ObjectId, String, Option<ScriptArg>)> = Vec::new();
|
||||
let mut new_bubbles: Vec<SpeechBubble> = Vec::new();
|
||||
// Collected outside the board borrow so we can assign to self.active_scroll.
|
||||
let mut new_scroll: Option<Scroll> = None;
|
||||
{
|
||||
let mut board = self.board_mut();
|
||||
for ba in actions {
|
||||
@@ -159,6 +180,10 @@ impl GameState {
|
||||
Action::Send { target, fn_name, arg } => {
|
||||
sends.push((target, fn_name, arg));
|
||||
}
|
||||
// Later scrolls overwrite earlier ones from the same tick.
|
||||
Action::Scroll(lines) => {
|
||||
new_scroll = Some(Scroll { source: ba.source, lines });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +193,9 @@ impl GameState {
|
||||
self.speech_bubbles.retain(|b| b.object_id != bubble.object_id);
|
||||
self.speech_bubbles.push(bubble);
|
||||
}
|
||||
if let Some(scroll) = new_scroll {
|
||||
self.active_scroll = Some(scroll);
|
||||
}
|
||||
for (bumped, bumper) in bumps {
|
||||
self.scripts.run_bump(bumped, bumper);
|
||||
}
|
||||
@@ -177,6 +205,20 @@ impl GameState {
|
||||
self.drain_errors();
|
||||
}
|
||||
|
||||
/// Closes the active scroll overlay, optionally dispatching a named choice
|
||||
/// back to the object that opened it 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);
|
||||
self.drain_errors();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to move the player one cell in `dir`.
|
||||
///
|
||||
/// The move is ignored if the target cell is out of bounds, or it is neither
|
||||
|
||||
+23
-2
@@ -45,12 +45,12 @@
|
||||
//!
|
||||
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`, `Queue.push(map)`
|
||||
|
||||
use crate::action::{action_to_map, rhai_action_module, Action, MOVE_COST};
|
||||
use crate::action::{action_to_map, rhai_action_module, Action, ScrollLine, MOVE_COST};
|
||||
use crate::board::Board;
|
||||
use crate::log::LogLine;
|
||||
use crate::map_file::{color_to_hex, parse_color};
|
||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||
use rhai::{CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope, AST};
|
||||
use rhai::{Array, CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope, AST};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
@@ -613,6 +613,27 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
emit(&q, source_of(&ctx), Action::Say(msg.to_string()));
|
||||
});
|
||||
|
||||
// scroll(lines): open a full-screen scrollable overlay. Each element of `lines`
|
||||
// is either a plain string (text) or a 2-element array [choice_key, display_text].
|
||||
let q = queues.clone();
|
||||
engine.register_fn("scroll", move |ctx: NativeCallContext, arr: Array| {
|
||||
let lines = arr.into_iter().filter_map(|item| {
|
||||
if let Ok(s) = item.clone().into_string() {
|
||||
Some(ScrollLine::Text(s))
|
||||
} else {
|
||||
let pair: Vec<Dynamic> = item.into_array().ok()?;
|
||||
if pair.len() == 2 {
|
||||
let choice = pair[0].clone().into_string().ok()?;
|
||||
let display = pair[1].clone().into_string().ok()?;
|
||||
Some(ScrollLine::Choice { choice, display })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}).collect();
|
||||
emit(&q, source_of(&ctx), Action::Scroll(lines));
|
||||
});
|
||||
|
||||
let q = queues.clone();
|
||||
engine.register_fn(
|
||||
"set_tag",
|
||||
|
||||
Reference in New Issue
Block a user