hey the dialog system kinda works!

* .mono dialog files
* clicking trees loads the dialog in to the dialog box
* dialog box only visible when dialog is active/idle (not none i.e.,
  hidden)

Next up: Debug info about tree's dialogs and the active dialog
main
Elijah Voigt 4 months ago
parent e7557c67f2
commit 0d0b814b56

2
Cargo.lock generated

@ -2205,6 +2205,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_rapier3d", "bevy_rapier3d",
"serde",
"thiserror 2.0.12",
] ]
[[package]] [[package]]

@ -3,6 +3,13 @@ name = "games"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies]
thiserror = "2.0.12"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
[dependencies.bevy_rapier3d] [dependencies.bevy_rapier3d]
version = "0.30.0" version = "0.30.0"

@ -0,0 +1,27 @@
a
b
c
---
d
e
f
---
g
h
---
i
j
---
k

@ -0,0 +1,29 @@
# This empty dialog should be ignored
---
# This is a comment
this is one line of dialog
this is another options
# This is another comment
---
# this is
# a lot
# of comments
# back to back
together they can make poetry
together they can tell a story
# and a few more for good measure
---
# This should be ignored
---
# This too
---

@ -0,0 +1,19 @@
###
# Industry
# Written by Elijah Voigt
# No copyright, it's bad on purpose
###
industry
---
cars drone in the distance with no sign of stopping...
the roar of a jet echos in the sky somewhere between takeoff and landing...
a train in the distance warns of it's imminent arrival...
---
industry

@ -1,11 +1,18 @@
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::color::palettes::css::{DARK_GREY, DARK_ORANGE, GREY, ORANGE}; mod mono;
use bevy::color::palettes::css::{DARK_ORANGE, ORANGE};
use games::*; use games::*;
use mono::*;
fn main() { fn main() {
App::new() App::new()
.add_plugins(BaseGamePlugin) .add_plugins(BaseGamePlugin)
.add_plugins(MonologueAssetsPlugin)
.add_event::<DialogEvent>()
.init_state::<DialogState>()
.insert_resource(ClearColor(WHITE.into())) .insert_resource(ClearColor(WHITE.into()))
.add_systems( .add_systems(
Startup, Startup,
@ -14,9 +21,19 @@ fn main() {
.add_systems( .add_systems(
Update, Update,
( (
dialog_engine.run_if(input_just_pressed(KeyCode::KeyN)), // Start a dialog if none are running
start_dialog
.run_if(in_state(DialogState::None))
.run_if(on_event::<Pointer<Click>>),
// Close the dialog box if it is idle (not choosing)
end_dialog
.run_if(in_state(DialogState::Idle))
.run_if(on_event::<Pointer<Click>>),
dialog_engine.run_if(on_event::<DialogEvent>),
mouse_wheel_scroll.run_if(on_event::<MouseWheel>), mouse_wheel_scroll.run_if(on_event::<MouseWheel>),
auto_scroll.run_if(any_component_added::<DialogOption>), auto_scroll.run_if(any_component_added::<DialogOption>),
dialog_box_visibility
.run_if(state_changed::<DialogState>)
), ),
) )
.add_observer(add_dialog_option) .add_observer(add_dialog_option)
@ -28,6 +45,9 @@ fn main() {
#[derive(Component)] #[derive(Component)]
struct Tree; struct Tree;
#[derive(Component, PartialEq)]
struct TreeMonologue(Handle<Monologue>);
/// Initialize the trees, currently placeholders /// Initialize the trees, currently placeholders
/// Trees are 2d cards in a 3d world for flexibility /// Trees are 2d cards in a 3d world for flexibility
/// Might move fully 2d if the art style allows it /// Might move fully 2d if the art style allows it
@ -49,10 +69,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend, alpha_mode: AlphaMode::Blend,
..default() ..default()
}); });
let monologue: Handle<Monologue> = server.load("trees/red.mono");
let tree_transform_red = let tree_transform_red =
Transform::from_xyz(-15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0)); Transform::from_xyz(-15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn(( commands.spawn((
Tree, Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()), Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_red), MeshMaterial3d(tree_material_red),
tree_transform_red, tree_transform_red,
@ -68,10 +90,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend, alpha_mode: AlphaMode::Blend,
..default() ..default()
}); });
let monologue: Handle<Monologue> = server.load("trees/green.mono");
let tree_transform_green = let tree_transform_green =
Transform::from_xyz(15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0)); Transform::from_xyz(15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn(( commands.spawn((
Tree, Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()), Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_green), MeshMaterial3d(tree_material_green),
tree_transform_green, tree_transform_green,
@ -87,10 +111,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend, alpha_mode: AlphaMode::Blend,
..default() ..default()
}); });
let monologue: Handle<Monologue> = server.load("trees/blue.mono");
let tree_transform_blue = let tree_transform_blue =
Transform::from_xyz(0.0, 0.0, -15.0).with_scale(Vec3::splat(10.0)); Transform::from_xyz(0.0, 0.0, -15.0).with_scale(Vec3::splat(10.0));
commands.spawn(( commands.spawn((
Tree, Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()), Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_blue), MeshMaterial3d(tree_material_blue),
tree_transform_blue, tree_transform_blue,
@ -109,6 +135,7 @@ fn init_ui(mut commands: Commands) {
.spawn(( .spawn((
DialogBox, DialogBox,
BackgroundColor(BLACK.with_alpha(0.9).into()), BackgroundColor(BLACK.with_alpha(0.9).into()),
DialogState::Ongoing,
Node { Node {
align_self: AlignSelf::End, align_self: AlignSelf::End,
justify_self: JustifySelf::Center, justify_self: JustifySelf::Center,
@ -195,36 +222,145 @@ struct DialogOption;
#[derive(Component)] #[derive(Component)]
struct DialogLine; struct DialogLine;
/// Events that drive the dialog engine
#[derive(Event, PartialEq)]
enum DialogEvent {
Start(Handle<Monologue>),
NextBatch,
End,
}
/// State tracking if we are actively "doing a monologue"
#[derive(States, Debug, Hash, Eq, PartialEq, Clone, Component, Default)]
enum DialogState {
// Dialog is running and being interacted with
Ongoing,
// There is no more dialog so it's just sitting there
Idle,
// Dialog box is not visible
#[default]
None,
}
/// Start dialog, will expand later with handle to monologue asset
fn start_dialog(
mut click_events: EventReader<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
query: Query<&TreeMonologue>,
) {
click_events.read().for_each(|event| {
info!("Click event detected");
if let Ok(TreeMonologue(handle)) = query.get(event.target) {
info!("Tree Monologue received, sending start dialog event");
dialog_events.write(DialogEvent::Start(handle.clone()));
dialog_events.write(DialogEvent::NextBatch);
}
})
}
/// When dialog is complete and you click away from the dialog box, close it out
fn end_dialog(
mut click_events: EventReader<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
query: Query<Entity, Or<(With<TreeMonologue>, With<DialogBox>, With<DialogOption>)>>,
) {
click_events.read().for_each(|event| {
info!("Click even triggered end of dialog: {:?}", event.target);
if !query.contains(event.target) {
dialog_events.write(DialogEvent::End);
}
});
}
/// System which puts DialogOptions into the DialogBox /// System which puts DialogOptions into the DialogBox
fn dialog_engine( fn dialog_engine(
mut commands: Commands, // React to dialog events
mut events: EventReader<DialogEvent>,
// Reference to DialogBox
dialog_box: Single<Entity, With<DialogBox>>, dialog_box: Single<Entity, With<DialogBox>>,
// EntityCommands for Dialog Box
mut commands: Commands,
// Handle to "active" monologue
mut handle: Local<Handle<Monologue>>,
// Index into "active" monologue
mut idx: Local<usize>, mut idx: Local<usize>,
// Inform the rest of the game what state of dialog we are in
mut next_state: ResMut<NextState<DialogState>>,
// Monologue assets for obvious reasons
monologues: Res<Assets<Monologue>>,
// Monologue trees so we can remove that component at end of monologue
monologue_trees: Query<(Entity, &TreeMonologue)>,
// Dialog lines to despawn them at the end/start of a dialog
lines: Query<Entity, With<DialogLine>>,
) { ) {
let dialog: Vec<Vec<&str>> = vec![ debug_assert!(
vec!["A", "B", "C"], !events.is_empty(),
vec!["E", "F", "G"], "Dialog engine is triggered by Dialog Events"
vec!["H", "I"], );
vec!["J", "K"],
vec!["L"], events.read().for_each(|event| {
]; match event {
DialogEvent::Start(h) => {
debug!("Dialog start: {:?}", h);
debug!("Show options: {:?}", dialog.get(*idx)); // Set state to "Active"
next_state.set(DialogState::Ongoing);
// Copy monologue asset into local
*handle = h.clone();
}
DialogEvent::NextBatch => {
debug!("Dialog batch");
commands.entity(*dialog_box).with_children(|parent| { commands.entity(*dialog_box).with_children(|parent| {
if let Some(options) = dialog.get(*idx) { // Fetch this monologue from the assets
if let Some(monologue) = monologues.get(handle.clone().id()) {
// Fetch this batch of options
if let Some(options) = monologue.get(*idx) {
// Spawn the dialog options in the dialog box
options.iter().for_each(|option| { options.iter().for_each(|option| {
parent.spawn((Text::new(*option), DialogOption)); parent.spawn((Text::new(option.clone()), DialogOption));
});
*idx += 1;
} else {
// Set the dialog state to "Idle"
next_state.set(DialogState::Idle);
}
}
}); });
} }
DialogEvent::End => {
debug!("Dialog ended");
// Remove the TreeMonologue component from the active tree
monologue_trees.iter().filter_map(|(entity, tree_monologue)| {
(*tree_monologue == TreeMonologue(handle.clone())).then_some(entity)
}).for_each(|e| {
commands.entity(e).remove::<TreeMonologue>();
}); });
*idx += 1; // Remove lines from dialog box
lines.iter().for_each(|e| {
commands.entity(e).despawn();
});
// Reset index into the dialog options array
*idx = 0;
// Wipe the current handle from context
*handle = Handle::default();
// Set state to "Active"
next_state.set(DialogState::None);
}
}
});
} }
/// When a dialog option is chosen (clicked on) we do the following: /// When a dialog option is chosen (clicked on) we do the following:
fn choose_dialog_option( fn choose_dialog_option(
trigger: Trigger<Pointer<Click>>, trigger: Trigger<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
mut commands: Commands, mut commands: Commands,
texts: Query<&Text>, texts: Query<&Text>,
options: Query<Entity, With<DialogOption>>, options: Query<Entity, With<DialogOption>>,
@ -243,6 +379,9 @@ fn choose_dialog_option(
parent.spawn((t.clone(), DialogLine)); parent.spawn((t.clone(), DialogLine));
}); });
} }
// trigger the next dialog line
dialog_events.write(DialogEvent::NextBatch);
} }
fn hover_dialog_option_over( fn hover_dialog_option_over(
@ -251,13 +390,17 @@ fn hover_dialog_option_over(
) { ) {
if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) { if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) {
*tc = TextColor(DARK_ORANGE.into()); *tc = TextColor(DARK_ORANGE.into());
bg.set_alpha(1.0); bg.0.set_alpha(1.0);
} }
} }
fn hover_dialog_option_out(trigger: Trigger<Pointer<Out>>, mut query: Query<&mut TextColor>) { fn hover_dialog_option_out(
if let Ok(mut tc) = query.get_mut(trigger.target()) { trigger: Trigger<Pointer<Out>>,
mut query: Query<(&mut TextColor, &mut BackgroundColor)>,
) {
if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) {
*tc = TextColor(ORANGE.into()); *tc = TextColor(ORANGE.into());
bg.0.set_alpha(0.0);
} }
} }
@ -283,3 +426,14 @@ fn add_dialog_option(trigger: Trigger<OnAdd, DialogOption>, mut commands: Comman
fn add_dialog_line(trigger: Trigger<OnAdd, DialogLine>, mut commands: Commands) { fn add_dialog_line(trigger: Trigger<OnAdd, DialogLine>, mut commands: Commands) {
debug!("Adding dialog line"); debug!("Adding dialog line");
} }
fn dialog_box_visibility(
state: Res<State<DialogState>>,
mut dialog_box: Single<&mut Visibility, With<DialogBox>>,
) {
**dialog_box = if *state.get() == DialogState::None {
Visibility::Hidden
} else {
Visibility::Inherited
};
}

@ -0,0 +1,82 @@
use bevy::{
asset::{AssetLoader, LoadContext, io::Reader},
prelude::*,
reflect::TypePath,
};
use serde::Deserialize;
use thiserror::Error;
#[derive(Asset, TypePath, Debug, Deserialize, Default, Clone)]
pub(crate) struct Monologue {
value: Vec<Vec<String>>,
}
impl Monologue {
pub fn get(&self, idx: usize) -> Option<&Vec<String>> {
self.value.get(idx)
}
}
#[derive(Default)]
struct MonologueLoader;
#[derive(Debug, Error)]
enum MonologueLoaderError {
#[error("Could not load asset: {0}")]
Io(#[from] std::io::Error),
#[error("Could not parse utf8")]
Utf8(#[from] std::string::FromUtf8Error),
}
impl AssetLoader for MonologueLoader {
type Asset = Monologue;
type Settings = ();
type Error = MonologueLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes: Vec<u8> = Vec::new();
reader.read_to_end(&mut bytes).await?;
let raw_string = String::from_utf8(bytes)?;
let value: Vec<Vec<String>> = raw_string
// First split on the '---' separators between batches
.split_terminator("---")
.map(|batch| {
batch
// Then split batches into newline-separated groups of text
.split_terminator("\n\n")
.filter_map(|line| {
// Filter out comments, empty lines, and extraneous newlines
(!line.starts_with("#") && !line.is_empty() && line != "\n")
// Trim the resulting dialog option
.then_some(line.trim().into())
})
.collect()
})
.filter(|sub: &Vec<String>| !sub.is_empty())
.collect();
info!("Monologue: {:#?}", value);
let thing = Monologue { value };
Ok(thing)
}
fn extensions(&self) -> &[&str] {
&["mono"]
}
}
pub struct MonologueAssetsPlugin;
impl Plugin for MonologueAssetsPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<Monologue>()
.init_asset_loader::<MonologueLoader>();
}
}
Loading…
Cancel
Save