use crate::{
builder::{BuildNodeId, TalkBuilder},
prelude::{Actor, ActorSlug},
};
use bevy::{prelude::*, reflect::TypePath, utils::HashMap};
use indexmap::IndexMap;
pub(crate) type ActionId = usize;
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, serde::Deserialize)]
pub enum NodeKind {
Start,
#[default]
Talk,
Choice,
Join,
Leave,
}
#[derive(Debug, Default, Clone, Eq, Hash, PartialEq)]
pub(crate) struct Action {
pub(crate) kind: NodeKind,
pub(crate) actors: Vec<ActorSlug>,
pub(crate) choices: Vec<ChoiceData>,
pub(crate) text: String,
pub(crate) next: Option<ActionId>,
}
#[derive(Default, Debug, Clone, Eq, Hash, PartialEq)]
pub(crate) struct ChoiceData {
pub(crate) text: String,
pub(crate) next: ActionId,
}
#[derive(Asset, Debug, Default, Clone, TypePath)]
pub struct TalkData {
pub(crate) script: IndexMap<ActionId, Action>,
pub(crate) actors: Vec<Actor>,
}
impl TalkData {
#[allow(dead_code)]
pub(crate) fn new(script: IndexMap<ActionId, Action>, actors: Vec<Actor>) -> Self {
Self { script, actors }
}
pub(crate) fn fill_builder(&self, mut builder: TalkBuilder) -> TalkBuilder {
builder = builder.add_actors(self.actors.clone());
if self.script.is_empty() {
return builder;
}
let mut visited = HashMap::with_capacity(self.script.len());
let start_id = self.script.keys().next().unwrap();
prepare_builder(*start_id, &self.script, builder, &mut visited)
}
}
fn prepare_builder(
starting_action_id: usize,
actions: &IndexMap<ActionId, Action>,
mut builder: TalkBuilder,
visited: &mut HashMap<usize, BuildNodeId>,
) -> TalkBuilder {
let mut the_action = &actions[&starting_action_id];
let mut the_id = starting_action_id;
let mut done = false;
while !done {
match the_action.kind {
NodeKind::Start => (), NodeKind::Talk => {
builder = match the_action.actors.len() {
0 => builder.say(&the_action.text),
1 => builder.actor_say(&the_action.actors[0], &the_action.text),
2.. => builder.actors_say(&the_action.actors, &the_action.text),
}
}
NodeKind::Choice => {
let mut choice_vec = Vec::with_capacity(the_action.choices.len());
for c in the_action.choices.iter() {
let text = c.text.clone();
let next = c.next;
let mut inner_builder = TalkBuilder::default();
if visited.get(&next).is_some() {
inner_builder = inner_builder.connect_to(visited[&next].clone());
} else {
inner_builder = prepare_builder(next, actions, inner_builder, visited);
}
choice_vec.push((text, inner_builder));
}
builder = builder.choose(choice_vec);
visited.insert(the_id, builder.last_node_id());
break; }
NodeKind::Join => builder = builder.join(&the_action.actors),
NodeKind::Leave => builder = builder.leave(&the_action.actors),
}
visited.insert(the_id, builder.last_node_id());
if let Some(next) = the_action.next {
if visited.get(&next).is_some() {
builder = builder.connect_to(visited[&next].clone());
done = true; }
the_action = &actions[&next];
the_id = next;
} else {
done = true; }
}
builder
}
#[cfg(test)]
mod tests {
use crate::{
prelude::*,
tests::{count, talks_minimal_app},
FollowedBy,
};
use aery::{edges::Root, operations::utils::Relations, tuple_traits::RelationEntries};
use bevy::{ecs::system::Command, prelude::*, utils::hashbrown::HashMap};
use indexmap::{indexmap, IndexMap};
use rstest::rstest;
fn build(talk_data: TalkData) -> World {
let mut app = talks_minimal_app();
BuildTalkCommand::new(
app.world.spawn_empty().id(),
talk_data.fill_builder(TalkBuilder::default()),
)
.apply(&mut app.world);
app.world
}
#[rstest]
#[case(1)]
#[case(2)]
#[case(10)]
#[case(200)]
fn linear_talk_nodes(#[case] nodes: usize) {
let mut script = IndexMap::with_capacity(nodes);
let mut map = HashMap::with_capacity(nodes);
for index in 0..nodes {
script.insert(
index,
Action {
text: "Hello".to_string(),
next: if nodes > 1 && index < nodes - 1 {
Some(index + 1)
} else {
None
},
..default()
},
);
let target = if nodes > 1 && index < nodes - 1 {
Some((index + 3) as u32)
} else {
None
};
map.insert(index + 2, (target, "Hello"));
}
let mut world = build(TalkData::new(script, vec![]));
assert_eq!(count::<&TextNode>(&mut world), nodes);
assert_on_text_nodes(world, map);
}
#[test]
fn talk_nodes_with_loop() {
let script = indexmap! {
1 => Action { text: "1".to_string(), next: Some(10), ..default() },
2 => Action { text: "2".to_string(), next: Some(10), ..default() },
10 => Action { text: "10".to_string(), next: Some(2), ..default() },
};
let mut world = build(TalkData::new(script, vec![]));
assert_eq!(count::<&TextNode>(&mut world), 3);
let mut map = HashMap::new();
map.insert(2, (Some(3), "1"));
map.insert(3, (Some(4), "10"));
map.insert(4, (Some(3), "2"));
assert_on_text_nodes(world, map);
}
#[test]
fn choice_pointing_to_talks() {
let script = indexmap! {
0 =>
Action {
choices: vec![
ChoiceData { text: "Choice 1".to_string(), next: 1, },
ChoiceData { text: "Choice 2".to_string(), next: 2, },
],
kind: NodeKind::Choice,
..default()
},
1 => Action { text: "Hello".to_string(), next: Some(2), ..default() },
2 => Action { text: "Fin".to_string(), ..default() },
};
let mut world = build(TalkData::new(script, vec![]));
assert_eq!(count::<&TextNode>(&mut world), 2);
assert_eq!(count::<&ChoiceNode>(&mut world), 1);
assert_eq!(count::<Root<FollowedBy>>(&mut world), 1);
let mut map: HashMap<usize, (Vec<u32>, Vec<&str>)> = HashMap::new();
map.insert(2, (vec![3, 4], vec!["Choice 1", "Choice 2"]));
assert_on_choice_nodes(&mut world, map);
}
#[test]
fn connect_back_from_branch_book_example() {
let script = indexmap! {
0 => Action { text: "First Text".to_string(), next: Some(1), ..default() },
1 => Action { text: "Second Text".to_string(), next: Some(2), ..default() },
2 =>
Action {
choices: vec![
ChoiceData { text: "Choice 1".to_string(), next: 3, },
ChoiceData { text: "Choice 2".to_string(), next: 4, },
],
kind: NodeKind::Choice,
..default()
},
3 => Action { text: "Third Text (End)".to_string(), ..default() },
4 => Action { text: "Fourth Text".to_string(), next: Some(0), ..default() },
};
let mut world = build(TalkData::new(script, vec![]));
assert_eq!(count::<&TextNode>(&mut world), 4);
assert_eq!(count::<&ChoiceNode>(&mut world), 1);
assert_eq!(count::<Root<FollowedBy>>(&mut world), 1);
let mut choice_map = HashMap::new();
choice_map.insert(4, (vec![5, 6], vec!["Choice 1", "Choice 2"]));
assert_on_choice_nodes(&mut world, choice_map);
let mut talk_map = HashMap::new();
talk_map.insert(2, (Some(3), "First Text"));
talk_map.insert(3, (Some(4), "Second Text"));
talk_map.insert(5, (None, "Third Text (End)"));
talk_map.insert(6, (Some(2), "Fourth Text"));
assert_on_text_nodes(world, talk_map);
}
#[test]
fn connect_forward_from_book_example() {
let script = indexmap! {
0 => Action {
choices: vec![
ChoiceData { text: "First Choice 1".to_string(), next: 1, },
ChoiceData { text: "First Choice 2".to_string(), next: 2, },
],
kind: NodeKind::Choice,
..default()
},
1 => Action { text: "First Text".to_string(), next: Some(3), ..default() },
2 => Action { text: "Last Text".to_string(), next: None, ..default() },
3 =>
Action {
choices: vec![
ChoiceData { text: "Second Choice 1".to_string(), next: 2, },
ChoiceData { text: "Second Choice 2".to_string(), next: 4, },
],
kind: NodeKind::Choice,
..default()
},
4 => Action { text: "Second Text".to_string(), next: Some(2), ..default() },
};
let mut world = build(TalkData::new(script, vec![]));
assert_eq!(count::<&TextNode>(&mut world), 3);
assert_eq!(count::<&ChoiceNode>(&mut world), 2);
assert_eq!(count::<Root<FollowedBy>>(&mut world), 1);
let mut choice_map = HashMap::new();
choice_map.insert(2, (vec![3, 5], vec!["First Choice 1", "First Choice 2"]));
choice_map.insert(4, (vec![5, 6], vec!["Second Choice 1", "Second Choice 2"]));
assert_on_choice_nodes(&mut world, choice_map);
let mut talk_map = HashMap::new();
talk_map.insert(3, (Some(4), "First Text"));
talk_map.insert(5, (None, "Last Text"));
talk_map.insert(6, (Some(5), "Second Text"));
assert_on_text_nodes(world, talk_map);
}
#[rstest]
#[case(1)]
#[case(2)]
#[case(10)]
#[case(200)]
fn linear_talk_nodes_with_actors(#[case] nodes: usize) {
let actors = vec![
Actor::new("actor1", "Actor 1"),
Actor::new("actor2", "Actor 2"),
Actor::new("actor3", "Actor 3"),
];
let mut script = IndexMap::with_capacity(nodes);
let mut map = HashMap::with_capacity(nodes);
for index in 0..nodes {
script.insert(
index,
Action {
text: "Hello".to_string(),
next: if nodes > 1 && index < nodes - 1 {
Some(index + 1)
} else {
None
},
actors: vec![actors[index % 3].slug.clone()],
..default()
},
);
let target = if nodes > 1 && index < nodes - 1 {
Some((index + 3) as u32)
} else {
None
};
map.insert(index + 2, (target, "Hello"));
}
let mut world = build(TalkData::new(script, actors));
assert_eq!(count::<&TextNode>(&mut world), nodes);
assert_eq!(count::<&Actor>(&mut world), 3);
assert_on_text_nodes(world, map);
}
#[track_caller]
fn assert_on_text_nodes(mut world: World, map: HashMap<usize, (Option<u32>, &str)>) {
for (e, t, edges) in world
.query::<(Entity, &TextNode, Relations<FollowedBy>)>()
.iter(&world)
{
let eid = e.index() as usize;
let expected_text = map[&eid].1;
let maybe_target = map[&eid].0;
let mut expected_count = 0;
if let Some(expected_target) = maybe_target {
assert_eq!(
edges.targets(FollowedBy).iter().next().unwrap().index(),
expected_target
);
expected_count = 1;
}
assert_eq!(edges.targets(FollowedBy).iter().count(), expected_count);
assert_eq!(t.0, expected_text);
}
}
#[track_caller]
fn assert_on_choice_nodes(world: &mut World, map: HashMap<usize, (Vec<u32>, Vec<&str>)>) {
for (e, t, edges) in world
.query::<(Entity, &ChoiceNode, Relations<FollowedBy>)>()
.iter(&world)
{
let eid = e.index() as usize;
let expected_texts = map[&eid].1.clone();
let expected_count = expected_texts.len();
for target in map[&eid].0.clone() {
assert!(edges
.targets(FollowedBy)
.iter()
.any(|e| e.index() == target));
}
assert_eq!(edges.targets(FollowedBy).iter().count(), expected_count);
for (i, c) in t.0.iter().enumerate() {
assert_eq!(c.text, expected_texts[i]);
}
}
}
}