aboutsummaryrefslogtreecommitdiff
path: root/src/actor
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <silas@exvacuum.dev>2024-08-25 02:22:37 -0400
committerLibravatar Silas Bartha <silas@exvacuum.dev>2024-08-25 02:22:37 -0400
commitaddcfff12a76f861e07d844eabfa349e2f4014c1 (patch)
treedf591c562fc39909bd5c13c8fa1b1ae8004bf08a /src/actor
Initial Commitv0.1.0
Diffstat (limited to 'src/actor')
-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
5 files changed, 348 insertions, 0 deletions
diff --git a/src/actor/components.rs b/src/actor/components.rs
new file mode 100644
index 0000000..43987c0
--- /dev/null
+++ b/src/actor/components.rs
@@ -0,0 +1,52 @@
+//! 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
new file mode 100644
index 0000000..e24e7f3
--- /dev/null
+++ b/src/actor/events.rs
@@ -0,0 +1,72 @@
+//! 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
new file mode 100644
index 0000000..3ecc32e
--- /dev/null
+++ b/src/actor/mod.rs
@@ -0,0 +1,95 @@
+//! 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
new file mode 100644
index 0000000..76ead59
--- /dev/null
+++ b/src/actor/resources.rs
@@ -0,0 +1,8 @@
+//! 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
new file mode 100644
index 0000000..a719858
--- /dev/null
+++ b/src/actor/systems.rs
@@ -0,0 +1,121 @@
+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());
+ }
+ }
+ }
+ }
+}