diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c8828676..801043bcc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Currently playing music track and artist now shows in the debug menu. - Added a setting to influence the gap between music track plays. +- Added a Craft All button. ### Changed - Use fluent for translations diff --git a/assets/voxygen/i18n/ca_CA/hud/crafting.ftl b/assets/voxygen/i18n/ca_CA/hud/crafting.ftl index 9d0fb2a7dd..985787479d 100644 --- a/assets/voxygen/i18n/ca_CA/hud/crafting.ftl +++ b/assets/voxygen/i18n/ca_CA/hud/crafting.ftl @@ -2,6 +2,7 @@ hud-crafting = Elaborar hud-crafting-recipes = Receptes hud-crafting-ingredients = Ingredients: hud-crafting-craft = Elaborar +hud-crafting-craft_all = Elaborar Tot hud-crafting-tool_cata = Requereix: hud-crafting-req_crafting_station = Requereix: hud-crafting-anvil = Enclusa diff --git a/assets/voxygen/i18n/en/hud/crafting.ftl b/assets/voxygen/i18n/en/hud/crafting.ftl index 8e72a2476a..a549ae11b3 100644 --- a/assets/voxygen/i18n/en/hud/crafting.ftl +++ b/assets/voxygen/i18n/en/hud/crafting.ftl @@ -2,6 +2,7 @@ hud-crafting = Crafting hud-crafting-recipes = Recipes hud-crafting-ingredients = Ingredients: hud-crafting-craft = Craft +hud-crafting-craft_all = Craft All hud-crafting-tool_cata = Requires: hud-crafting-req_crafting_station = Requires: hud-crafting-anvil = Anvil diff --git a/assets/voxygen/i18n/es_ES/hud/crafting.ftl b/assets/voxygen/i18n/es_ES/hud/crafting.ftl index fdb35bef08..4b09d5cd30 100644 --- a/assets/voxygen/i18n/es_ES/hud/crafting.ftl +++ b/assets/voxygen/i18n/es_ES/hud/crafting.ftl @@ -2,6 +2,7 @@ hud-crafting = Fabricación hud-crafting-recipes = Recetas hud-crafting-ingredients = Ingredientes: hud-crafting-craft = Fabricar +hud-crafting-craft_all = Fabricar Todo hud-crafting-tool_cata = Requisitos: hud-crafting-req_crafting_station = Requisitos: hud-crafting-anvil = Yunque diff --git a/assets/voxygen/i18n/fr_FR/hud/crafting.ftl b/assets/voxygen/i18n/fr_FR/hud/crafting.ftl index 3fc546d361..8d6735d715 100644 --- a/assets/voxygen/i18n/fr_FR/hud/crafting.ftl +++ b/assets/voxygen/i18n/fr_FR/hud/crafting.ftl @@ -2,6 +2,7 @@ hud-crafting = Fabrication hud-crafting-recipes = Recettes hud-crafting-ingredients = Ingrédients : hud-crafting-craft = Fabriquer +hud-crafting-craft_all = Tout Fabriquer hud-crafting-tool_cata = Nécessite : hud-crafting-req_crafting_station = Nécessite: hud-crafting-anvil = Enclume diff --git a/assets/voxygen/i18n/pt_BR/hud/crafting.ftl b/assets/voxygen/i18n/pt_BR/hud/crafting.ftl index 0f63973ac5..12b78d6981 100644 --- a/assets/voxygen/i18n/pt_BR/hud/crafting.ftl +++ b/assets/voxygen/i18n/pt_BR/hud/crafting.ftl @@ -1,7 +1,8 @@ hud-crafting = Criação hud-crafting-recipes = Receitas hud-crafting-ingredients = Ingredientes: -hud-crafting-craft = Criar +hud-crafting-craft = Forjar +hud-crafting-craft_all = Forjar Tudo hud-crafting-tool_cata = Requer: hud-crafting-req_crafting_station = Requer: hud-crafting-anvil = Bigorna diff --git a/client/src/lib.rs b/client/src/lib.rs index 9a583bcb05..711639cc65 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1074,13 +1074,13 @@ impl Client { /// Returns whether the specified recipe can be crafted and the sprite, if /// any, that is required to do so. - pub fn can_craft_recipe(&self, recipe: &str) -> (bool, Option) { + pub fn can_craft_recipe(&self, recipe: &str, amount: u32) -> (bool, Option) { self.recipe_book .get(recipe) .zip(self.inventories().get(self.entity())) .map(|(recipe, inv)| { ( - recipe.inventory_contains_ingredients(inv).is_ok(), + recipe.inventory_contains_ingredients(inv, amount).is_ok(), recipe.craft_sprite, ) }) @@ -1092,8 +1092,9 @@ impl Client { recipe: &str, slots: Vec<(u32, InvSlotId)>, craft_sprite: Option<(Vec3, SpriteKind)>, + amount: u32, ) -> bool { - let (can_craft, required_sprite) = self.can_craft_recipe(recipe); + let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount); let has_sprite = required_sprite.map_or(true, |s| Some(s) == craft_sprite.map(|(_, s)| s)); if can_craft && has_sprite { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent( @@ -1101,6 +1102,7 @@ impl Client { craft_event: CraftEvent::Simple { recipe: recipe.to_string(), slots, + amount, }, craft_sprite: craft_sprite.map(|(pos, _)| pos), }, @@ -1213,7 +1215,7 @@ impl Client { .iter() .map(|(name, _)| name.clone()) .filter_map(|name| { - let (can_craft, required_sprite) = self.can_craft_recipe(&name); + let (can_craft, required_sprite) = self.can_craft_recipe(&name, 1); if can_craft { Some((name, required_sprite)) } else { diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 9ceeb62c11..6c87700846 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -98,6 +98,7 @@ pub enum CraftEvent { Simple { recipe: String, slots: Vec<(u32, InvSlotId)>, + amount: u32, }, Salvage(InvSlotId), // TODO: Maybe look at making this more general when there are more modular recipes? diff --git a/common/src/recipe.rs b/common/src/recipe.rs index 079f00ff1b..4bc704e895 100644 --- a/common/src/recipe.rs +++ b/common/src/recipe.rs @@ -164,13 +164,52 @@ impl Recipe { pub fn inventory_contains_ingredients( &self, inv: &Inventory, + recipe_amount: u32, ) -> Result, Vec<(&RecipeInput, u32)>> { inventory_contains_ingredients( self.inputs() .map(|(input, amount, _is_modular)| (input, amount)), inv, + recipe_amount, ) } + + /// Calculates the maximum number of items craftable given the current + /// inventory state. + pub fn max_from_ingredients(&self, inv: &Inventory) -> u32 { + let mut max_recipes = None; + + for (input, amount) in self + .inputs() + .map(|(input, amount, _is_modular)| (input, amount)) + { + let needed = amount as f32; + let mut input_max = HashMap::::new(); + + // Checks through every slot, filtering to only those that contain items that + // can satisfy the input. + for (inv_slot_id, slot) in inv.slots_with_id() { + if let Some(item) = slot + .as_ref() + .filter(|item| item.matches_recipe_input(&*input, amount)) + { + *input_max.entry(inv_slot_id).or_insert(0) += item.amount(); + } + } + + // Updates maximum craftable amount based on least recipe-proportional + // availability. + let max_item_proportion = + ((input_max.values().sum::() as f32) / needed).floor() as u32; + max_recipes = Some(match max_recipes { + None => max_item_proportion, + Some(max_recipes) if (max_item_proportion < max_recipes) => max_item_proportion, + Some(n) => n, + }); + } + + max_recipes.unwrap_or(0) + } } /// Determine whether the inventory contains the ingredients for a recipe. @@ -183,6 +222,7 @@ impl Recipe { fn inventory_contains_ingredients<'a, I: Iterator>( ingredients: I, inv: &Inventory, + recipe_amount: u32, ) -> Result, Vec<(&'a RecipeInput, u32)>> { // Hashmap tracking the quantity that needs to be removed from each slot (so // that it doesn't think a slot can provide more items than it contains) @@ -194,7 +234,7 @@ fn inventory_contains_ingredients<'a, I: Iterator let mut missing = Vec::<(&RecipeInput, u32)>::new(); for (i, (input, amount)) in ingredients.enumerate() { - let mut needed = amount; + let mut needed = amount * recipe_amount; let mut contains_any = false; // Checks through every slot, filtering to only those that contain items that // can satisfy the input @@ -358,7 +398,7 @@ impl RecipeBook { pub fn get_available(&self, inv: &Inventory) -> Vec<(String, Recipe)> { self.recipes .iter() - .filter(|(_, recipe)| recipe.inventory_contains_ingredients(inv).is_ok()) + .filter(|(_, recipe)| recipe.inventory_contains_ingredients(inv, 1).is_ok()) .map(|(name, recipe)| (name.clone(), recipe.clone())) .collect() } @@ -654,6 +694,7 @@ impl ComponentRecipe { .iter() .map(|(input, amount)| (input, *amount)), inv, + 1, ) } diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 70a806a731..618339160c 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -672,7 +672,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv }; let crafted_items = match craft_event { - CraftEvent::Simple { recipe, slots } => recipe_book + CraftEvent::Simple { + recipe, + slots, + amount, + } => recipe_book .get(&recipe) .filter(|r| { if let Some(needed_sprite) = r.craft_sprite { @@ -683,13 +687,21 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } }) .and_then(|r| { - r.craft_simple( - &mut inventory, - slots, - &state.ecs().read_resource::(), - &state.ecs().read_resource::(), - ) - .ok() + let items = (0..amount) + .into_iter() + .filter_map(|_| { + r.craft_simple( + &mut inventory, + slots.clone(), + &state.ecs().read_resource::(), + &state.ecs().read_resource::(), + ) + .ok() + }) + .flatten() + .collect::>(); + + if items.is_empty() { None } else { Some(items) } }), CraftEvent::Salvage(slot) => { let sprite = get_craft_sprite(state, craft_sprite); diff --git a/voxygen/src/hud/crafting.rs b/voxygen/src/hud/crafting.rs index 9de2389392..c8c91e01e7 100644 --- a/voxygen/src/hud/crafting.rs +++ b/voxygen/src/hud/crafting.rs @@ -38,7 +38,7 @@ use hashbrown::HashMap; use i18n::Localization; use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; use strum::{EnumIter, IntoEnumIterator}; -use tracing::warn; +use tracing::{error, warn}; use vek::*; widget_ids! { @@ -61,6 +61,7 @@ widget_ids! { align_ing, scrollbar_ing, btn_craft, + btn_craft_all, recipe_list_btns[], recipe_list_labels[], recipe_list_quality_indicators[], @@ -96,7 +97,10 @@ widget_ids! { } pub enum Event { - CraftRecipe(String), + CraftRecipe { + recipe_name: String, + amount: u32, + }, CraftModularWeapon { primary_slot: InvSlotId, secondary_slot: InvSlotId, @@ -1355,7 +1359,7 @@ impl<'a> Widget for Crafting<'a> { .label_font_size(self.fonts.cyri.scale(12)) .label_font_id(self.fonts.cyri.conrod_id) .image_color(can_perform.then_some(TEXT_COLOR).unwrap_or(TEXT_GRAY_COLOR)) - .mid_bottom_with_margin_on(state.ids.align_ing, -31.0) + .bottom_left_with_margins_on(state.ids.align_ing, -31.0, 15.0) .parent(state.ids.window_frame) .set(state.ids.btn_craft, ui) .was_clicked() @@ -1381,10 +1385,62 @@ impl<'a> Widget for Crafting<'a> { }); } }, - RecipeKind::Simple => events.push(Event::CraftRecipe(recipe_name)), + RecipeKind::Simple => events.push(Event::CraftRecipe { + recipe_name, + amount: 1, + }), } } + // Craft All button + let can_perform_all = can_perform && matches!(recipe_kind, RecipeKind::Simple); + if Button::image(self.imgs.button) + .w_h(105.0, 25.0) + .hover_image( + can_perform + .then_some(self.imgs.button_hover) + .unwrap_or(self.imgs.button), + ) + .press_image( + can_perform + .then_some(self.imgs.button_press) + .unwrap_or(self.imgs.button), + ) + .label(&self.localized_strings.get("hud.crafting.craft_all")) + .label_y(conrod_core::position::Relative::Scalar(1.0)) + .label_color( + can_perform_all + .then_some(TEXT_COLOR) + .unwrap_or(TEXT_GRAY_COLOR), + ) + .label_font_size(self.fonts.cyri.scale(12)) + .label_font_id(self.fonts.cyri.conrod_id) + .image_color( + can_perform_all + .then_some(TEXT_COLOR) + .unwrap_or(TEXT_GRAY_COLOR), + ) + .bottom_right_with_margins_on(state.ids.align_ing, -31.0, 15.0) + .parent(state.ids.window_frame) + .set(state.ids.btn_craft_all, ui) + .was_clicked() + && can_perform_all + { + if let (RecipeKind::Simple, Some(selected_recipe)) = + (recipe_kind, &state.selected_recipe) + { + let amount = recipe.max_from_ingredients(self.inventory); + if amount > 0 { + events.push(Event::CraftRecipe { + recipe_name: selected_recipe.to_string(), + amount, + }); + } + } else { + error!("State shows no selected recipe when trying to craft multiple."); + } + }; + // Crafting Station Info if recipe.craft_sprite.is_some() { Text::new( diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 1f4520b767..3c7f322347 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -538,8 +538,9 @@ pub enum Event { Quit, CraftRecipe { - recipe: String, + recipe_name: String, craft_sprite: Option<(Vec3, SpriteKind)>, + amount: u32, }, SalvageItem { slot: InvSlotId, @@ -2912,10 +2913,14 @@ impl Hud { .set(self.ids.crafting_window, ui_widgets) { match event { - crafting::Event::CraftRecipe(recipe) => { + crafting::Event::CraftRecipe { + recipe_name, + amount, + } => { events.push(Event::CraftRecipe { - recipe, + recipe_name, craft_sprite: self.show.crafting_fields.craft_sprite, + amount, }); }, crafting::Event::CraftModularWeapon { diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 60b682073b..21e97b1dec 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1541,26 +1541,30 @@ impl PlayState for SessionState { }, HudEvent::CraftRecipe { - recipe, + recipe_name: recipe, craft_sprite, + amount, } => { let slots = { let client = self.client.borrow(); if let Some(recipe) = client.recipe_book().get(&recipe) { - client - .inventories() - .get(client.entity()) - .and_then(|inv| recipe.inventory_contains_ingredients(inv).ok()) + client.inventories().get(client.entity()).and_then(|inv| { + recipe.inventory_contains_ingredients(inv, 1).ok() + }) } else { None } }; if let Some(slots) = slots { - self.client - .borrow_mut() - .craft_recipe(&recipe, slots, craft_sprite); + self.client.borrow_mut().craft_recipe( + &recipe, + slots, + craft_sprite, + amount, + ); } }, + HudEvent::CraftModularWeapon { primary_slot, secondary_slot, @@ -1572,6 +1576,7 @@ impl PlayState for SessionState { craft_sprite, ); }, + HudEvent::CraftModularWeaponComponent { toolkind, material,