Consolidated crafting UI for the primnary component of modular weapons.

This commit is contained in:
Sam 2022-01-09 19:10:25 -05:00
parent 74a3f4a7dc
commit d436362a8d
12 changed files with 5952 additions and 3213 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ use common::{
chat::{KillSource, KillType},
controller::CraftEvent,
group,
inventory::item::{modular, ItemKind},
inventory::item::{modular, tool, ItemKind},
invite::{InviteKind, InviteResponse},
skills::Skill,
slot::{EquipSlot, InvSlotId, Slot},
@ -39,7 +39,7 @@ use common::{
lod,
mounting::Rider,
outcome::Outcome,
recipe::RecipeBook,
recipe::{ComponentRecipeBook, RecipeBook},
resources::{PlayerEntity, TimeOfDay},
spiral::Spiral2d,
terrain::{
@ -173,6 +173,7 @@ pub struct Client {
pois: Vec<PoiInfo>,
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>,
@ -291,6 +292,7 @@ impl Client {
sites,
pois,
recipe_book,
component_recipe_book,
max_group_size,
client_timeout,
) = match loop {
@ -306,6 +308,7 @@ impl Client {
client_timeout,
world_map,
recipe_book,
component_recipe_book,
material_stats,
ability_map,
} => {
@ -593,6 +596,7 @@ impl Client {
world_map.sites,
world_map.pois,
recipe_book,
component_recipe_book,
max_group_size,
client_timeout,
))
@ -627,6 +631,7 @@ impl Client {
.collect(),
pois,
recipe_book,
component_recipe_book,
available_recipes: HashMap::default(),
chat_mode: ChatMode::default(),
@ -1006,6 +1011,8 @@ impl Client {
pub fn recipe_book(&self) -> &RecipeBook { &self.recipe_book }
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
&self.available_recipes
}
@ -1131,6 +1138,27 @@ impl Client {
}
}
pub fn craft_modular_weapon_component(
&mut self,
toolkind: tool::ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Option<Vec3<i32>>,
) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::ModularWeaponPrimaryComponent {
toolkind,
material,
modifier,
slots,
},
craft_sprite: sprite_pos,
},
)));
}
fn update_available_recipes(&mut self) {
self.available_recipes = self
.recipe_book

View File

@ -9,7 +9,7 @@ use common::{
comp::{self, invite::InviteKind, item::MaterialStatManifest},
lod,
outcome::Outcome,
recipe::RecipeBook,
recipe::{ComponentRecipeBook, RecipeBook},
resources::TimeOfDay,
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult},
@ -61,6 +61,7 @@ pub enum ServerInit {
client_timeout: Duration,
world_map: crate::msg::world_msg::WorldMapMsg,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap,
},

View File

@ -1,7 +1,10 @@
use crate::{
comp::{
ability,
inventory::slot::{EquipSlot, InvSlotId, Slot},
inventory::{
item::tool::ToolKind,
slot::{EquipSlot, InvSlotId, Slot},
},
invite::{InviteKind, InviteResponse},
BuffKind,
},
@ -103,6 +106,13 @@ pub enum CraftEvent {
primary_component: InvSlotId,
secondary_component: InvSlotId,
},
// TODO: Maybe try to consolidate into another? Otherwise eventually make more general.
ModularWeaponPrimaryComponent {
toolkind: ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -3,7 +3,9 @@ use crate::{
comp::{
inventory::slot::InvSlotId,
item::{
modular, tool::AbilityMap, ItemBase, ItemDef, ItemKind, ItemTag, MaterialStatManifest,
modular,
tool::{AbilityMap, ToolKind},
ItemBase, ItemDef, ItemKind, ItemTag, MaterialStatManifest,
},
Inventory, Item,
},
@ -154,15 +156,29 @@ impl Recipe {
.map(|(item_def, amount, is_mod_comp)| (item_def, *amount, *is_mod_comp))
}
/// Determines if the inventory contains the ingredients for a given recipe
pub fn inventory_contains_ingredients(
&self,
inv: &Inventory,
) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
inventory_contains_ingredients(
self.inputs()
.map(|(input, amount, _is_modular)| (input, amount)),
inv,
)
}
}
/// Determine whether the inventory contains the ingredients for a recipe.
/// If it does, return a vec of inventory slots that contain the
/// ingredients needed, whose positions correspond to particular recipe
/// inputs. If items are missing, return the missing items, and how many
/// are missing.
pub fn inventory_contains_ingredients(
&self,
inv: &Inventory,
) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
#[allow(clippy::type_complexity)]
fn inventory_contains_ingredients<'a, 'b, I: Iterator<Item = (&'a RecipeInput, u32)>>(
ingredients: I,
inv: &'b Inventory,
) -> Result<Vec<(u32, InvSlotId)>, 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)
let mut slot_claims = HashMap::<InvSlotId, u32>::new();
@ -172,7 +188,7 @@ impl Recipe {
// The inputs to a recipe that have missing items, and the amount missing
let mut missing = Vec::<(&RecipeInput, u32)>::new();
for (i, (input, amount, _)) in self.inputs().enumerate() {
for (i, (input, amount)) in ingredients.enumerate() {
let mut needed = amount;
let mut contains_any = false;
// Checks through every slot, filtering to only those that contain items that
@ -202,7 +218,6 @@ impl Recipe {
Err(missing)
}
}
}
pub enum SalvageError {
NotSalvageable,
@ -443,6 +458,331 @@ impl assets::Compound for RecipeBook {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ComponentRecipeBook {
recipes: HashMap<ComponentKey, ComponentRecipe>,
}
impl ComponentRecipeBook {
pub fn get(&self, key: &ComponentKey) -> Option<&ComponentRecipe> { self.recipes.get(key) }
pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ComponentKey, &ComponentRecipe)> {
self.recipes.iter()
}
}
#[derive(Clone, Deserialize)]
#[serde(transparent)]
struct RawComponentRecipeBook(HashMap<ComponentKey, RawComponentRecipe>);
impl assets::Asset for RawComponentRecipeBook {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, Eq, PartialEq)]
pub struct ComponentKey {
// Can't use ItemDef here because hash needed, item definition id used instead
// TODO: Figure out how to get back to ItemDef maybe?
// Keeping under ComponentRecipe may be sufficient?
// TODO: Make more general for other things that have component inputs that should be tracked
// after item creation
pub toolkind: ToolKind,
pub material: String,
pub modifier: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ComponentRecipe {
output: ComponentOutput,
material: (RecipeInput, u32),
modifier: Option<(RecipeInput, u32)>,
additional_inputs: Vec<(RecipeInput, u32)>,
pub craft_sprite: Option<SpriteKind>,
}
impl ComponentRecipe {
/// Craft an itme that has components, returning a list of missing items on
/// failure
pub fn craft_component(
&self,
inv: &mut Inventory,
material_slot: InvSlotId,
modifier_slot: Option<InvSlotId>,
// Vec tying an input to a slot
slots: Vec<(u32, InvSlotId)>,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new();
fn handle_requirement<'a, 'b, I: Iterator<Item = InvSlotId>>(
slot_claims: &mut HashMap<InvSlotId, u32>,
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
inv: &'b Inventory,
input: &'a RecipeInput,
amount: u32,
input_slots: I,
) {
let mut required = amount;
// Check used for recipes that have an input that is not consumed, e.g.
// craftsman hammer
let mut contains_any = false;
// Goes through each slot and marks some amount from each slot as claimed
for slot in input_slots {
// Checks that the item in the slot can be used for the input
if let Some(item) = inv
.get(slot)
.filter(|item| item.matches_recipe_input(input, amount))
{
// Gets the number of items claimed from the slot, or sets to 0 if slot has
// not been claimed by another input yet
let claimed = slot_claims.entry(slot).or_insert(0);
let available = item.amount().saturating_sub(*claimed);
let provided = available.min(required);
required -= provided;
*claimed += provided;
contains_any = true;
}
}
// If there were not sufficient items to cover requirement between all provided
// slots, or if non-consumed item was not present, mark input as not satisfied
if required > 0 || !contains_any {
unsatisfied_requirements.push((input, required));
}
}
// Checks each input against slots in the inventory. If the slots contain an
// item that fulfills the need of the input, marks some of the item as claimed
// up to quantity needed for the crafting input. If the item either
// cannot be used, or there is insufficient quantity, adds input and
// number of materials needed to unsatisfied requirements.
handle_requirement(
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
&self.material.0,
self.material.1,
core::iter::once(material_slot),
);
if let Some((modifier_input, modifier_amount)) = &self.modifier {
// TODO: Better way to get slot to use that ensures this requirement fails if no
// slot provided?
handle_requirement(
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
modifier_input,
*modifier_amount,
core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
);
}
self.additional_inputs
.iter()
.enumerate()
.for_each(|(i, (input, amount))| {
// Gets all slots provided for this input by the frontend
let input_slots = slots
.iter()
.filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
.copied();
// Checks if requirement is met, and if not marks it as unsatisfied
handle_requirement(
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
input,
*amount,
input_slots,
);
});
// If there are no unsatisfied requirements, create the items produced by the
// recipe in the necessary quantity and remove the items that the recipe
// consumes
if unsatisfied_requirements.is_empty() {
for (slot, to_remove) in slot_claims.iter() {
for _ in 0..*to_remove {
let _ = inv
.take(*slot, ability_map, msm)
.expect("Expected item to exist in the inventory");
}
}
let crafted_item = self.item_output(ability_map, msm);
Ok(vec![crafted_item])
} else {
Err(unsatisfied_requirements)
}
}
#[allow(clippy::type_complexity)]
/// Determines if the inventory contains the ignredients for a given recipe
pub fn inventory_contains_additional_ingredients<'a>(
&self,
inv: &'a Inventory,
) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
inventory_contains_ingredients(
self.additional_inputs
.iter()
.map(|(input, amount)| (input, *amount)),
inv,
)
}
pub fn item_output(&self, ability_map: &AbilityMap, msm: &MaterialStatManifest) -> Item {
match &self.output {
ComponentOutput::Item(item_def) => Item::new_from_item_base(
ItemBase::Raw(Arc::clone(item_def)),
Vec::new(),
ability_map,
msm,
),
ComponentOutput::ItemComponents {
item: item_def,
components,
} => {
let components = components
.iter()
.map(|item_def| {
Item::new_from_item_base(
ItemBase::Raw(Arc::clone(item_def)),
Vec::new(),
ability_map,
msm,
)
})
.collect::<Vec<_>>();
Item::new_from_item_base(
ItemBase::Raw(Arc::clone(item_def)),
components,
ability_map,
msm,
)
},
}
}
}
#[derive(Clone, Deserialize)]
struct RawComponentRecipe {
output: RawComponentOutput,
material: (RawRecipeInput, u32),
modifier: Option<(RawRecipeInput, u32)>,
additional_inputs: Vec<(RawRecipeInput, u32)>,
craft_sprite: Option<SpriteKind>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum ComponentOutput {
Item(Arc<ItemDef>),
ItemComponents {
item: Arc<ItemDef>,
components: Vec<Arc<ItemDef>>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum RawComponentOutput {
Item(String),
ItemComponents {
item: String,
components: Vec<String>,
},
}
impl assets::Compound for ComponentRecipeBook {
fn load<S: assets::source::Source + ?Sized>(
cache: &assets::AssetCache<S>,
specifier: &str,
) -> Result<Self, assets::BoxedError> {
#[inline]
fn load_recipe_input(
(input, amount): &(RawRecipeInput, u32),
) -> Result<(RecipeInput, u32), assets::Error> {
let def = match &input {
RawRecipeInput::Item(name) => RecipeInput::Item(Arc::<ItemDef>::load_cloned(name)?),
RawRecipeInput::Tag(tag) => RecipeInput::Tag(*tag),
RawRecipeInput::TagSameItem(tag) => RecipeInput::TagSameItem(*tag),
RawRecipeInput::ListSameItem(list) => {
let assets = &ItemList::load_expect(list).read().0;
let items = assets
.iter()
.map(|asset| Arc::<ItemDef>::load_expect_cloned(asset))
.collect();
RecipeInput::ListSameItem(items)
},
};
Ok((def, *amount))
}
#[inline]
fn load_recipe_output(
output: &RawComponentOutput,
) -> Result<ComponentOutput, assets::Error> {
let def = match &output {
RawComponentOutput::Item(def) => {
ComponentOutput::Item(Arc::<ItemDef>::load_cloned(def)?)
},
RawComponentOutput::ItemComponents {
item: def,
components: defs,
} => ComponentOutput::ItemComponents {
item: Arc::<ItemDef>::load_cloned(def)?,
components: defs
.iter()
.map(|def| Arc::<ItemDef>::load_cloned(def))
.collect::<Result<Vec<_>, _>>()?,
},
};
Ok(def)
}
let raw = cache.load::<RawComponentRecipeBook>(specifier)?.cloned();
let recipes = raw
.0
.iter()
.map(
|(
key,
RawComponentRecipe {
output,
material,
modifier,
additional_inputs,
craft_sprite,
},
)| {
let additional_inputs = additional_inputs
.iter()
.map(load_recipe_input)
.collect::<Result<Vec<_>, _>>()?;
let material = load_recipe_input(material)?;
let modifier = modifier.as_ref().map(load_recipe_input).transpose()?;
let output = load_recipe_output(output)?;
Ok((key.clone(), ComponentRecipe {
output,
material,
modifier,
additional_inputs,
craft_sprite: *craft_sprite,
}))
},
)
.collect::<Result<_, assets::Error>>()?;
Ok(ComponentRecipeBook { recipes })
}
}
pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
RecipeBook::load_expect("common.recipe_book")
}
pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
ComponentRecipeBook::load_expect("common.component_recipe_book")
}

View File

@ -11,7 +11,7 @@ use common::{
slot::{self, Slot},
},
consts::MAX_PICKUP_RANGE,
recipe::{self, default_recipe_book},
recipe::{self, default_component_recipe_book, default_recipe_book},
terrain::SpriteKind,
trade::Trades,
uid::Uid,
@ -593,7 +593,9 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
craft_sprite,
} => {
use comp::controller::CraftEvent;
use recipe::ComponentKey;
let recipe_book = default_recipe_book().read();
let component_recipes = default_component_recipe_book().read();
let ability_map = &state.ecs().read_resource::<AbilityMap>();
let msm = state.ecs().read_resource::<MaterialStatManifest>();
@ -707,6 +709,64 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
None
}
},
CraftEvent::ModularWeaponPrimaryComponent {
toolkind,
material,
modifier,
slots,
} => {
let item_id = |slot| inventory.get(slot).map(|item| item.item_definition_id());
if let Some(material_item_id) = item_id(material) {
component_recipes
.get(&ComponentKey {
toolkind,
material: String::from(material_item_id),
modifier: modifier.and_then(item_id).map(String::from),
})
.filter(|r| {
if let Some(needed_sprite) = r.craft_sprite {
let sprite = craft_sprite
.filter(|pos| {
let entity_cylinder = get_cylinder(state, entity);
if !within_pickup_range(entity_cylinder, || {
Some(find_dist::Cube {
min: pos.as_(),
side_length: 1.0,
})
}) {
debug!(
?entity_cylinder,
"Failed to craft recipe as not within range \
of required sprite, sprite pos: {}",
pos
);
false
} else {
true
}
})
.and_then(|pos| state.terrain().get(pos).ok().copied())
.and_then(|block| block.get_sprite());
Some(needed_sprite) == sprite
} else {
true
}
})
.and_then(|r| {
r.craft_component(
&mut inventory,
material,
modifier,
slots,
&state.ecs().read_resource::<AbilityMap>(),
&state.ecs().read_resource::<item::MaterialStatManifest>(),
)
.ok()
})
} else {
None
}
},
};
// Attempt to insert items into inventory, dropping them if there is not enough

View File

@ -75,7 +75,7 @@ use common::{
cmd::ChatCommand,
comp,
event::{EventBus, ServerEvent},
recipe::default_recipe_book,
recipe::{default_component_recipe_book, default_recipe_book},
resources::{BattleMode, Time, TimeOfDay},
rtsim::RtSimEntity,
slowjob::SlowJobPool,
@ -1087,6 +1087,7 @@ impl Server {
client_timeout: self.settings().client_timeout,
world_map: self.map.clone(),
recipe_book: default_recipe_book().cloned(),
component_recipe_book: default_component_recipe_book().cloned(),
material_stats: (&*self
.state
.ecs()

View File

@ -16,14 +16,17 @@ use common::{
assets::AssetExt,
comp::inventory::{
item::{
item_key::ItemKey, modular, modular::ModularComponent, tool::AbilityMap, Item,
ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialStatManifest, Quality,
TagExampleInfo,
item_key::ItemKey,
modular,
modular::ModularComponent,
tool::{AbilityMap, ToolKind},
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialKind,
MaterialStatManifest, Quality, TagExampleInfo,
},
slot::InvSlotId,
Inventory,
},
recipe::{Recipe, RecipeInput},
recipe::{ComponentKey, Recipe, RecipeInput},
terrain::SpriteKind,
};
use conrod_core::{
@ -34,7 +37,7 @@ use conrod_core::{
};
use hashbrown::HashMap;
use i18n::Localization;
use std::{borrow::Cow, sync::Arc};
use std::{collections::BTreeMap, sync::Arc};
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
@ -99,10 +102,16 @@ pub enum Event {
primary_slot: InvSlotId,
secondary_slot: InvSlotId,
},
CraftModularWeaponComponent {
toolkind: ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
},
ChangeCraftingTab(CraftingTab),
Close,
Focus(widget::Id),
SearchRecipe(Option<String>),
ClearRecipeInputs,
}
pub struct CraftingShow {
@ -489,8 +498,7 @@ impl<'a> Widget for Crafting<'a> {
}
};
let (modular_entry_name, modular_entry_recipe) = {
let recipe = Recipe {
let weapon_recipe = Recipe {
output: (
Arc::<ItemDef>::load_expect_cloned("common.items.weapons.empty.empty"),
0,
@ -498,10 +506,53 @@ impl<'a> Widget for Crafting<'a> {
inputs: Vec::new(),
craft_sprite: Some(SpriteKind::CraftingBench),
};
(
let metal_comp_recipe = Recipe {
output: (
Arc::<ItemDef>::load_expect_cloned("common.items.weapons.empty.empty"),
0,
),
inputs: Vec::new(),
craft_sprite: Some(SpriteKind::Anvil),
};
let wood_comp_recipe = Recipe {
output: (
Arc::<ItemDef>::load_expect_cloned("common.items.weapons.empty.empty"),
0,
),
inputs: Vec::new(),
craft_sprite: Some(SpriteKind::CraftingBench),
};
let modular_entries = {
let mut modular_entries = BTreeMap::new();
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon"),
recipe,
)
(&weapon_recipe, "Modular Weapon"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sword"),
(&metal_comp_recipe, "Sword Blade"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.axe"),
(&metal_comp_recipe, "Axe Head"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.hammer"),
(&metal_comp_recipe, "Hammer Head"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.bow"),
(&wood_comp_recipe, "Bow Limbs"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.staff"),
(&wood_comp_recipe, "Staff Shaft"),
);
modular_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sceptre"),
(&wood_comp_recipe, "Sceptre Shaft"),
);
modular_entries
};
// First available recipes, then ones with available materials,
@ -553,14 +604,17 @@ impl<'a> Widget for Crafting<'a> {
})
.chain(
matches!(sel_crafting_tab, CraftingTab::Weapon | CraftingTab::All)
.then_some((
&modular_entry_name,
&modular_entry_recipe,
.then_some(modular_entries.iter().map(|(recipe_name, (recipe, _))| {
(
recipe_name,
*recipe,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
== modular_entry_recipe.craft_sprite,
== recipe.craft_sprite,
true,
))
.into_iter(),
)
}))
.into_iter()
.flatten(),
)
.collect();
ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials)| {
@ -573,9 +627,7 @@ impl<'a> Widget for Crafting<'a> {
});
// Recipe list
// 1 added for modular weapon fake recipe entry
// TODO: Some better automated way of doing this
let recipe_list_length = self.client.recipe_book().iter().len() + 1;
let recipe_list_length = self.client.recipe_book().iter().len() + modular_entries.len();
if state.ids.recipe_list_btns.len() < recipe_list_length {
state.update(|state| {
state
@ -630,12 +682,13 @@ impl<'a> Widget for Crafting<'a> {
.press_image(self.imgs.selection_press)
.image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
let recipe_name = match name.as_str() {
"veloren.core.pseudo_recipe.modular_weapon" => Cow::Borrowed("Modular Weapon"),
_ => recipe.output.0.name(),
let recipe_name = if let Some((_recipe, modular_name)) = modular_entries.get(name) {
*modular_name
} else {
&recipe.output.0.name
};
let text = Text::new(&recipe_name)
let text = Text::new(recipe_name)
.color(if is_craftable {
TEXT_COLOR
} else {
@ -672,6 +725,7 @@ impl<'a> Widget for Crafting<'a> {
}
state.update(|s| s.selected_recipe = Some(name.clone()));
}
events.push(Event::ClearRecipeInputs);
}
// set the text here so that the correct position of the button is retrieved
text.set(state.ids.recipe_list_labels[i], ui);
@ -739,19 +793,26 @@ impl<'a> Widget for Crafting<'a> {
// Selected Recipe
if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
Some("veloren.core.pseudo_recipe.modular_weapon") => {
Some((modular_entry_name.as_str(), &modular_entry_recipe))
},
sel_recipe => {
sel_recipe.and_then(|rn| self.client.recipe_book().get(rn).map(|r| (rn, r)))
Some(selected_recipe) => {
if let Some((modular_recipe, _modular_name)) = modular_entries.get(selected_recipe)
{
Some((selected_recipe, *modular_recipe))
} else {
self.client
.recipe_book()
.get(selected_recipe)
.map(|r| (selected_recipe, r))
}
},
None => None,
} {
let title = match recipe_name {
"veloren.core.pseudo_recipe.modular_weapon" => Cow::Borrowed("Modular Weapon"),
_ => recipe.output.0.name(),
let title = if let Some((_recipe, modular_name)) = modular_entries.get(recipe_name) {
*modular_name
} else {
&recipe.output.0.name
};
// Title
Text::new(&title)
Text::new(title)
.mid_top_with_margin_on(state.ids.align_ing, -22.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
@ -759,8 +820,36 @@ impl<'a> Widget for Crafting<'a> {
.parent(state.ids.window)
.set(state.ids.title_ing, ui);
match recipe_name {
"veloren.core.pseudo_recipe.modular_weapon" => {
if modular_entries.get(recipe_name).is_some() {
#[derive(Clone, Copy, Debug)]
enum RecipeKind {
ModularWeapon,
Component(ToolKind),
}
let recipe_kind = match recipe_name {
"veloren.core.pseudo_recipe.modular_weapon" => Some(RecipeKind::ModularWeapon),
"veloren.core.pseudo_recipe.modular_weapon_component.sword" => {
Some(RecipeKind::Component(ToolKind::Sword))
},
"veloren.core.pseudo_recipe.modular_weapon_component.axe" => {
Some(RecipeKind::Component(ToolKind::Axe))
},
"veloren.core.pseudo_recipe.modular_weapon_component.hammer" => {
Some(RecipeKind::Component(ToolKind::Hammer))
},
"veloren.core.pseudo_recipe.modular_weapon_component.bow" => {
Some(RecipeKind::Component(ToolKind::Bow))
},
"veloren.core.pseudo_recipe.modular_weapon_component.staff" => {
Some(RecipeKind::Component(ToolKind::Staff))
},
"veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
Some(RecipeKind::Component(ToolKind::Sceptre))
},
_ => None,
};
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
@ -793,10 +882,12 @@ impl<'a> Widget for Crafting<'a> {
.mid_top_with_margin_on(state.ids.align_ing, 55.0)
.w_h(168.0, 250.0)
.set(state.ids.modular_art, ui);
let primary_slot = CraftSlot {
index: 0,
invslot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
requirement: |inv, slot| {
requirement: match recipe_kind {
Some(RecipeKind::ModularWeapon) => |inv, slot| {
inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
matches!(
&*item.kind(),
@ -806,7 +897,44 @@ impl<'a> Widget for Crafting<'a> {
)
})
},
required_amount: 1,
// TODO: Maybe try to figure out way to get this to work?
// Captures self and toolkind which prevents it from becoming a function
// Some(RecipeKind::Component(toolkind)) => |inv, slot| {
// inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
// self.client.component_recipe_book().iter().filter(|(key,
// _recipe)| key.toolkind == toolkind).any(|(key, _recipe)| key.material ==
// item.item_definition_id()) })
// },
Some(RecipeKind::Component(
ToolKind::Sword | ToolKind::Axe | ToolKind::Hammer,
)) => |inv, slot| {
inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
matches!(&*item.kind(), ItemKind::Ingredient { .. })
&& item
.tags()
.contains(&ItemTag::MaterialKind(MaterialKind::Metal))
&& item
.tags()
.iter()
.any(|tag| matches!(tag, ItemTag::Material(_)))
})
},
Some(RecipeKind::Component(
ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre,
)) => |inv, slot| {
inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
matches!(&*item.kind(), ItemKind::Ingredient { .. })
&& item
.tags()
.contains(&ItemTag::MaterialKind(MaterialKind::Wood))
&& item
.tags()
.iter()
.any(|tag| matches!(tag, ItemTag::Material(_)))
})
},
Some(RecipeKind::Component(_)) | None => |_, _| false,
},
};
let primary_slot_widget = slot_maker
@ -849,7 +977,8 @@ impl<'a> Widget for Crafting<'a> {
let secondary_slot = CraftSlot {
index: 1,
invslot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
requirement: |inv, slot| {
requirement: match recipe_kind {
Some(RecipeKind::ModularWeapon) => |inv, slot| {
inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
matches!(
&*item.kind(),
@ -859,7 +988,22 @@ impl<'a> Widget for Crafting<'a> {
)
})
},
required_amount: 1,
// TODO: Maybe try to figure out way to get this to work?
// Captures self and toolkind which prevents it from becoming a function
// Some(RecipeKind::Component(toolkind)) => |inv, slot| {
// inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
// self.client.component_recipe_book().iter().filter(|(key,
// _recipe)| key.toolkind == toolkind).any(|(key, _recipe)| key.modifier ==
// Some(item.item_definition_id())) })
// },
Some(RecipeKind::Component(_)) => |inv, slot| {
inv.and_then(|inv| inv.get(slot)).map_or(false, |item| {
item.item_definition_id()
.starts_with("common.items.crafting_ing.animal_misc")
})
},
None => |_, _| false,
},
};
let secondary_slot_widget = slot_maker
@ -898,8 +1042,13 @@ impl<'a> Widget for Crafting<'a> {
// .set(state.ids.modular_inputs[1], ui);
// }
let can_perform =
primary_slot.invslot.is_some() && secondary_slot.invslot.is_some();
let can_perform = match recipe_kind {
Some(RecipeKind::ModularWeapon) => {
primary_slot.invslot.is_some() && secondary_slot.invslot.is_some()
},
Some(RecipeKind::Component(_)) => primary_slot.invslot.is_some(),
None => false,
};
let prim_item_placed = primary_slot.invslot.is_some();
let sec_item_placed = secondary_slot.invslot.is_some();
@ -928,6 +1077,8 @@ impl<'a> Widget for Crafting<'a> {
.set(state.ids.btn_craft, ui)
.was_clicked()
{
match recipe_kind {
Some(RecipeKind::ModularWeapon) => {
if let (Some(primary_slot), Some(secondary_slot)) =
(primary_slot.invslot, secondary_slot.invslot)
{
@ -936,6 +1087,18 @@ impl<'a> Widget for Crafting<'a> {
secondary_slot,
});
}
},
Some(RecipeKind::Component(toolkind)) => {
if let Some(primary_slot) = primary_slot.invslot {
events.push(Event::CraftModularWeaponComponent {
toolkind,
material: primary_slot,
modifier: secondary_slot.invslot,
});
}
},
None => {},
}
}
// Output Image
@ -962,18 +1125,25 @@ impl<'a> Widget for Crafting<'a> {
.set(state.ids.modular_wep_ing_2_bg, ui);
}
if let (Some(primary_comp), Some(secondary_comp)) = (
primary_slot
let ability_map = &AbilityMap::load().read();
let msm = &MaterialStatManifest::load().read();
if let Some(output_item) = match recipe_kind {
Some(RecipeKind::ModularWeapon) => {
if let Some((primary_comp, toolkind)) = primary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.filter(|item| {
matches!(
&*item.kind(),
ItemKind::ModularComponent(
ModularComponent::ToolPrimaryComponent { .. }
)
)
}),
.and_then(|item| {
if let ItemKind::ModularComponent(
ModularComponent::ToolPrimaryComponent { toolkind, .. },
) = &*item.kind()
{
Some((item, *toolkind))
} else {
None
}
})
{
secondary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
@ -981,14 +1151,12 @@ impl<'a> Widget for Crafting<'a> {
matches!(
&*item.kind(),
ItemKind::ModularComponent(
ModularComponent::ToolSecondaryComponent { .. }
ModularComponent::ToolSecondaryComponent { toolkind: toolkind_b, .. }
) if toolkind == *toolkind_b
)
)
}),
) {
let ability_map = &AbilityMap::load().read();
let msm = &MaterialStatManifest::load().read();
let output_item = Item::new_from_item_base(
})
.map(|secondary_comp| {
Item::new_from_item_base(
ItemBase::Modular(modular::ModularBase::Tool),
vec![
primary_comp.duplicate(ability_map, msm),
@ -996,14 +1164,39 @@ impl<'a> Widget for Crafting<'a> {
],
ability_map,
msm,
);
)
})
} else {
None
}
},
Some(RecipeKind::Component(toolkind)) => {
if let Some(material) = primary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.map(|item| String::from(item.item_definition_id()))
{
let component_key = ComponentKey {
toolkind,
material,
modifier: secondary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.map(|item| String::from(item.item_definition_id())),
};
self.client.component_recipe_book().get(&component_key).map(
|component_recipe| component_recipe.item_output(ability_map, msm),
)
} else {
None
}
},
None => None,
} {
Button::image(animate_by_pulse(
&self
.item_imgs
.img_ids_or_not_found_img(ItemKey::ModularWeapon(
modular::weapon_to_key(&output_item),
)),
.img_ids_or_not_found_img(ItemKey::from(&output_item)),
self.pulse,
))
.w_h(55.0, 55.0)
@ -1035,16 +1228,14 @@ impl<'a> Widget for Crafting<'a> {
.graphics_for(state.ids.output_img)
.set(state.ids.modular_wep_empty_bg, ui);
}
},
_ => {
} else {
let can_perform =
self.client
.available_recipes()
.get(recipe_name)
.map_or(false, |cs| {
cs.map_or(true, |cs| {
Some(cs)
== self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
Some(cs) == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
})
});
@ -1131,9 +1322,7 @@ impl<'a> Widget for Crafting<'a> {
CraftingTab::All => false,
_ => crafting_tab.satisfies(recipe),
})
.filter(|crafting_tab| {
crafting_tab != &self.show.crafting_fields.crafting_tab
})
.filter(|crafting_tab| crafting_tab != &self.show.crafting_fields.crafting_tab)
.collect::<Vec<_>>()
.chunks(3)
.enumerate()
@ -1225,8 +1414,7 @@ impl<'a> Widget for Crafting<'a> {
.set(state.ids.req_station_txt, ui);
}
// Ingredients Text
let mut ing_txt =
Text::new(self.localized_strings.get("hud.crafting.ingredients"))
let mut ing_txt = Text::new(self.localized_strings.get("hud.crafting.ingredients"))
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(18))
.color(TEXT_COLOR);
@ -1289,8 +1477,7 @@ impl<'a> Widget for Crafting<'a> {
.slots()
.find_map(|slot| {
slot.as_ref().and_then(|item| {
if item.matches_recipe_input(recipe_input, *amount)
{
if item.matches_recipe_input(recipe_input, *amount) {
Some(item.item_definition_id())
} else {
None
@ -1300,14 +1487,12 @@ impl<'a> Widget for Crafting<'a> {
.unwrap_or_else(|| tag.exemplar_identifier()),
)
},
RecipeInput::ListSameItem(item_defs) => {
Arc::<ItemDef>::load_expect_cloned(
RecipeInput::ListSameItem(item_defs) => Arc::<ItemDef>::load_expect_cloned(
self.inventory
.slots()
.find_map(|slot| {
slot.as_ref().and_then(|item| {
if item.matches_recipe_input(recipe_input, *amount)
{
if item.matches_recipe_input(recipe_input, *amount) {
Some(item.item_definition_id())
} else {
None
@ -1320,8 +1505,7 @@ impl<'a> Widget for Crafting<'a> {
.map(|i| i.item_definition_id())
.unwrap_or("common.items.weapons.empty.empty")
}),
)
},
),
};
// Grey color for images and text if their amount is too low to craft the
@ -1452,7 +1636,6 @@ impl<'a> Widget for Crafting<'a> {
.set(state.ids.ingredients[i], ui);
}
}
},
}
} else if *sel_crafting_tab == CraftingTab::Dismantle {
// Title

View File

@ -538,6 +538,12 @@ pub enum Event {
secondary_slot: InvSlotId,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
},
CraftModularWeaponComponent {
toolkind: ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
},
InviteMember(Uid),
AcceptInvite,
DeclineInvite,
@ -2959,6 +2965,18 @@ impl Hud {
craft_sprite: self.show.crafting_fields.craft_sprite,
});
},
crafting::Event::CraftModularWeaponComponent {
toolkind,
material,
modifier,
} => {
events.push(Event::CraftModularWeaponComponent {
toolkind,
material,
modifier,
craft_sprite: self.show.crafting_fields.craft_sprite,
});
},
crafting::Event::Close => {
self.show.stats = false;
self.show.crafting(false);
@ -2978,6 +2996,9 @@ impl Hud {
crafting::Event::SearchRecipe(search_key) => {
self.show.search_crafting_recipe(search_key);
},
crafting::Event::ClearRecipeInputs => {
self.show.crafting_fields.recipe_inputs.clear();
},
}
}
}

View File

@ -240,13 +240,11 @@ pub struct CraftSlot {
pub index: u32,
pub invslot: Option<InvSlotId>,
pub requirement: fn(Option<&Inventory>, InvSlotId) -> bool,
pub required_amount: u32,
}
impl PartialEq for CraftSlot {
fn eq(&self, other: &Self) -> bool {
(self.index, self.invslot, self.required_amount)
== (other.index, other.invslot, other.required_amount)
(self.index, self.invslot) == (other.index, other.invslot)
}
}
@ -266,7 +264,7 @@ impl SlotKey<Inventory, ItemImgs> for CraftSlot {
fn amount(&self, source: &Inventory) -> Option<u32> {
self.invslot
.and_then(|invslot| source.get(invslot))
.map(|item| self.required_amount.min(item.amount()))
.map(|item| item.amount())
.filter(|amount| *amount > 1)
}

View File

@ -25,6 +25,7 @@ use common::{
link::Is,
mounting::Mount,
outcome::Outcome,
recipe,
terrain::{Block, BlockKind},
trade::TradeResult,
util::{Dir, Plane},
@ -1431,6 +1432,48 @@ impl PlayState for SessionState {
craft_sprite.map(|(pos, _sprite)| pos),
);
},
HudEvent::CraftModularWeaponComponent {
toolkind,
material,
modifier,
craft_sprite,
} => {
let additional_slots = {
let client = self.client.borrow();
let item_id = |slot| {
client
.inventories()
.get(client.entity())
.and_then(|inv| inv.get(slot))
.map(|item| String::from(item.item_definition_id()))
};
if let Some(material_id) = item_id(material) {
let key = recipe::ComponentKey {
toolkind,
material: material_id,
modifier: modifier.and_then(item_id),
};
if let Some(recipe) = client.component_recipe_book().get(&key) {
client.inventories().get(client.entity()).and_then(|inv| {
recipe.inventory_contains_additional_ingredients(inv).ok()
})
} else {
None
}
} else {
None
}
};
if let Some(additional_slots) = additional_slots {
self.client.borrow_mut().craft_modular_weapon_component(
toolkind,
material,
modifier,
additional_slots,
craft_sprite.map(|(pos, _sprite)| pos),
);
}
},
HudEvent::SalvageItem { slot, salvage_pos } => {
self.client.borrow_mut().salvage_item(slot, salvage_pos);
},