bevy_talks/
lib.rs

1//! `bevy_talks` is a Bevy plugin that provides the basics to build and handle dialogues in games.
2
3use aery::{prelude::*, tuple_traits::RelationEntries};
4use bevy::prelude::*;
5
6use prelude::*;
7use ron_loader::loader::TalksLoader;
8use traverse::{choice_handler, next_handler, set_has_started};
9
10pub mod actors;
11pub mod builder;
12pub mod errors;
13pub mod events;
14pub mod prelude;
15pub mod ron_loader;
16pub mod talk;
17pub mod talk_asset;
18mod traverse;
19
20/// The plugin that provides the basics to build and handle dialogues in games.
21///
22/// # Note
23/// If you are using [Aery](https://crates.io/crates/aery), add it to the App before this plugin, or just add this plugin.
24/// This plugin will add Aery if it's not in the app, since it is a unique plugin, having multiple will panic.
25pub struct TalksPlugin;
26
27impl Plugin for TalksPlugin {
28    fn build(&self, app: &mut App) {
29        if !app.is_plugin_added::<Aery>() {
30            app.add_plugins(Aery);
31        }
32
33        app.add_plugins(TalksEventsPlugin)
34            .register_asset_loader(TalksLoader)
35            .init_asset::<TalkData>()
36            .configure_sets(PreUpdate, TalksSet)
37            .add_systems(
38                PreUpdate,
39                (
40                    next_handler.pipe(error_logger),
41                    choice_handler.pipe(error_logger),
42                    refire_handler.pipe(error_logger),
43                    set_has_started.after(next_handler),
44                )
45                    .in_set(TalksSet),
46            );
47    }
48}
49
50/// The `SystemSet` for the `TalksPlugin`.
51#[derive(SystemSet, Debug, Default, Clone, PartialEq, Eq, Hash)]
52struct TalksSet;
53
54/// Logs errors from the other systems.
55fn error_logger(In(result): In<Result<(), NextActionError>>) {
56    if let Err(err) = result {
57        error!("Error: {err}");
58    }
59}
60
61/// Handles the `RefireNodeRequest` events. It will emit the events in the current node.
62fn refire_handler(
63    mut cmd: Commands,
64    mut reqs: EventReader<RefireNodeRequest>,
65    current_nodes: Query<(Entity, &Parent), With<CurrentNode>>,
66    start: Query<Entity, With<StartNode>>,
67    end: Query<Entity, With<EndNode>>,
68    all_actors: Query<&Actor>,
69    performers: Query<Relations<PerformedBy>>,
70    emitters: Query<&dyn NodeEventEmitter>,
71    type_registry: Res<AppTypeRegistry>,
72    mut start_ev_writer: EventWriter<StartEvent>,
73    mut end_ev_writer: EventWriter<EndEvent>,
74) -> Result<(), NextActionError> {
75    if let Some(event) = reqs.read().next() {
76        for (current_node, talk_parent) in &current_nodes {
77            let this_talk = talk_parent.get();
78            // if this is the talk we want to advance
79            if this_talk == event.talk {
80                // send start event if we are at the start node
81                maybe_emit_start_event(&start, current_node, &mut start_ev_writer, event.talk);
82
83                // send end event if current node is an end node
84                maybe_emit_end_event(&end, current_node, &mut end_ev_writer, event.talk);
85
86                // grab the actors in the next node
87                let actors_in_node = retrieve_actors(&performers, current_node, &all_actors);
88
89                // emit the events in current node
90                emit_events(
91                    &mut cmd,
92                    &emitters,
93                    current_node,
94                    &type_registry,
95                    actors_in_node,
96                );
97                return Ok(());
98            }
99        }
100        return Err(NextActionError::NoTalk);
101    }
102    Ok(())
103}
104
105/// Emits the start event if the current node is a start node.
106#[inline]
107pub(crate) fn maybe_emit_start_event(
108    start: &Query<Entity, With<StartNode>>,
109    current_node: Entity,
110    start_ev_writer: &mut EventWriter<StartEvent>,
111    requested_talk: Entity,
112) {
113    if start.get(current_node).is_ok() {
114        start_ev_writer.send(StartEvent(requested_talk));
115    }
116}
117
118/// Emit the end event if the current node is an end node.
119#[inline]
120pub(crate) fn maybe_emit_end_event(
121    end: &Query<Entity, With<EndNode>>,
122    next_node: Entity,
123    end_ev_writer: &mut EventWriter<EndEvent>,
124    requested_talk: Entity,
125) {
126    if end.get(next_node).is_ok() {
127        end_ev_writer.send(EndEvent(requested_talk));
128    }
129}
130
131/// Retrieves the actors connected to the given node.
132#[inline]
133pub(crate) fn retrieve_actors(
134    performers: &Query<Relations<PerformedBy>>,
135    next_node: Entity,
136    all_actors: &Query<&Actor>,
137) -> Vec<Actor> {
138    let mut actors_in_node = Vec::<Actor>::new();
139    if let Ok(actor_edges) = &performers.get(next_node) {
140        for actor in actor_edges.targets(PerformedBy) {
141            actors_in_node.push(all_actors.get(*actor).expect("Actor").clone());
142        }
143    }
144    actors_in_node
145}
146
147/// Iterates over the `NodeEventEmitter` in the current node and emits the events.
148#[inline]
149pub(crate) fn emit_events(
150    cmd: &mut Commands,
151    emitters: &Query<&dyn NodeEventEmitter>,
152    next_node: Entity,
153    type_registry: &Res<AppTypeRegistry>,
154    actors_in_node: Vec<Actor>,
155) {
156    if let Ok(emitters) = emitters.get(next_node) {
157        let type_registry = type_registry.read();
158
159        for emitter in &emitters {
160            let emitted_event = emitter.make(&actors_in_node);
161
162            let event_type_id = emitted_event.type_id();
163            // The #[reflect] attribute we put on our event trait generated a new `ReflectEvent` struct
164            // that we can use as the event type.
165            let reflect_event = type_registry
166                .get_type_data::<ReflectEvent>(event_type_id)
167                .expect("Event not registered for event type")
168                .clone();
169
170            cmd.add(move |world: &mut World| {
171                reflect_event.send(&*emitted_event, world);
172            });
173        }
174    }
175}
176#[cfg(test)]
177mod tests {
178    use bevy::ecs::{
179        query::{ROQueryItem, WorldQuery},
180        system::Command,
181    };
182
183    use indexmap::indexmap;
184
185    use super::*;
186
187    /// A minimal Bevy app with the Talks plugin.
188    pub fn talks_minimal_app() -> App {
189        let mut app = App::new();
190        app.add_plugins((AssetPlugin::default(), TalksPlugin));
191        app
192    }
193
194    #[inline]
195    #[track_caller]
196    pub fn get_comp<C: Component>(e: Entity, world: &mut World) -> &C {
197        world.entity(e).get::<C>().expect("Component")
198    }
199
200    #[inline]
201    #[track_caller]
202    pub fn count<Q: WorldQuery>(world: &mut World) -> usize {
203        world.query::<Q>().iter(&world).count()
204    }
205
206    #[inline]
207    #[track_caller]
208    pub fn single<Q: WorldQuery>(world: &mut World) -> ROQueryItem<Q> {
209        world.query::<Q>().single(world)
210    }
211
212    /// Setup a talk with the given data, and send the first `NextActionRequest` event.
213    /// Returns the app for further testing.
214    #[track_caller]
215    pub fn setup_and_next(talk_data: &TalkData) -> App {
216        let mut app = talks_minimal_app();
217        let builder = TalkBuilder::default().fill_with_talk_data(talk_data);
218        BuildTalkCommand::new(app.world.spawn(Talk::default()).id(), builder).apply(&mut app.world);
219        let (talk_ent, _) = single::<(Entity, With<Talk>)>(&mut app.world);
220        let (edges, _) = single::<(Relations<FollowedBy>, With<CurrentNode>)>(&mut app.world);
221
222        assert_eq!(edges.targets(FollowedBy).len(), 1);
223        let start_following_ent = edges.targets(FollowedBy)[0];
224
225        app.world.send_event(NextNodeRequest::new(talk_ent));
226        app.update();
227
228        let (next_e, _) = single::<(Entity, With<CurrentNode>)>(&mut app.world);
229        assert_eq!(next_e, start_following_ent);
230        app
231    }
232
233    #[test]
234    fn refire_request_sends_events() {
235        let script = indexmap! {
236            0 => Action { text: "Hello".to_string(), actors: vec!["actor_1".to_string()], ..default() }, // this will be a text node
237        };
238        let mut app = setup_and_next(&TalkData::new(script, vec![Actor::new("actor_1", "Actor")]));
239        let evs = app.world.resource::<Events<TextNodeEvent>>();
240        assert_eq!(evs.get_reader().read(evs).len(), 1);
241
242        let (talk_ent, _) = single::<(Entity, With<Talk>)>(&mut app.world);
243        app.world.send_event(RefireNodeRequest::new(talk_ent));
244        app.update();
245
246        let evs = app.world.resource::<Events<TextNodeEvent>>();
247        assert_eq!(evs.get_reader().read(evs).len(), 2);
248    }
249}