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 = [
"bevy",
"bevy_rapier3d",
"serde",
"thiserror 2.0.12",
]
[[package]]

@ -3,6 +3,13 @@ name = "games"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2.0.12"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
[dependencies.bevy_rapier3d]
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::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 mono::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin)
.add_plugins(MonologueAssetsPlugin)
.add_event::<DialogEvent>()
.init_state::<DialogState>()
.insert_resource(ClearColor(WHITE.into()))
.add_systems(
Startup,
@ -14,9 +21,19 @@ fn main() {
.add_systems(
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>),
auto_scroll.run_if(any_component_added::<DialogOption>),
dialog_box_visibility
.run_if(state_changed::<DialogState>)
),
)
.add_observer(add_dialog_option)
@ -28,6 +45,9 @@ fn main() {
#[derive(Component)]
struct Tree;
#[derive(Component, PartialEq)]
struct TreeMonologue(Handle<Monologue>);
/// Initialize the trees, currently placeholders
/// Trees are 2d cards in a 3d world for flexibility
/// Might move fully 2d if the art style allows it
@ -49,10 +69,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend,
..default()
});
let monologue: Handle<Monologue> = server.load("trees/red.mono");
let tree_transform_red =
Transform::from_xyz(-15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_red),
tree_transform_red,
@ -68,10 +90,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend,
..default()
});
let monologue: Handle<Monologue> = server.load("trees/green.mono");
let tree_transform_green =
Transform::from_xyz(15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_green),
tree_transform_green,
@ -87,10 +111,12 @@ fn init_trees(
alpha_mode: AlphaMode::Blend,
..default()
});
let monologue: Handle<Monologue> = server.load("trees/blue.mono");
let tree_transform_blue =
Transform::from_xyz(0.0, 0.0, -15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
TreeMonologue(monologue),
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_blue),
tree_transform_blue,
@ -109,6 +135,7 @@ fn init_ui(mut commands: Commands) {
.spawn((
DialogBox,
BackgroundColor(BLACK.with_alpha(0.9).into()),
DialogState::Ongoing,
Node {
align_self: AlignSelf::End,
justify_self: JustifySelf::Center,
@ -195,36 +222,145 @@ struct DialogOption;
#[derive(Component)]
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
fn dialog_engine(
mut commands: Commands,
// React to dialog events
mut events: EventReader<DialogEvent>,
// Reference to 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>,
// 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![
vec!["A", "B", "C"],
vec!["E", "F", "G"],
vec!["H", "I"],
vec!["J", "K"],
vec!["L"],
];
debug_assert!(
!events.is_empty(),
"Dialog engine is triggered by Dialog Events"
);
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| {
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| {
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:
fn choose_dialog_option(
trigger: Trigger<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
mut commands: Commands,
texts: Query<&Text>,
options: Query<Entity, With<DialogOption>>,
@ -243,6 +379,9 @@ fn choose_dialog_option(
parent.spawn((t.clone(), DialogLine));
});
}
// trigger the next dialog line
dialog_events.write(DialogEvent::NextBatch);
}
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()) {
*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>) {
if let Ok(mut tc) = query.get_mut(trigger.target()) {
fn hover_dialog_option_out(
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());
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) {
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