use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Reflect)]
#[serde(rename_all = "lowercase")]
pub enum DialogueNodeType {
#[default]
Text,
Choice,
Condition,
Action,
End,
}
impl DialogueNodeType {
pub fn display_name(&self) -> &'static str {
match self {
DialogueNodeType::Text => "Text",
DialogueNodeType::Choice => "Choice",
DialogueNodeType::Condition => "Condition",
DialogueNodeType::Action => "Action",
DialogueNodeType::End => "End",
}
}
pub fn all() -> &'static [DialogueNodeType] {
&[
DialogueNodeType::Text,
DialogueNodeType::Choice,
DialogueNodeType::Condition,
DialogueNodeType::Action,
DialogueNodeType::End,
]
}
pub fn color(&self) -> (u8, u8, u8) {
match self {
DialogueNodeType::Text => (100, 149, 237), DialogueNodeType::Choice => (255, 165, 0), DialogueNodeType::Condition => (147, 112, 219), DialogueNodeType::Action => (50, 205, 50), DialogueNodeType::End => (220, 20, 60), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Reflect)]
pub struct DialogueChoice {
pub text: String,
pub next_node: Option<String>,
pub condition: Option<String>,
}
impl DialogueChoice {
pub fn new(text: impl Into<String>, next_node: impl Into<String>) -> Self {
Self {
text: text.into(),
next_node: Some(next_node.into()),
condition: None,
}
}
pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
self.condition = Some(condition.into());
self
}
}
fn default_position() -> (f32, f32) {
(0.0, 0.0)
}
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct DialogueNode {
pub id: String,
#[serde(default)]
pub node_type: DialogueNodeType,
#[serde(default)]
pub speaker: String,
#[serde(default)]
pub text: String,
#[serde(default)]
pub choices: Vec<DialogueChoice>,
pub next_node: Option<String>,
pub condition: Option<String>,
pub action: Option<String>,
#[serde(default = "default_position")]
pub position: (f32, f32),
}
impl Default for DialogueNode {
fn default() -> Self {
Self {
id: Uuid::new_v4().to_string(),
node_type: DialogueNodeType::Text,
speaker: String::new(),
text: String::new(),
choices: Vec::new(),
next_node: None,
condition: None,
action: None,
position: (0.0, 0.0),
}
}
}
impl DialogueNode {
pub fn new_text(speaker: impl Into<String>, text: impl Into<String>) -> Self {
Self {
speaker: speaker.into(),
text: text.into(),
..Default::default()
}
}
pub fn new_choice(speaker: impl Into<String>, prompt: impl Into<String>) -> Self {
Self {
node_type: DialogueNodeType::Choice,
speaker: speaker.into(),
text: prompt.into(),
..Default::default()
}
}
pub fn new_end() -> Self {
Self {
node_type: DialogueNodeType::End,
..Default::default()
}
}
pub fn new_condition(condition: impl Into<String>) -> Self {
Self {
node_type: DialogueNodeType::Condition,
condition: Some(condition.into()),
..Default::default()
}
}
pub fn new_action(action: impl Into<String>) -> Self {
Self {
node_type: DialogueNodeType::Action,
action: Some(action.into()),
..Default::default()
}
}
pub fn with_next(mut self, next_node: impl Into<String>) -> Self {
self.next_node = Some(next_node.into());
self
}
pub fn with_choice(mut self, choice: DialogueChoice) -> Self {
self.choices.push(choice);
self
}
pub fn with_position(mut self, x: f32, y: f32) -> Self {
self.position = (x, y);
self
}
}
fn default_dialogue_id() -> String {
Uuid::new_v4().to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Asset, Reflect)]
pub struct DialogueTree {
#[serde(default = "default_dialogue_id")]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub start_node: String,
#[serde(default)]
#[reflect(ignore)]
pub nodes: HashMap<String, DialogueNode>,
}
impl DialogueTree {
pub fn new(name: impl Into<String>) -> Self {
let start_id = Uuid::new_v4().to_string();
let mut nodes = HashMap::new();
nodes.insert(
start_id.clone(),
DialogueNode {
id: start_id.clone(),
node_type: DialogueNodeType::Text,
speaker: String::new(),
text: "Hello!".to_string(),
choices: Vec::new(),
next_node: None,
condition: None,
action: None,
position: (100.0, 100.0),
},
);
Self {
id: Uuid::new_v4().to_string(),
name: name.into(),
start_node: start_id,
nodes,
}
}
pub fn empty(name: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: name.into(),
start_node: String::new(),
nodes: HashMap::new(),
}
}
pub fn add_node(&mut self, node: DialogueNode) -> String {
let id = node.id.clone();
self.nodes.insert(id.clone(), node);
id
}
pub fn remove_node(&mut self, id: &str) {
self.nodes.remove(id);
for node in self.nodes.values_mut() {
if node.next_node.as_deref() == Some(id) {
node.next_node = None;
}
node.choices.retain(|c| c.next_node.as_deref() != Some(id));
}
if self.start_node == id {
self.start_node = String::new();
}
}
pub fn get_node(&self, id: &str) -> Option<&DialogueNode> {
self.nodes.get(id)
}
pub fn get_node_mut(&mut self, id: &str) -> Option<&mut DialogueNode> {
self.nodes.get_mut(id)
}
pub fn get_start_node(&self) -> Option<&DialogueNode> {
self.nodes.get(&self.start_node)
}
pub fn set_start_node(&mut self, id: impl Into<String>) {
self.start_node = id.into();
}
pub fn node_ids(&self) -> impl Iterator<Item = &str> {
self.nodes.keys().map(|s| s.as_str())
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.start_node.is_empty() {
errors.push("No start node defined".to_string());
} else if !self.nodes.contains_key(&self.start_node) {
errors.push(format!("Start node '{}' not found", self.start_node));
}
for (id, node) in &self.nodes {
if let Some(next) = &node.next_node {
if !self.nodes.contains_key(next) {
errors.push(format!(
"Node '{}' references non-existent node '{}'",
id, next
));
}
}
for choice in &node.choices {
if let Some(next) = &choice.next_node {
if !self.nodes.contains_key(next) {
errors.push(format!(
"Choice '{}' in node '{}' references non-existent node '{}'",
choice.text, id, next
));
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[derive(Component, Debug, Clone, Default, Reflect)]
pub struct DialogueHandle(#[reflect(ignore)] pub Handle<DialogueTree>);
impl DialogueHandle {
pub fn new(handle: Handle<DialogueTree>) -> Self {
Self(handle)
}
}
#[derive(Message, Debug, Clone)]
pub struct StartDialogueEvent {
pub speaker_entity: Entity,
pub dialogue: Handle<DialogueTree>,
}
#[derive(Message, Debug, Clone)]
pub struct DialogueChoiceEvent {
pub choice_index: usize,
}
#[derive(Message, Debug, Clone)]
pub struct DialogueEndEvent {
pub speaker_entity: Entity,
}
#[derive(Resource, Debug, Clone, Default)]
pub struct DialogueRunner {
pub active: bool,
pub speaker_entity: Option<Entity>,
pub dialogue_handle: Option<Handle<DialogueTree>>,
pub current_node_id: Option<String>,
}
impl DialogueRunner {
pub fn start(&mut self, speaker: Entity, dialogue: Handle<DialogueTree>, start_node: String) {
self.active = true;
self.speaker_entity = Some(speaker);
self.dialogue_handle = Some(dialogue);
self.current_node_id = Some(start_node);
}
pub fn end(&mut self) {
self.active = false;
self.speaker_entity = None;
self.dialogue_handle = None;
self.current_node_id = None;
}
pub fn advance_to(&mut self, node_id: String) {
self.current_node_id = Some(node_id);
}
pub fn is_active(&self) -> bool {
self.active
}
}
pub struct DialoguePlugin;
impl Plugin for DialoguePlugin {
fn build(&self, app: &mut App) {
app.init_asset::<DialogueTree>()
.register_type::<DialogueNodeType>()
.register_type::<DialogueChoice>()
.register_type::<DialogueNode>()
.register_type::<DialogueTree>()
.register_type::<DialogueHandle>()
.init_resource::<DialogueRunner>()
.init_resource::<Messages<StartDialogueEvent>>()
.init_resource::<Messages<DialogueChoiceEvent>>()
.init_resource::<Messages<DialogueEndEvent>>()
.add_systems(Update, (handle_start_dialogue, handle_dialogue_choice));
}
}
fn handle_start_dialogue(
mut events: MessageReader<StartDialogueEvent>,
mut runner: ResMut<DialogueRunner>,
dialogues: Res<Assets<DialogueTree>>,
) {
for event in events.read() {
if let Some(tree) = dialogues.get(&event.dialogue) {
if !tree.start_node.is_empty() {
runner.start(
event.speaker_entity,
event.dialogue.clone(),
tree.start_node.clone(),
);
}
}
}
}
fn handle_dialogue_choice(
mut choice_events: MessageReader<DialogueChoiceEvent>,
mut end_events: MessageWriter<DialogueEndEvent>,
mut runner: ResMut<DialogueRunner>,
dialogues: Res<Assets<DialogueTree>>,
) {
for event in choice_events.read() {
if !runner.active {
continue;
}
let Some(handle) = &runner.dialogue_handle else {
continue;
};
let Some(tree) = dialogues.get(handle) else {
continue;
};
let Some(current_id) = &runner.current_node_id else {
continue;
};
let Some(node) = tree.get_node(current_id) else {
continue;
};
match node.node_type {
DialogueNodeType::Text => {
if let Some(next) = &node.next_node {
runner.advance_to(next.clone());
} else {
let speaker = runner.speaker_entity;
runner.end();
if let Some(entity) = speaker {
end_events.write(DialogueEndEvent {
speaker_entity: entity,
});
}
}
}
DialogueNodeType::Choice => {
if let Some(choice) = node.choices.get(event.choice_index) {
if let Some(next) = &choice.next_node {
runner.advance_to(next.clone());
} else {
let speaker = runner.speaker_entity;
runner.end();
if let Some(entity) = speaker {
end_events.write(DialogueEndEvent {
speaker_entity: entity,
});
}
}
}
}
DialogueNodeType::End => {
let speaker = runner.speaker_entity;
runner.end();
if let Some(entity) = speaker {
end_events.write(DialogueEndEvent {
speaker_entity: entity,
});
}
}
_ => {
if let Some(next) = &node.next_node {
runner.advance_to(next.clone());
}
}
}
}
}