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,54 +156,67 @@ impl Recipe {
.map(|(item_def, amount, is_mod_comp)| (item_def, *amount, *is_mod_comp))
}
/// 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.
/// 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)>> {
// 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();
// Important to be a vec and to remain separate from slot_claims as it must
// remain ordered, unlike the hashmap
let mut slots = Vec::<(u32, InvSlotId)>::new();
// The inputs to a recipe that have missing items, and the amount missing
let mut missing = Vec::<(&RecipeInput, u32)>::new();
inventory_contains_ingredients(
self.inputs()
.map(|(input, amount, _is_modular)| (input, amount)),
inv,
)
}
}
for (i, (input, amount, _)) in self.inputs().enumerate() {
let mut needed = amount;
let mut contains_any = false;
// 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))
{
let claim = slot_claims.entry(inv_slot_id).or_insert(0);
slots.push((i as u32, inv_slot_id));
let can_claim = (item.amount().saturating_sub(*claim)).min(needed);
*claim += can_claim;
needed -= can_claim;
contains_any = true;
}
}
/// 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.
#[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();
// Important to be a vec and to remain separate from slot_claims as it must
// remain ordered, unlike the hashmap
let mut slots = Vec::<(u32, InvSlotId)>::new();
// The inputs to a recipe that have missing items, and the amount missing
let mut missing = Vec::<(&RecipeInput, u32)>::new();
if needed > 0 || !contains_any {
missing.push((input, needed));
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
// 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))
{
let claim = slot_claims.entry(inv_slot_id).or_insert(0);
slots.push((i as u32, inv_slot_id));
let can_claim = (item.amount().saturating_sub(*claim)).min(needed);
*claim += can_claim;
needed -= can_claim;
contains_any = true;
}
}
if missing.is_empty() {
Ok(slots)
} else {
Err(missing)
if needed > 0 || !contains_any {
missing.push((input, needed));
}
}
if missing.is_empty() {
Ok(slots)
} else {
Err(missing)
}
}
pub enum SalvageError {
@ -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()

File diff suppressed because it is too large Load Diff

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);
},