#![allow(dead_code)] #![allow(clippy::type_complexity)] #![allow(clippy::too_many_arguments)] #![feature(trim_prefix_suffix)] mod mono; use bevy::{picking::hover::HoverMap, platform::hash::RandomState}; use games::*; use mono::*; use std::hash::BuildHasher; fn main() { App::new() .add_plugins(BaseGamePlugin { name: "trees".into(), }) .add_plugins(MonologueAssetsPlugin) .add_event::() .add_event::() .add_event::() .init_state::() .insert_resource(ClearColor(WHITE.into())) .add_systems( Startup, ( init_trees, init_ui, init_debug_ui, load_monologues, position_camera.after(setup_camera), ), ) .add_systems( Update, ( // Start a dialog if none are running start_dialog .run_if(in_state(DebuggingState::Off)) .run_if(in_state(DialogState::None)) .run_if(on_event::>), // Close the dialog box if it is idle (not choosing) end_dialog .run_if(in_state(DebuggingState::Off)) .run_if(in_state(DialogState::Idle)) .run_if(on_event::>), spawn_debug_buttons.run_if(on_event::>), handle_plant_tree.run_if(on_event::), assign_monologue_to_tree.run_if(on_event::), dialog_engine.run_if(on_event::), auto_scroll.run_if(any_component_added::), dialog_box_visibility.run_if(state_changed::), monologue_asset_tooltip .run_if(on_event::>.or(on_event::>)), scale_window.run_if(on_event::), hide_menu.run_if(any_component_changed::), clear_monologue.run_if(any_component_changed::), control_menu.run_if(on_event::>.or(on_event::>)), ), ) .add_observer(add_dialog_option) .add_observer(add_tree_monologue) .add_observer(remove_tree_monologue) .add_observer(show_monologue_list) .add_observer(hide_monologue_list) .run(); } /// Tree marker component #[derive(Component)] struct Tree; #[derive(Component, PartialEq, Clone)] struct TreeMonologue(Handle); /// 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 fn init_trees(mut ambient_light: ResMut) { // Global light ambient_light.brightness = 500.0; } /// Dialog box marker component #[derive(Component)] struct DialogBox; /// Initialize the UI which consists soley of a dialog box (for now?) fn init_ui(mut commands: Commands) { commands .spawn(( DialogBox, BackgroundColor(BLACK.with_alpha(0.9).into()), DialogState::Ongoing, Node { align_self: AlignSelf::Start, justify_self: JustifySelf::Center, width: Val::Percent(98.0), max_height: Val::Percent(50.0), align_items: AlignItems::Center, margin: UiRect::all(Val::Percent(1.0)), padding: UiRect::all(Val::Percent(1.0)), flex_direction: FlexDirection::Column, // Scroll on the Y axis overflow: Overflow::scroll_y(), ..default() }, )) .observe(scroll) .observe(hover_dialog_box_over) .observe(hover_dialog_box_out); } #[derive(Component)] struct MonologuesContainer; #[derive(Component)] struct MonologuesList; #[derive(Component)] struct MonologuePreview; /// Panel for selecting which monologue tree to spawn fn init_debug_ui(mut commands: Commands) { commands.spawn(( Name::new("Tree Planter"), Text::new("+Tree"), Node { top: Val::Px(0.0), left: Val::Px(0.0), min_width: Val::Px(25.0), min_height: Val::Px(25.0), ..default() }, BackgroundColor(RED.into()), DebuggingState::On, MonologuesContainer, Button, )).observe(spawn_tree); let monologue_button = commands .spawn(( Name::new("Monologue Assignment Menu"), Text::new("+Monologue"), Node { top: Val::Px(25.0), left: Val::Px(0.0), min_width: Val::Px(25.0), min_height: Val::Px(25.0), ..default() }, BackgroundColor(RED.into()), DebuggingState::On, MonologuesContainer, )) .id(); commands .spawn(( NavParent(monologue_button), NavState::default(), Name::new("Container"), Node { flex_direction: FlexDirection::Row, top: Val::Px(50.0), height: Val::Percent(90.0), width: Val::Percent(80.0), ..default() }, BackgroundColor(BLACK.into()), )).with_children(|parent| { parent.spawn(( Name::new("Buttons"), Node { flex_direction: FlexDirection::Column, height: Val::Percent(100.0), width: Val::Percent(40.0), overflow: Overflow::scroll_y(), ..default() }, ScrollPosition::default(), BackgroundColor(ORANGE.into()), MonologuesList, )).observe(scroll); parent.spawn(( Name::new("Preview"), MonologuePreview, NavParent(monologue_button), NavState::default(), Node { flex_direction: FlexDirection::Column, padding: UiRect::all(Val::Px(10.0)), overflow: Overflow::scroll_y(), width: Val::Percent(60.0), ..default() }, BackgroundColor(ORANGE_RED.into()), )); }); } fn hover_dialog_box_over( trigger: Trigger>, mut query: Query<&mut BackgroundColor, With>, ) { if let Ok(mut bg) = query.get_mut(trigger.target()) { bg.0.set_alpha(0.95); } } fn hover_dialog_box_out( trigger: Trigger>, mut query: Query<&mut BackgroundColor, With>, ) { if let Ok(mut bg) = query.get_mut(trigger.target()) { bg.0.set_alpha(0.9); } } /// On startup move the camera to a suitable position /// This should be mostly static for the entire game fn position_camera(mut query: Query<&mut Transform, (With, With)>) { use std::f32::consts::PI; query.iter_mut().for_each(|mut t| { *t = Transform::from_xyz(0.0, 100.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y); t.rotate_y(-PI * 0.5) }) } /// Automatically scrolls dialog when a new batch of options are added fn auto_scroll( added: Query>, mut scroll_positions: Query<&mut ScrollPosition>, ) { debug_assert!( !added.is_empty(), "Should only scroll when dialog options are added" ); scroll_positions.iter_mut().for_each(|mut sp| { sp.offset_y = f32::MAX; }); } /// A possible line of dialog the user can choose #[derive(Component)] struct DialogOption; /// A line of dialog which has been chosen, so is permanent #[derive(Component)] struct DialogLine; /// Events that drive the dialog engine #[derive(Event, PartialEq)] enum DialogEvent { Start(Entity, Handle), 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>, mut dialog_events: EventWriter, query: Query<&TreeMonologue, With>, ) { click_events.read().for_each(|event| { debug!("Click event detected"); if let Ok(TreeMonologue(handle)) = query.get(event.target) { debug!("Tree Monologue received, sending start dialog event"); dialog_events.write(DialogEvent::Start(event.target, 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>, mut dialog_events: EventWriter, query: Query, With, With)>>, ) { click_events.read().for_each(|event| { debug!("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( // React to dialog events mut events: EventReader, // Reference to DialogBox dialog_box: Single>, // EntityCommands for Dialog Box mut commands: Commands, // Handle to "active" monologue mut handle: Local>, // Track active entity as well as the monologue mut tree_entity: Local>, // Index into "active" monologue mut idx: Local, // Inform the rest of the game what state of dialog we are in mut next_state: ResMut>, // Monologue assets for obvious reasons monologues: Res>, // Dialog lines to despawn them at the end/start of a dialog lines: Query>, ) { debug_assert!( !events.is_empty(), "Dialog engine is triggered by Dialog Events" ); events.read().for_each(|event| { match event { DialogEvent::Start(e, h) => { debug!("Dialog start: {:?}", h); // Set state to "Active" next_state.set(DialogState::Ongoing); // Copy monologue asset into local *handle = h.clone(); *tree_entity = Some(*e); } DialogEvent::NextBatch => { debug!("Dialog batch"); commands.entity(*dialog_box).with_children(|parent| { // Fetch this monologue from the assets if let Some(monologue) = monologues.get(handle.clone().id()) { // Fetch this batch of options if let Some(batch) = monologue.get(*idx) { // Spawn the dialog options in the dialog box batch.lines.iter().for_each(|line| { parent.spawn(( Text::new(line.clone()), DialogOption, TextLayout::new(JustifyText::Left, LineBreak::NoWrap), )); }); *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 tree we just "spoke to" commands .entity(tree_entity.unwrap()) .remove::(); // 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>, mut dialog_events: EventWriter, mut commands: Commands, texts: Query<&Text>, options: Query>, dialog_box: Single>, ) { debug!("Choosing dialog {:?}", trigger.target()); debug!("Despawning dialog options"); options.iter().for_each(|e| { commands.entity(e).despawn(); }); debug!("Inserting dialog line"); if let Ok(t) = texts.get(trigger.target()) { commands.entity(*dialog_box).with_children(|parent| { parent.spawn((t.clone(), DialogLine)); }); } // trigger the next dialog line dialog_events.write(DialogEvent::NextBatch); } fn hover_dialog_option_over( trigger: Trigger>, mut query: Query<(&mut TextColor, &mut BackgroundColor)>, ) { if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) { *tc = TextColor(DARK_ORANGE.into()); bg.0.set_alpha(1.0); } } fn hover_dialog_option_out( trigger: Trigger>, 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); } } /// When a (Text) dialog option is added: /// 1. Add the Button component /// 2. Change the color to Orange /// 3. Add observers for click (select) and hover (change color) fn add_dialog_option(trigger: Trigger, mut commands: Commands) { commands .entity(trigger.target()) .insert(Button) .insert(Node { width: Val::Percent(100.0), ..default() }) .insert(TextLayout::new_with_justify(JustifyText::Center)) .insert(TextColor(ORANGE.into())) .observe(choose_dialog_option) .observe(hover_dialog_option_over) .observe(hover_dialog_option_out); } fn dialog_box_visibility( state: Res>, mut dialog_box: Single<&mut Visibility, With>, ) { **dialog_box = if *state.get() == DialogState::None { Visibility::Hidden } else { Visibility::Inherited }; } /// Add the "script: path/to/file.mono" tooltip info fn monologue_asset_tooltip( mut over_events: EventReader>, mut out_events: EventReader>, mut tooltip: ResMut, trees: Query<(&Tree, Option<&TreeMonologue>)>, ) { out_events .read() .filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target)) .for_each(|_| { tooltip.remove("Script"); }); over_events .read() .filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target)) .for_each(|e| { match trees.get(e) { Ok((_tree, Some(TreeMonologue(handle)))) => { match handle.path() { Some(p) => tooltip.insert("Script", format!("{p}")), None => tooltip.insert("Script", "A".into()), } }, Ok((_tree, None)) => { tooltip.insert("Script", "N/A".into()); }, _ => () } }); } fn add_tree_monologue( trigger: Trigger, query: Query<&MeshMaterial3d>, mut materials: ResMut>, ) { // Get the affected entity's MeshMaterial3d if let Ok(handle) = query.get(trigger.target()) { // Get the concrete StandardMaterial if let Some(material) = materials.get_mut(handle) { material.base_color = WHITE.with_alpha(1.0).into(); } } } fn remove_tree_monologue( trigger: Trigger, query: Query<&MeshMaterial3d>, mut materials: ResMut>, ) { // Get the affected entity's MeshMaterial3d if let Ok(handle) = query.get(trigger.target()) { // Get the concrete StandardMaterial if let Some(material) = materials.get_mut(handle) { // Make it dull material.base_color = WHITE.with_alpha(0.9).into(); } } } fn scale_window(events: EventReader, mut window: Single<&mut Window>) { debug_assert!(!events.is_empty(), "Only scale window when resized"); let r = &mut window.resolution; let a: f32 = match r.physical_width() as usize { 0..=640 => 0.6, 641..=1280 => 1.0, 1281..=1920 => 1.6, 1921.. => 2.0, }; let b: f32 = match r.physical_height() as usize { 0..=360 => 0.6, 361..=720 => 1.0, 721..=1080 => 1.6, 1081.. => 2.0, }; let n = a.min(b); r.set_scale_factor(n); debug!( "Proposed scale factor: ({} -> {a} / {} -> {b}) {n}", r.width(), r.height(), ); } fn delete_tree(trigger: Trigger>, mut commands: Commands) { if matches!(trigger.event.button, PointerButton::Middle) { info!("Middle Click -> Despawning {}", trigger.target()); commands.entity(trigger.target()).despawn(); } } /// Load all monologues so they are in the asset store and trigger on-load events fn load_monologues(server: ResMut, mut loaded_assets: Local>>) { *loaded_assets = include_str!("../../../assets/trees/MONOLOGUES") .split("\n") .map(|path| server.load(path)) .collect(); } fn spawn_debug_buttons( mut events: EventReader>, mut commands: Commands, container: Single, Without