use bevy::{input_focus::{AutoFocus, InputFocus}, ui::RelativeCursorPosition};
use crate::{attachment::PartInShip, client::colors, config::recipe::RecipesConfig, ecs::{CanCraft, CraftingUi, Drill, MainCamera, Me, Part, SingleStorage, ToggleDrillEvent}, prelude::*};
pub fn crafting_ui_plugin(app: &mut App) {
app.init_resource::<RecipeCollection>();
app.add_systems(Startup, load_recipes);
app.add_systems(PreUpdate, (initial_create_recipe_list, update_recipe_list));
app.add_systems(Update, (close_button, drill_button, drill_state_change, single_storage_display));
}
#[derive(Component)]
struct CloseButton(Entity); // stores corresponding menu entity
#[derive(Component)]
struct PreviousInteraction(Interaction);
#[derive(Component)]
struct DrillButton(Entity); // stores corresponding part
#[derive(Component)]
struct SingleStorageDisplay(Entity); // stores corresponding part
#[derive(Component)]
struct RecipesHolder;
#[derive(Component)]
struct PendingRecipesHolder;
#[derive(Resource, Default)]
struct RecipeCollection {
handle: Option<Handle<RecipesConfig>>,
}
fn load_recipes(asset_server: Res<AssetServer>, mut recipe_collection: ResMut<RecipeCollection>) {
recipe_collection.handle = Some(asset_server.load("config/recipes.rc.toml"));
}
pub fn open_crafting_ui(
ev: On<Pointer<Press>>,
crafting_parts: Query<(Entity, &Transform, Option<&Drill>, Option<&SingleStorage>), (With<PartInShip>, With<CanCraft>)>,
hearty: Query<(Entity, &Transform, Option<&Drill>, Option<&SingleStorage>), (With<Me>, With<CanCraft>)>,
camera: Single<(Entity, &Camera, &GlobalTransform), (With<MainCamera>, Without<PartInShip>)>,
commands: Commands,
) {
if matches!(ev.button, PointerButton::Secondary) {
let (entity, transform, drill, single_storage) = if let Ok(part) = crafting_parts.get(ev.entity) {
part
} else if let Ok(part) = hearty.get(ev.entity) {
part
} else {
return
};
// we have our crafting entity!
// now make the ui
setup_ui(entity, transform, commands, camera, drill, single_storage);
}
}
fn setup_ui(
parent_part: Entity,
parent_transform: &Transform,
mut commands: Commands,
camera: Single<(Entity, &Camera, &GlobalTransform), (With<MainCamera>, Without<PartInShip>)>,
drill: Option<&Drill>,
single_storage: Option<&SingleStorage>,
) {
let parent_pos = camera.1.world_to_viewport(camera.2, parent_transform.translation).unwrap();
let entity = commands.spawn((
UiTargetCamera(camera.0),
Node {
position_type: PositionType::Absolute,
left: Val::Px(parent_pos.x),
top: Val::Px(parent_pos.y),
width: Val::Px(100.0),
height: Val::Px(100.0),
display: Display::Flex,
flex_direction: FlexDirection::Column,
..default()
},
AutoFocus,
CraftingUi,
BackgroundColor(colors::MANTLE),
RelativeCursorPosition::default(),
))
.with_children(|parent| {
parent.spawn((
Node {
width: Val::Px(25.0),
height: Val::Px(25.0),
justify_content: JustifyContent::Center,
align_content: AlignContent::Center,
..Default::default()
},
Button,
BackgroundColor(colors::RED),
CloseButton(parent.target_entity()),
PreviousInteraction(Interaction::None),
))
.with_children(|parent| {
parent.spawn((
Node {
..Default::default()
},
Text::new("x"),
));
});
// only add the drill button if the part is a drill
if let Some(drill) = drill {
parent.spawn((
Node {
width: Val::Px(100.0),
height: Val::Px(30.0),
..Default::default()
},
Button,
DrillButton(parent_part),
BackgroundColor(colors::CRUST),
PreviousInteraction(Interaction::None),
))
.with_children(|parent| {
parent.spawn((
Node {
..Default::default()
},
TextLayout::new(Justify::Center, LineBreak::WordBoundary),
TextFont {
font_size: 10.0,
..Default::default()
},
Text::new(get_drill_text(drill)),
));
});
}
// only add storage if the part has single storage
if let Some(single_storage) = single_storage {
parent.spawn((
Node {
..Default::default()
},
TextFont {
font_size: 10.0,
..Default::default()
},
Text::new(format!("{}: {}", single_storage.resource_name, single_storage.stored)),
SingleStorageDisplay(parent_part),
));
}
// assume CanCraft for now (THIS WILL CHANGE)
parent.spawn((
Node {
..Default::default()
},
PendingRecipesHolder,
));
});
}
fn initial_create_recipe_list(
mut commands: Commands,
added_recipes_holders: Query<Entity, With<PendingRecipesHolder>>,
recipe_collection: ResMut<RecipeCollection>,
recipes_config: Res<Assets<RecipesConfig>>,
) {
if let Some(strong_recipes_config) = recipes_config.get(&recipe_collection.handle.clone().unwrap()) {
for recipe_holder in &added_recipes_holders {
let mut recipe_holder = commands.get_entity(recipe_holder).unwrap();
debug!("{:?}", strong_recipes_config);
recipe_holder
.insert(RecipesHolder)
.remove::<PendingRecipesHolder>();
}
}
}
fn update_recipe_list(
mut ev_config: MessageReader<AssetEvent<RecipesConfig>>,
recipe_collection: ResMut<RecipeCollection>,
) {
let Some(handle) = recipe_collection.handle.as_ref() else {
return
};
for ev in ev_config.read() {
if let AssetEvent::Modified { id } = ev {
if *id == handle.id() {
debug!("recipe list config modified - reloading lists");
}
}
}
}
fn drill_button(
mut interaction_query: Query<
(
&Interaction,
&mut PreviousInteraction,
&mut BackgroundColor,
&DrillButton,
&mut Button,
&Children,
),
Changed<Interaction>,
>,
mut toggle_drill_writer: MessageWriter<ToggleDrillEvent>,
mut text_query: Query<&mut Text>,
drills: Query<&Drill>,
) {
for (interaction, mut previous_interaction, mut color, drill_button, mut button, children) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
*color = colors::SURFACE_1.into();
}
Interaction::Hovered => {
*color = colors::SURFACE_0.into();
if previous_interaction.0 == Interaction::Pressed {
// released
let mut text = text_query.get_mut(children[0]).unwrap();
let Ok(drill) = drills.get(drill_button.0) else {
error!("A former drill is now not a drill, causing a problem in the drill button");
previous_interaction.0 = *interaction;
return
};
// don't allow drill toggling while not on a planet
if drill.on_planet.is_none() { return }
// the text is flipped because drill.drilling is an old value,
// which was now toggled
if drill.drilling {
**text = "Start Drill".to_string();
} else {
**text = "Stop Drill".to_string();
}
toggle_drill_writer.write(ToggleDrillEvent { drill_entity: drill_button.0 });
}
}
Interaction::None => {
*color = colors::CRUST.into();
}
}
previous_interaction.0 = *interaction;
}
}
fn drill_state_change(
drills: Query<&Drill, Changed<Drill>>,
drill_buttons: Query<(&DrillButton, &Children)>,
mut text_query: Query<&mut Text>,
) {
for (drill_button, children) in &drill_buttons {
let Ok(drill) = drills.get(drill_button.0) else {
continue
};
let mut text = text_query.get_mut(children[0]).unwrap();
**text = get_drill_text(drill);
}
}
fn get_drill_text(drill: &Drill) -> String {
if drill.on_planet.is_some() {
if drill.drilling {
"Stop Drill".to_string()
} else {
"Start Drill".to_string()
}
} else {
"Drill not on planet".to_string()
}
}
fn single_storage_display(
mut single_storage_display_query: Query<(&mut Text, &SingleStorageDisplay)>,
part_query: Query<&SingleStorage, With<Part>>,
) {
for (mut text, single_storage_display) in &mut single_storage_display_query {
let single_storage = part_query.get(single_storage_display.0).expect("In single_storage_display, the entity didn't match a storage.");
**text = format!("{}: {}", single_storage.resource_name, single_storage.stored);
}
}
fn close_button(
mut commands: Commands,
mut interaction_query: Query<
(&Interaction, &mut PreviousInteraction, &mut BackgroundColor, &CloseButton, &mut Button),
Changed<Interaction>,
>,
mouse: Res<ButtonInput<MouseButton>>,
) {
for (interaction, mut previous_interaction, mut color,
close_button, mut button) in &mut interaction_query
{
match *interaction {
Interaction::Pressed => {
*color = colors::MAROON.into();
}
Interaction::Hovered => {
*color = colors::PINK.into();
if previous_interaction.0 == Interaction::Pressed {
commands.entity(close_button.0).despawn();
}
}
Interaction::None => {
*color = colors::RED.into();
}
}
previous_interaction.0 = *interaction;
}
}