diff --git a/assets/common/recipe_book.ron b/assets/common/recipe_book.ron index cb49dea6fd..68b4898466 100644 --- a/assets/common/recipe_book.ron +++ b/assets/common/recipe_book.ron @@ -1927,15 +1927,15 @@ craft_sprite: Some(Anvil), is_recycling: false, ), - "linen": ( - output: ("common.items.crafting_ing.cloth.linen", 1), - inputs: [ - (Tag(Material(Linen)), 1), - (Item("common.items.crafting_tools.sewing_set"), 0), - ], - craft_sprite: None, - is_recycling: true, - ), + // "linen": ( + // output: ("common.items.crafting_ing.cloth.linen", 1), + // inputs: [ + // (Tag(Material(Linen)), 1), + // (Item("common.items.crafting_tools.sewing_set"), 0), + // ], + // craft_sprite: None, + // is_recycling: true, + // ), "wool": ( output: ("common.items.crafting_ing.cloth.wool", 1), inputs: [ diff --git a/client/src/lib.rs b/client/src/lib.rs index bdbcf94da0..7ddc91b0a6 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,6 +23,7 @@ use common::{ comp::{ self, chat::{KillSource, KillType}, + controller::CraftEvent, group, invite::{InviteKind, InviteResponse}, skills::Skill, @@ -1004,7 +1005,7 @@ impl Client { if can_craft && has_sprite { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent( InventoryEvent::CraftRecipe { - recipe: recipe.to_string(), + craft_event: CraftEvent::Simple(recipe.to_string()), craft_sprite: craft_sprite.map(|(pos, _)| pos), }, ))); diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index a3b9ca9076..61e2af20ab 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -23,7 +23,7 @@ pub enum InventoryEvent { SplitDrop(InvSlotId), Sort, CraftRecipe { - recipe: String, + craft_event: CraftEvent, craft_sprite: Option>, }, } @@ -48,7 +48,7 @@ pub enum InventoryManip { SplitDrop(Slot), Sort, CraftRecipe { - recipe: String, + craft_event: CraftEvent, craft_sprite: Option>, }, SwapEquippedWeapons, @@ -80,16 +80,22 @@ impl From for InventoryManip { InventoryEvent::SplitDrop(inv) => Self::SplitDrop(Slot::Inventory(inv)), InventoryEvent::Sort => Self::Sort, InventoryEvent::CraftRecipe { - recipe, + craft_event, craft_sprite, } => Self::CraftRecipe { - recipe, + craft_event, craft_sprite, }, } } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum CraftEvent { + Simple(String), + Salvage(InvSlotId), +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum GroupManip { Leave, diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 2eec60db2e..53d3adde18 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -170,6 +170,41 @@ impl Material { | Material::Dragonscale => MaterialKind::Hide, } } + + pub fn asset_identifier(&self) -> &'static str { + match self { + Material::Bronze => "common.items.mineral.ingot.bronze", + Material::Iron => "common.items.mineral.ingot.iron", + Material::Steel => "common.items.mineral.ingot.steel", + Material::Cobalt => "common.items.mineral.ingot.cobalt", + Material::Bloodsteel => "common.items.mineral.ingot.bloodsteel", + Material::Orichalcum => "common.items.mineral.ingot.orichalcum", + Material::Wood + | Material::Bamboo + | Material::Hardwood + | Material::Ironwood + | Material::Frostwood + | Material::Eldwood => unreachable!(), + Material::Rock + | Material::Granite + | Material::Bone + | Material::Basalt + | Material::Obsidian + | Material::Velorite => unreachable!(), + Material::Linen => "common.items.crafting_ing.cloth.linen", + Material::Wool => "common.items.crafting_ing.cloth.wool", + Material::Silk => "common.items.crafting_ing.cloth.silk", + Material::Lifecloth => "common.items.crafting_ing.cloth.lifecloth", + Material::Moonweave => "common.items.crafting_ing.cloth.moonweave", + Material::Sunsilk => "common.items.crafting_ing.cloth.sunsilk", + Material::Rawhide => "common.items.crafting_ing.leather.simple_leather", + Material::Leather => "common.items.crafting_ing.leather.thick_leather", + Material::Scale => "common.items.crafting_ing.leather.scales", + Material::Carapace => "common.items.crafting_ing.leather.carapace", + Material::Plate => "common.items.crafting_ing.leather.plate", + Material::Dragonscale => "common.items.crafting_ing.leather.dragon_scale", + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -202,6 +237,7 @@ pub enum ItemTag { CraftingTool, // Pickaxe, Craftsman-Hammer, Sewing-Set Utility, Bag, + SalvageInto(Material), } impl TagExampleInfo for ItemTag { @@ -219,6 +255,7 @@ impl TagExampleInfo for ItemTag { ItemTag::CraftingTool => "tool", ItemTag::Utility => "utility", ItemTag::Bag => "bag", + ItemTag::SalvageInto(_) => "salvage", } } @@ -237,6 +274,7 @@ impl TagExampleInfo for ItemTag { ItemTag::CraftingTool => "common.items.tag_examples.placeholder", ItemTag::Utility => "common.items.tag_examples.placeholder", ItemTag::Bag => "common.items.tag_examples.placeholder", + ItemTag::SalvageInto(_) => "common.items.tag_examples.placeholder", } } } @@ -759,6 +797,43 @@ impl Item { } } + pub fn is_salvageable(&self) -> bool { + self.item_def + .tags + .iter() + .any(|tag| matches!(tag, ItemTag::SalvageInto(_))) + } + + // Attempts to salvage an item, returning the salvaged items if salvageable, + // else the original item is not Theoretically supports returning multiple + // items, only returns one per tag in the item for now + pub fn try_salvage(self) -> Result, Item> { + if !self.is_salvageable() { + return Err(self); + } + + let salvaged_items: Vec<_> = self + .item_def + .tags + .iter() + .filter_map(|tag| { + if let ItemTag::SalvageInto(material) = tag { + Some(material) + } else { + None + } + }) + .map(|material| material.asset_identifier()) + .map(|asset| Item::new_from_asset_expect(asset)) + .collect(); + + if salvaged_items.is_empty() { + Err(self) + } else { + Ok(salvaged_items) + } + } + pub fn name(&self) -> &str { &self.item_def.name } pub fn description(&self) -> &str { &self.item_def.description } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 677b5c17e2..4d6411722c 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -13,7 +13,7 @@ pub mod character_state; #[cfg(not(target_arch = "wasm32"))] pub mod combo; pub mod compass; #[cfg(not(target_arch = "wasm32"))] -mod controller; +pub mod controller; #[cfg(not(target_arch = "wasm32"))] pub mod dialogue; #[cfg(not(target_arch = "wasm32"))] mod energy; diff --git a/common/src/recipe.rs b/common/src/recipe.rs index 8130e52ea0..821272b28d 100644 --- a/common/src/recipe.rs +++ b/common/src/recipe.rs @@ -1,6 +1,7 @@ use crate::{ assets::{self, AssetExt, AssetHandle}, comp::{ + inventory::slot::InvSlotId, item::{modular, tool::AbilityMap, ItemDef, ItemTag, MaterialStatManifest}, Inventory, Item, }, @@ -32,7 +33,7 @@ impl Recipe { inv: &mut Inventory, ability_map: &AbilityMap, msm: &MaterialStatManifest, - ) -> Result, Vec<(&RecipeInput, u32)>> { + ) -> Result<(Item, u32), Vec<(&RecipeInput, u32)>> { // Get ingredient cells from inventory, let mut components = Vec::new(); @@ -47,15 +48,18 @@ impl Recipe { }) }); - for i in 0..self.output.1 { - let crafted_item = - Item::new_from_item_def(Arc::clone(&self.output.0), &components, ability_map, msm); - if let Err(item) = inv.push(crafted_item) { - return Ok(Some((item, self.output.1 - i))); - } - } + let crafted_item = + Item::new_from_item_def(Arc::clone(&self.output.0), &components, ability_map, msm); - Ok(None) + Ok((crafted_item, self.output.1)) + + // for i in 0..self.output.1 { + // if let Err(item) = inv.push(crafted_item) { + // return Ok(Some((item, self.output.1 - i))); + // } + // } + + // Ok(None) } pub fn inputs(&self) -> impl ExactSizeIterator { @@ -65,6 +69,33 @@ impl Recipe { } } +pub enum SalvageError { + NotSalvageable, +} + +pub fn try_salvage( + inv: &mut Inventory, + slot: InvSlotId, + ability_map: &AbilityMap, + msm: &MaterialStatManifest, +) -> Result, SalvageError> { + if inv.get(slot).map_or(false, |item| item.is_salvageable()) { + let salvage_item = inv + .take(slot, ability_map, msm) + .expect("Expected item to exist in inventory"); + match salvage_item.try_salvage() { + Ok(items) => Ok(items), + Err(item) => { + inv.push(item) + .expect("Item taken from inventory just before"); + Err(SalvageError::NotSalvageable) + }, + } + } else { + Err(SalvageError::NotSalvageable) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RecipeBook { recipes: HashMap, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index fb27e9354f..b2843f6322 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -11,7 +11,7 @@ use common::{ slot::{self, Slot}, }, consts::MAX_PICKUP_RANGE, - recipe::default_recipe_book, + recipe::{self, default_recipe_book}, trade::Trades, uid::Uid, util::find_dist::{self, FindDist}, @@ -563,53 +563,91 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv drop(inventories); }, comp::InventoryManip::CraftRecipe { - recipe, + craft_event, craft_sprite, } => { + use comp::controller::CraftEvent; let recipe_book = default_recipe_book().read(); - let craft_result = recipe_book - .get(&recipe) - .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 + let ability_map = &state.ecs().read_resource::(); + let msm = state.ecs().read_resource::(); + + let crafted_items = match craft_event { + CraftEvent::Simple(recipe) => recipe_book + .get(&recipe) + .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.perform( + &mut inventory, + &state.ecs().read_resource::(), + &state.ecs().read_resource::(), + ) + .ok() + }) + .map(|(crafted_item, amount)| { + let mut crafted_items = Vec::with_capacity(amount as usize); + for _ in 0..amount { + crafted_items.push(crafted_item.duplicate(ability_map, &msm)); + } + crafted_items + }), + CraftEvent::Salvage(slot) => { + recipe::try_salvage(&mut inventory, slot, ability_map, &msm).ok() + }, + }; + + // Attempt to insert items into inventory, dropping them if there is not enough + // space + let items_were_crafted = if let Some(crafted_items) = crafted_items { + for item in crafted_items { + if let Err(item) = inventory.push(item) { + dropped_items.push(( + state + .read_component_copied::(entity) + .unwrap_or_default(), + state + .read_component_copied::(entity) + .unwrap_or_default(), + item.duplicate(ability_map, &msm), + )); } - }) - .and_then(|r| { - r.perform( - &mut inventory, - &state.ecs().read_resource::(), - &state.ecs().read_resource::(), - ) - .ok() - }); + } + true + } else { + false + }; + drop(inventories); // FIXME: We should really require the drop and write to be atomic! - if craft_result.is_some() { + if items_were_crafted { let _ = state.ecs().write_storage().insert( entity, comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Craft), @@ -617,21 +655,22 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } // Drop the item if there wasn't enough space - if let Some(Some((item, amount))) = craft_result { - let ability_map = &state.ecs().read_resource::(); - let msm = state.ecs().read_resource::(); - for _ in 0..amount { - dropped_items.push(( - state - .read_component_copied::(entity) - .unwrap_or_default(), - state - .read_component_copied::(entity) - .unwrap_or_default(), - item.duplicate(ability_map, &msm), - )); - } - } + // if let Some(Some((item, amount))) = craft_result { + // let ability_map = &state.ecs().read_resource::(); + // let msm = + // state.ecs().read_resource::(); + // for _ in 0..amount { + // dropped_items.push(( + // state + // .read_component_copied::(entity) + // .unwrap_or_default(), + // state + // .read_component_copied::(entity) + // .unwrap_or_default(), + // item.duplicate(ability_map, &msm), + // )); + // } + // } }, comp::InventoryManip::Sort => { inventory.sort();