aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml2
-rw-r--r--src/actor/components.rs52
-rw-r--r--src/actor/events.rs72
-rw-r--r--src/actor/mod.rs95
-rw-r--r--src/actor/resources.rs8
-rw-r--r--src/actor/systems.rs121
-rw-r--r--src/lib.rs4
-rw-r--r--src/systems.rs19
-rw-r--r--src/widgets.rs121
9 files changed, 89 insertions, 405 deletions
diff --git a/Cargo.toml b/Cargo.toml
index f4832bb..633f61a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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());
- }
- }
- }
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
index c11e51f..4b383f1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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