Thu Nov 21 12:24:51 PM EST 2024

This commit is contained in:
Silas Bartha 2024-11-21 12:24:51 -05:00
parent addcfff12a
commit fa92174703
Signed by: soaos
GPG Key ID: 9BD3DCC0D56A09B2
9 changed files with 90 additions and 406 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_terminal_dialog"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
[dependencies]

View File

@ -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,
}
}
}

View File

@ -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>,
},
}

View File

@ -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()
}
}

View File

@ -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);

View File

@ -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());
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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((

View File

@ -1,54 +1,77 @@
//! bevy_terminal_display widgets for dialog boxes
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 unicode_segmentation::UnicodeSegmentation as _;
use std::time::{Duration, Instant};
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 bevy::prelude::*;
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 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