diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/actor/components.rs | 52 | ||||
-rw-r--r-- | src/actor/events.rs | 72 | ||||
-rw-r--r-- | src/actor/mod.rs | 95 | ||||
-rw-r--r-- | src/actor/resources.rs | 8 | ||||
-rw-r--r-- | src/actor/systems.rs | 121 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/systems.rs | 19 | ||||
-rw-r--r-- | src/widgets.rs | 121 |
9 files changed, 89 insertions, 405 deletions
@@ -1,6 +1,6 @@ [package] name = "bevy_terminal_dialog" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/src/actor/components.rs b/src/actor/components.rs deleted file mode 100644 index 43987c0..0000000 --- a/src/actor/components.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Components related to actors - -use bevy::{prelude::*, utils::HashMap}; -use yarnspinner::{compiler::{Compiler, File}, core::{Library, LineId}, runtime::{Dialogue, MemoryVariableStorage, StringTableTextProvider}}; - -/// Main actor component, holds state about dialogue along with the dialogue runner itself -#[derive(Component)] -pub struct Actor { - /// Whether this actor is currently conversing - pub active: bool, - /// Yarnspinner dialogue runner - pub dialogue: Dialogue, - /// Yarnspinner dialogue metadata - pub metadata: HashMap<LineId, Vec<String>>, -} - -impl Actor { - /// Create a new actor from the given source code, starting on the given start node, and with - /// the given function library - pub fn new(file_name: &str, source: &[u8], start_node: &str, function_library: &Library) -> Self { - let compilation = Compiler::new() - .add_file(File { - source: String::from_utf8_lossy(source).into(), - file_name: file_name.into(), - }) - .compile() - .unwrap(); - - let mut base_language_string_table = std::collections::HashMap::new(); - let mut metadata = HashMap::new(); - - for (k, v) in compilation.string_table { - base_language_string_table.insert(k.clone(), v.text); - metadata.insert(k, v.metadata); - } - - let mut text_provider = StringTableTextProvider::new(); - text_provider.extend_base_language(base_language_string_table); - - let mut dialogue = Dialogue::new(Box::new(MemoryVariableStorage::new()), Box::new(text_provider)); - dialogue.library_mut().extend(function_library.clone()); - dialogue.add_program(compilation.program.unwrap()); - dialogue.set_node(start_node).unwrap(); - - Self { - active: false, - dialogue, - metadata, - } - } -} - diff --git a/src/actor/events.rs b/src/actor/events.rs deleted file mode 100644 index e24e7f3..0000000 --- a/src/actor/events.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Actor-related events - -use bevy::prelude::*; -use yarnspinner::{core::LineId, runtime::{Command, DialogueOption, Line, OptionId}}; - -/// Event called by user to progress dialogue -#[derive(Debug, Event)] -pub enum ContinueDialogueEvent { - /// Continue to next line of dialogue for given actor entity - Continue(Entity), - /// Submit option selection to given actor entity - SelectedOption { - /// Target actor entity - actor: Entity, - /// Selected option ID - option: OptionId - }, -} - -/// Event called by plugin in response to a corresponding yarnspinner dialogue events -/// -/// The user should catch these events to update UI, and never call it directly. -#[derive(Event)] -pub enum DialogueEvent { - /// Recieved new line of dialogue - Line { - /// Actor entity - actor: Entity, - /// Line of dialogue received - line: Line, - }, - /// Dialogue complete - DialogueComplete { - /// Actor entity - actor: Entity, - }, - /// Encountered an option selection - Options { - /// Actor entity - actor: Entity, - /// Options to select from - options: Vec<DialogueOption>, - }, - /// Triggered a yarnspinner command - Command { - /// Actor entity - actor: Entity, - /// Triggered command - command: Command, - }, - /// Node started - NodeStart { - /// Actor entity - actor: Entity, - /// Name of started node - name: String, - }, - /// Node complete - NodeComplete { - /// Actor entity - actor: Entity, - /// Name of completed node - name: String, - }, - /// Received line hints - LineHints { - /// Actor entity - actor: Entity, - /// Lines affected - lines: Vec<LineId>, - }, -} diff --git a/src/actor/mod.rs b/src/actor/mod.rs deleted file mode 100644 index 3ecc32e..0000000 --- a/src/actor/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! NPCs containing their own individual yarnspinner contexts -// TODO: Split off into own crate? - -use std::sync::{Arc, Mutex}; - -use bevy::{prelude::*, utils::HashMap}; -use lazy_static::lazy_static; -use resources::FunctionLibrary; -use yarnspinner::core::YarnValue; - -pub mod components; -pub mod events; -pub mod resources; -mod systems; - -lazy_static! { - /// Custom yarnspinner variable storage - /// Stores variables as <instance>.<varname> - /// Global variables are stored in the "global" instance - pub static ref DIRWORLD_VARIABLE_STORAGE: Arc<Mutex<DirworldVariableStorage>> = - Arc::new(Mutex::new(DirworldVariableStorage::default())); -} - -/// Plugin which controls the behavior of actors -pub struct ActorPlugin; - -impl Plugin for ActorPlugin { - fn build(&self, app: &mut App) { - let mut function_library = FunctionLibrary::default(); - function_library.add_function("get_string", get_string); - function_library.add_function("get_number", get_number); - function_library.add_function("get_bool", get_bool); - - app.add_systems( - Update, - (systems::handle_dialog_initiation, systems::progress_dialog, systems::handle_variable_set_commands), - ) - .insert_resource(function_library) - .add_event::<events::ContinueDialogueEvent>() - .add_event::<events::DialogueEvent>(); - } -} - -fn get_string(instance_name: &str, var_name: &str) -> String { - if let Some(YarnValue::String(value)) = DIRWORLD_VARIABLE_STORAGE - .lock() - .unwrap() - .get(instance_name, var_name) - { - value - } else { - "".into() - } -} - -fn get_number(instance_name: &str, var_name: &str) -> f32 { - if let Some(YarnValue::Number(value)) = DIRWORLD_VARIABLE_STORAGE - .lock() - .unwrap() - .get(instance_name, var_name) - { - value - } else { - 0.0 - } -} - -fn get_bool(instance_name: &str, var_name: &str) -> bool { - if let Some(YarnValue::Boolean(value)) = DIRWORLD_VARIABLE_STORAGE - .lock() - .unwrap() - .get(instance_name, var_name) - { - value - } else { - false - } -} - -/// Variable Storage -#[derive(Default, Debug)] -pub struct DirworldVariableStorage(pub HashMap<String, YarnValue>); - -impl DirworldVariableStorage { - /// Set value of instance variable (use "global" for global) - pub fn set(&mut self, instance_name: &str, var_name: &str, value: YarnValue) { - self.0.insert(format!("{instance_name}.{var_name}"), value); - } - - /// Get value of instance variable (use "global" for global) - pub fn get(&self, instance_name: &str, var_name: &str) -> Option<YarnValue> { - self.0.get(&format!("{instance_name}.{var_name}")).cloned() - } -} - diff --git a/src/actor/resources.rs b/src/actor/resources.rs deleted file mode 100644 index 76ead59..0000000 --- a/src/actor/resources.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Actor-related resources - -use bevy::prelude::*; -use yarnspinner::core::Library; - -/// Library of yarnspinner function callbacks -#[derive(Resource, Deref, DerefMut, Default, Debug)] -pub struct FunctionLibrary(pub Library); diff --git a/src/actor/systems.rs b/src/actor/systems.rs deleted file mode 100644 index a719858..0000000 --- a/src/actor/systems.rs +++ /dev/null @@ -1,121 +0,0 @@ -use bevy::prelude::*; -use bevy_basic_interaction::events::InteractionEvent; -use yarnspinner::core::YarnValue; - -use super::{ - components::Actor, - events::{ContinueDialogueEvent, DialogueEvent}, DIRWORLD_VARIABLE_STORAGE, -}; - -pub fn handle_dialog_initiation( - mut event_reader: EventReader<InteractionEvent>, - mut actor_query: Query<(Entity, &mut Actor)>, - mut event_writer: EventWriter<ContinueDialogueEvent>, -) { - for InteractionEvent { interactable, .. } in event_reader.read() { - if let Ok((actor_entity, mut actor)) = actor_query.get_mut(*interactable) { - actor.active = true; - event_writer.send(ContinueDialogueEvent::Continue(actor_entity)); - } - } -} - -pub fn progress_dialog( - mut event_reader: EventReader<ContinueDialogueEvent>, - mut actor_query: Query<&mut Actor>, - mut event_writer: EventWriter<DialogueEvent>, -) { - for event in event_reader.read() { - let actor_entity = match event { - ContinueDialogueEvent::Continue(actor) => actor, - ContinueDialogueEvent::SelectedOption { actor, .. } => actor, - }; - - if let Ok(mut actor) = actor_query.get_mut(*actor_entity) { - if let ContinueDialogueEvent::SelectedOption { option, .. } = event { - actor.dialogue.set_selected_option(*option).unwrap(); - } - if actor.dialogue.current_node().is_none() { - actor.dialogue.set_node("Start").unwrap(); - } - match actor.dialogue.continue_() { - Ok(events) => { - info!("BATCH"); - for event in events { - info!("Event: {:?}", event); - match event { - yarnspinner::prelude::DialogueEvent::Line(line) => { - event_writer.send(DialogueEvent::Line { - actor: *actor_entity, - line, - }); - } - yarnspinner::prelude::DialogueEvent::DialogueComplete => { - event_writer.send(DialogueEvent::DialogueComplete { - actor: *actor_entity, - }); - } - yarnspinner::prelude::DialogueEvent::Options(options) => { - event_writer.send(DialogueEvent::Options { - actor: *actor_entity, - options, - }); - } - yarnspinner::runtime::DialogueEvent::Command(command) => { - event_writer.send(DialogueEvent::Command { - actor: *actor_entity, - command, - }); - } - yarnspinner::runtime::DialogueEvent::NodeStart(name) => { - event_writer.send(DialogueEvent::NodeStart { - actor: *actor_entity, - name, - }); - } - yarnspinner::runtime::DialogueEvent::NodeComplete(name) => { - event_writer.send(DialogueEvent::NodeComplete { - actor: *actor_entity, - name, - }); - } - yarnspinner::runtime::DialogueEvent::LineHints(lines) => { - event_writer.send(DialogueEvent::LineHints { - actor: *actor_entity, - lines, - }); - } - } - } - } - Err(err) => error!("{:?}", err), - } - } - } -} - -pub fn handle_variable_set_commands( - mut event_reader: EventReader<DialogueEvent>, - mut event_writer: EventWriter<ContinueDialogueEvent>, -) { - for event in event_reader.read() { - if let DialogueEvent::Command { command, actor } = event { - if command.name != "set_var" { - continue; - } - - event_writer.send(ContinueDialogueEvent::Continue(*actor)); - - if command.parameters.len() != 3 { - warn!("Incorrect number of parameters passed to set command: {}", command.parameters.len()); - continue; - } - - if let YarnValue::String(instance_name) = &command.parameters[0] { - if let YarnValue::String(var_name) = &command.parameters[1] { - DIRWORLD_VARIABLE_STORAGE.lock().unwrap().set(instance_name, var_name, command.parameters[2].clone()); - } - } - } - } -} @@ -8,14 +8,12 @@ use bevy::prelude::*; mod systems; pub mod widgets; pub mod util; -pub mod actor; /// Plugin which provides dialog functionality pub struct TerminalDialogPlugin; impl Plugin for TerminalDialogPlugin { fn build(&self, app: &mut App) { - app.add_plugins(actor::ActorPlugin) - .add_systems(Startup, systems::setup); + app.add_systems(Startup, systems::setup); } } diff --git a/src/systems.rs b/src/systems.rs index 444e99f..fc94d9a 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,24 +1,21 @@ +use std::time::Duration; + use bevy::prelude::*; use bevy_terminal_display::widgets::components::Widget; -use super::widgets::{DialogBox, DialogBoxWidget, InteractTooltip, InteractTooltipWidget, OptionsBox, OptionsBoxWidget}; +use super::widgets::{DialogBox, DialogBoxWidget, OptionsBox, OptionsBoxWidget}; pub fn setup(mut commands: Commands) { commands.spawn(( - InteractTooltip, - Widget { - enabled: false, - depth: 0, - widget: Box::new(InteractTooltipWidget), - }, - )); - - commands.spawn(( DialogBox, Widget { enabled: false, depth: 0, - widget: Box::<DialogBoxWidget>::default(), + widget: Box::new(DialogBoxWidget::new( + None, + vec![], + Duration::from_millis(25), + )), }, )); commands.spawn(( diff --git a/src/widgets.rs b/src/widgets.rs index 52eff3c..c1e057f 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,54 +1,77 @@ //! bevy_terminal_display widgets for dialog boxes +use std::time::{Duration, Instant}; + +use arbitrary_chunks::ArbitraryChunks as _; use bevy::prelude::*; -use bevy_terminal_display::{crossterm, ratatui::{layout::{Alignment, Constraint, Flex, Layout}, style::Style, text::{Line, Span}, widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap}}, widgets::TerminalWidget}; -use yarnspinner::runtime::DialogueOption; +use bevy_terminal_display::{ + crossterm, + ratatui::{ + layout::{Constraint, Flex, Layout}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{ + Block, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, + Wrap, + }, + }, + widgets::TerminalWidget, +}; use unicode_segmentation::UnicodeSegmentation as _; -use arbitrary_chunks::ArbitraryChunks as _; - -/// Interaction tooltip widget marker -// TODO: Move tooltip out of this crate? -#[derive(Component)] -pub struct InteractTooltip; - -/// Interaction tooltip widget -pub struct InteractTooltipWidget; - -impl TerminalWidget for InteractTooltipWidget { - fn render( - &mut self, - frame: &mut bevy_terminal_display::ratatui::Frame, - rect: bevy_terminal_display::ratatui::prelude::Rect, - ) { - let text = Paragraph::new("E") - .block( - Block::new() - .borders(Borders::ALL) - .padding(Padding::horizontal(1)), - ) - .alignment(Alignment::Center); - let [area] = Layout::horizontal([Constraint::Length(5)]) - .flex(Flex::Center) - .areas(rect); - let [area] = Layout::vertical([Constraint::Length(3)]) - .flex(Flex::Center) - .areas(area); - frame.render_widget(Clear, area); - frame.render_widget(text, area); - } -} +use yarnspinner::runtime::DialogueOption; /// Dialog box widget marker #[derive(Component)] pub struct DialogBox; /// Dialog box widget -#[derive(Default)] pub struct DialogBoxWidget { /// Name of speaking character - pub character: Option<String>, + character: Option<String>, /// Chunks of text and corresponding styles representing the currently spoken line - pub text: Vec<(String, Style)>, + text: Vec<(String, Style)>, + /// Index of last character to draw (for typewriter effect). + typewriter_index: usize, + /// Time last character was drawn. + last_character: Instant, + /// Speed of typewriter effect (time between characters). + speed: Duration, + /// Number of characters in the text + character_count: usize, +} + +impl DialogBoxWidget { + pub fn new(character: Option<String>, text: Vec<(String, Style)>, speed: Duration) -> Self { + Self { + character, + character_count: text + .iter() + .fold(0, |count, (string, _)| count + string.graphemes(true).count()), + text, + speed, + typewriter_index: 0, + last_character: Instant::now(), + } + } + + pub fn set_character(&mut self, character: Option<String>) { + self.character = character + } + + pub fn set_text(&mut self, text: Vec<(String, Style)>) { + self.character_count = text.iter().fold(0, |count, (string, _)| count + string.graphemes(true).count()); + self.text = text; + self.typewriter_index = 0; + self.last_character = Instant::now(); + } + + pub fn typewriter_complete(&self) -> bool { + self.character_count == 0 || self.typewriter_index >= self.character_count + } + + pub fn skip_typewriter(&mut self) { + self.typewriter_index = self.character_count; + } } impl TerminalWidget for DialogBoxWidget { @@ -57,11 +80,16 @@ impl TerminalWidget for DialogBoxWidget { frame: &mut bevy_terminal_display::ratatui::Frame, rect: bevy_terminal_display::ratatui::prelude::Rect, ) { - let text = Paragraph::new(bevy_terminal_display::ratatui::text::Line::from( + let line = Line::from_iter( self.text .iter() - .map(|(text, style)| Span::styled(text, *style)) - .collect::<Vec<_>>(), + .map(|(text, style)| Span::styled(text, *style)), + ); + let graphemes = line.styled_graphemes(Style::default()); + let text = Paragraph::new(Line::from_iter( + graphemes + .take(self.typewriter_index) + .map(|g| Span::styled(g.symbol, g.style)), )) .wrap(Wrap { trim: true }) .block({ @@ -69,7 +97,7 @@ impl TerminalWidget for DialogBoxWidget { .borders(Borders::ALL) .padding(Padding::horizontal(1)); if let Some(character) = &self.character { - block = block.title(character.clone()); + block = block.title(Line::from(character.clone()).bold().underlined()); } block }); @@ -82,6 +110,15 @@ impl TerminalWidget for DialogBoxWidget { frame.render_widget(Clear, area); frame.render_widget(text, area); } + + fn update(&mut self, _time: &Time, _commands: &mut Commands) { + if self.character_count > 0 && self.typewriter_index < self.character_count { + if self.last_character.elapsed() >= self.speed { + self.typewriter_index += 1; + self.last_character = Instant::now(); + } + } + } } /// Option selection box widget marker |