use crate::default_impl::{MemoryVariableStorage, StringsFileTextProvider};
use crate::line_provider::SharedTextProvider;
use crate::prelude::*;
use bevy::prelude::*;
use bevy::utils::HashMap;
use rand::{rngs::SmallRng, Rng, SeedableRng};
use std::any::{Any, TypeId};
use std::fmt;
use std::fmt::{Debug, Formatter};
pub(crate) fn dialogue_runner_builder_plugin(_app: &mut App) {}
pub struct DialogueRunnerBuilder {
variable_storage: Box<dyn VariableStorage>,
text_provider: SharedTextProvider,
asset_providers: HashMap<TypeId, Box<dyn AssetProvider>>,
library: YarnLibrary,
commands: YarnCommands,
compilation: Compilation,
localizations: Option<Localizations>,
asset_server: AssetServer,
}
impl Debug for DialogueRunnerBuilder {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("DialogueRunnerBuilder")
.field("variable_storage", &self.variable_storage)
.field("text_provider", &self.text_provider)
.field("asset_providers", &self.asset_providers)
.field("library", &self.library)
.field("commands", &self.commands)
.field("compilation", &self.compilation)
.field("localizations", &self.localizations)
.field("asset_server", &())
.finish()
}
}
impl DialogueRunnerBuilder {
#[must_use]
pub(crate) fn from_yarn_project(yarn_project: &YarnProject) -> Self {
Self {
variable_storage: Box::new(MemoryVariableStorage::new()),
text_provider: SharedTextProvider::new(StringsFileTextProvider::from_yarn_project(
yarn_project,
)),
asset_providers: HashMap::new(),
library: create_extended_standard_library(),
commands: YarnCommands::builtin_commands(),
compilation: yarn_project.compilation().clone(),
localizations: yarn_project.localizations().cloned(),
asset_server: yarn_project.asset_server.clone(),
}
}
#[must_use]
pub fn with_variable_storage(mut self, storage: Box<dyn VariableStorage>) -> Self {
self.variable_storage = storage;
self
}
#[must_use]
pub fn with_text_provider(mut self, provider: impl TextProvider + 'static) -> Self {
self.text_provider.replace(provider);
self
}
#[must_use]
pub fn add_asset_provider(mut self, provider: impl AssetProvider + 'static) -> Self {
self.asset_providers
.insert(provider.type_id(), Box::new(provider));
self
}
pub fn build(self) -> DialogueRunner {
self.try_build().unwrap_or_else(|error| {
panic!("Failed to build DialogueRunner: {error}");
})
}
pub fn try_build(mut self) -> Result<DialogueRunner> {
let text_provider = Box::new(self.text_provider);
let mut dialogue = Dialogue::new(self.variable_storage, text_provider.clone());
dialogue
.set_line_hints_enabled(true)
.library_mut()
.extend(self.library);
dialogue.add_program(self.compilation.program.unwrap());
for asset_provider in self.asset_providers.values_mut() {
if let Some(ref localizations) = self.localizations {
asset_provider.set_localizations(localizations.clone());
}
asset_provider.set_asset_server(self.asset_server.clone());
}
let popped_line_hints = dialogue.pop_line_hints();
let base_language = self
.localizations
.as_ref()
.map(|l| &l.base_localization.language)
.cloned();
let mut dialogue_runner = DialogueRunner {
dialogue,
text_provider,
popped_line_hints,
run_selected_options_as_lines: false,
asset_providers: self.asset_providers,
commands: self.commands,
is_running: default(),
command_tasks: default(),
will_continue_in_next_update: default(),
last_selected_option: default(),
just_started: default(),
unsent_events: default(),
localizations: self.localizations,
};
if let Some(base_language) = base_language {
dialogue_runner.set_language(base_language);
}
Ok(dialogue_runner)
}
}
fn create_extended_standard_library() -> YarnLibrary {
let mut library = YarnLibrary::standard_library();
library
.add_function("random", || SmallRng::from_entropy().gen_range(0.0..1.0))
.add_function("random_range", |min: f32, max: f32| {
if let Some(min) = min.as_int() {
if let Some(max_inclusive) = max.as_int() {
return SmallRng::from_entropy().gen_range(min..=max_inclusive) as f32;
}
}
SmallRng::from_entropy().gen_range(min..max)
})
.add_function("dice", |sides: u32| {
if sides == 0 {
return 1;
}
SmallRng::from_entropy().gen_range(1..=sides)
})
.add_function("round", |num: f32| num.round() as i32)
.add_function("round_places", |num: f32, places: u32| {
num.round_places(places)
})
.add_function("floor", |num: f32| num.floor() as i32)
.add_function("ceil", |num: f32| num.ceil() as i32)
.add_function("inc", |num: f32| {
if let Some(num) = num.as_int() {
num + 1
} else {
num.ceil() as i32
}
})
.add_function("dec", |num: f32| {
if let Some(num) = num.as_int() {
num - 1
} else {
num.floor() as i32
}
})
.add_function("decimal", |num: f32| num.fract())
.add_function("int", |num: f32| num.trunc() as i32);
library
}
trait FloatExt: Copy {
fn as_int(self) -> Option<i32>;
fn round_places(self, places: u32) -> Self;
}
impl FloatExt for f32 {
fn as_int(self) -> Option<i32> {
(self.fract() <= f32::EPSILON).then_some(self as i32)
}
fn round_places(self, places: u32) -> Self {
let factor = 10_u32.pow(places) as f32;
(self * factor).round() / factor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_places() {
for (num, places, expected) in [
(1.0, 0, 1.0),
(1.2, 1, 1.2),
(0.4, 0, 0.0),
(43.132, 0, 43.0),
(1.1, 2, 1.1),
(123.123, 3, 123.123),
(-10.3, 1, -10.3),
(-11.99, 1, -12.0),
] {
assert_eq!(expected, num.round_places(places));
}
}
}