#![allow(dead_code)] #![allow(clippy::type_complexity)] #![allow(clippy::too_many_arguments)] #![feature(trim_prefix_suffix)] mod debug; mod mono; use bevy::{picking::hover::HoverMap, platform::hash::RandomState}; use debug::*; use games::*; use mono::*; use std::hash::BuildHasher; fn main() { App::new() .add_plugins(BaseGamePlugin { name: "trees".into(), ..default() }) .add_plugins(MonologueAssetsPlugin) .add_plugins(TreesDebugPlugin) .add_event::() .add_event::() .add_event::() .init_state::() .insert_resource(ClearColor(WHITE.into())) .add_systems( Startup, ( init_trees, init_ui, load_monologues, position_camera.after(create_camera_3d), ), ) // When we're done loading, plant forest .add_systems(OnEnter(LoadingState::Idle), plant_forest.run_if(run_once)) .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::>), handle_plant_tree.run_if(on_event::), assign_monologue_to_tree .run_if(on_event::) .after(handle_plant_tree), dialog_engine.run_if(on_event::), auto_scroll.run_if(any_component_added::), dialog_box_visibility.run_if(state_changed::), scale_window.run_if(on_event::), ), ) .add_observer(add_tree_monologue) .add_observer(remove_tree_monologue) .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); } 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 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 in start dialog systme"); 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); } } }); } 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); } } 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 }; } 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(), ); } /// 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(); } #[derive(Event)] struct PlantTree(Option>); /// Plan a tree in the world /// Handles random placement, 3d model, materials, and observers fn handle_plant_tree( mut events: EventReader, mut assignments: EventWriter, trees: Query>, server: Res, mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { events.read().for_each(|PlantTree(assignment)| { debug!("Planing tree"); let mut tree = commands.spawn(Tree); // Generate "random" X and Y Coordinates for this tree // 1. Take the top 2 bytes // 2. Interpet as u8s // 3. Re-interpret as i8s // 4. Cast to f32 let transform = { let n = RandomState::default().hash_one(tree.id()); let [a, b, ..] = n.to_be_bytes(); let x: f32 = a.cast_signed().wrapping_div(4).into(); let y: f32 = b.cast_signed().wrapping_div(4).into(); // Avoid mesh clipping by offsetting each on the z axis let z = trees.iter().len() as f32; debug!("Coordiantes: {x} {y}"); Transform::from_xyz(x, z, y).with_scale(Vec3::splat(10.0)) }; let material = MeshMaterial3d(materials.add(StandardMaterial { base_color_texture: Some(server.load("trees/placeholder/tree.png")), base_color: WHITE.with_alpha(0.9).into(), alpha_mode: AlphaMode::Blend, ..default() })); let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))); tree.insert((mesh, material, transform)); if let Some(handle) = assignment { assignments.write(AssignMonologue(handle.clone())); } }); } #[derive(Event, Debug)] struct AssignMonologue(Handle); /// Assign the given monologue to a tree fn assign_monologue_to_tree( mut events: EventReader, query: Query, Without)>, mut notice: ResMut, mut commands: Commands, ) { // Kinda a weird hack because query does not update // If we do this inline we assign new monologues to the same first tree let mut t = query.iter(); events.read().for_each(|event| { debug!("Assigning monologue {:?}", event); // Get a valid tree to assign an entity to if let Some(tree) = t.next() { // Create the TreeMonologue component let monologue = TreeMonologue(event.0.clone()); // Insert the component to the entity commands.entity(tree).insert(monologue); } else if let Some(path) = event.0.path() { error!("No trees avaliable for {path:?}"); notice.0 = format!("No trees avaliable for {path:?}"); } else { error!("Monologue is not yet loaded!"); notice.0 = "Monologue is not yet loaded!".into(); } }); } /// On startup, plant a forest (add a few trees to the game) fn plant_forest(monos: Res>, mut e_trees: EventWriter) { let mut i = 10; for id in monos.ids() { debug!("Planting tree with monologue {:?}", id); if i > 5 { e_trees.write(PlantTree(Some(Handle::Weak(id)))); } else if i > 0 { e_trees.write(PlantTree(None)); } else { break; } i -= 1; } }