Changed crafting to only consume items after checking that the crafting would be successful instead of consuming items first and reinserting on failure.

This commit is contained in:
Sam 2021-10-25 15:34:39 -04:00
parent 4b9e9c506b
commit fbd742abdb
4 changed files with 100 additions and 96 deletions

View File

@ -998,7 +998,7 @@ impl Client {
pub fn craft_recipe( pub fn craft_recipe(
&mut self, &mut self,
recipe: &str, recipe: &str,
slots: Vec<InvSlotId>, slots: Vec<(u32, InvSlotId)>,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>, craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
) -> bool { ) -> bool {
let (can_craft, required_sprite) = self.can_craft_recipe(recipe); let (can_craft, required_sprite) = self.can_craft_recipe(recipe);
@ -1019,6 +1019,7 @@ impl Client {
} }
} }
/// Checks if the item in the given slot can be salvaged.
pub fn can_salvage_item(&self, slot: InvSlotId) -> bool { pub fn can_salvage_item(&self, slot: InvSlotId) -> bool {
self.inventories() self.inventories()
.get(self.entity()) .get(self.entity())
@ -1026,6 +1027,8 @@ impl Client {
.map_or(false, |item| item.is_salvageable()) .map_or(false, |item| item.is_salvageable())
} }
/// Salvage the item in the given inventory slot. `salvage_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3<i32>) -> bool { pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3<i32>) -> bool {
let is_salvageable = self.can_salvage_item(slot); let is_salvageable = self.can_salvage_item(slot);
if is_salvageable { if is_salvageable {

View File

@ -94,7 +94,7 @@ impl From<InventoryEvent> for InventoryManip {
pub enum CraftEvent { pub enum CraftEvent {
Simple { Simple {
recipe: String, recipe: String,
slots: Vec<InvSlotId>, slots: Vec<(u32, InvSlotId)>,
}, },
Salvage(InvSlotId), Salvage(InvSlotId),
} }

View File

@ -171,38 +171,38 @@ impl Material {
} }
} }
pub fn asset_identifier(&self) -> &'static str { pub fn asset_identifier(&self) -> Option<&'static str> {
match self { match self {
Material::Bronze => "common.items.mineral.ingot.bronze", Material::Bronze => Some("common.items.mineral.ingot.bronze"),
Material::Iron => "common.items.mineral.ingot.iron", Material::Iron => Some("common.items.mineral.ingot.iron"),
Material::Steel => "common.items.mineral.ingot.steel", Material::Steel => Some("common.items.mineral.ingot.steel"),
Material::Cobalt => "common.items.mineral.ingot.cobalt", Material::Cobalt => Some("common.items.mineral.ingot.cobalt"),
Material::Bloodsteel => "common.items.mineral.ingot.bloodsteel", Material::Bloodsteel => Some("common.items.mineral.ingot.bloodsteel"),
Material::Orichalcum => "common.items.mineral.ingot.orichalcum", Material::Orichalcum => Some("common.items.mineral.ingot.orichalcum"),
Material::Wood Material::Wood
| Material::Bamboo | Material::Bamboo
| Material::Hardwood | Material::Hardwood
| Material::Ironwood | Material::Ironwood
| Material::Frostwood | Material::Frostwood
| Material::Eldwood => unimplemented!(), | Material::Eldwood => None,
Material::Rock Material::Rock
| Material::Granite | Material::Granite
| Material::Bone | Material::Bone
| Material::Basalt | Material::Basalt
| Material::Obsidian | Material::Obsidian
| Material::Velorite => unimplemented!(), | Material::Velorite => None,
Material::Linen => "common.items.crafting_ing.cloth.linen", Material::Linen => Some("common.items.crafting_ing.cloth.linen"),
Material::Wool => "common.items.crafting_ing.cloth.wool", Material::Wool => Some("common.items.crafting_ing.cloth.wool"),
Material::Silk => "common.items.crafting_ing.cloth.silk", Material::Silk => Some("common.items.crafting_ing.cloth.silk"),
Material::Lifecloth => "common.items.crafting_ing.cloth.lifecloth", Material::Lifecloth => Some("common.items.crafting_ing.cloth.lifecloth"),
Material::Moonweave => "common.items.crafting_ing.cloth.moonweave", Material::Moonweave => Some("common.items.crafting_ing.cloth.moonweave"),
Material::Sunsilk => "common.items.crafting_ing.cloth.sunsilk", Material::Sunsilk => Some("common.items.crafting_ing.cloth.sunsilk"),
Material::Rawhide => "common.items.crafting_ing.leather.simple_leather", Material::Rawhide => Some("common.items.crafting_ing.leather.simple_leather"),
Material::Leather => "common.items.crafting_ing.leather.thick_leather", Material::Leather => Some("common.items.crafting_ing.leather.thick_leather"),
Material::Scale => "common.items.crafting_ing.hide.scales", Material::Scale => Some("common.items.crafting_ing.hide.scales"),
Material::Carapace => "common.items.crafting_ing.hide.carapace", Material::Carapace => Some("common.items.crafting_ing.hide.carapace"),
Material::Plate => "common.items.crafting_ing.hide.plate", Material::Plate => Some("common.items.crafting_ing.hide.plate"),
Material::Dragonscale => "common.items.crafting_ing.hide.dragon_scale", Material::Dragonscale => Some("common.items.crafting_ing.hide.dragon_scale"),
} }
} }
} }
@ -815,27 +815,7 @@ impl Item {
None None
} }
}) })
.map(|material| material.asset_identifier()) .filter_map(|material| material.asset_identifier())
}
// Attempts to salvage an item by consuming it, returns the salvaged items if
// salvageable, else the original item
pub fn try_salvage(self) -> Result<Vec<Item>, Item> {
if !self.is_salvageable() {
return Err(self);
}
// Creates one item for every salvage tag in the target item
let salvaged_items: Vec<_> = self
.salvage_output()
.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 name(&self) -> &str { &self.item_def.name }

View File

@ -30,50 +30,65 @@ impl Recipe {
pub fn craft_simple( pub fn craft_simple(
&self, &self,
inv: &mut Inventory, inv: &mut Inventory,
slots: Vec<InvSlotId>, // Vec tying an input to a slot
slots: Vec<(u32, InvSlotId)>,
ability_map: &AbilityMap, ability_map: &AbilityMap,
msm: &MaterialStatManifest, msm: &MaterialStatManifest,
) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> { ) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
let mut recipe_inputs = Vec::new(); let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new(); let mut unsatisfied_requirements = Vec::new();
// Checks each input against a slot in the inventory. If the slot contains an // Checks each input against slots in the inventory. If the slots contain an
// item that fulfills the need of the input, takes from the inventory up to the // item that fulfills the need of the input, marks some of the item as claimed
// quantity needed for the crafting input. If the item either cannot be used, or // up to quantity needed for the crafting input. If the item either
// there is insufficient quantity, adds input and number of materials needed to // cannot be used, or there is insufficient quantity, adds input and
// unsatisfied requirements. // number of materials needed to unsatisfied requirements.
self.inputs self.inputs
.iter() .iter()
.enumerate() .enumerate()
.for_each(|(i, (input, amount))| { .for_each(|(i, (input, mut required))| {
let valid_input = if let Some(item) = slots.get(i).and_then(|slot| inv.get(*slot)) { // Check used for recipes that have an input that is not consumed, e.g.
item.matches_recipe_input(input) // craftsman hammer
} else { let mut contains_any = false;
false // Gets all slots provided for this input by the frontend
}; let input_slots = slots
.iter()
if let Some(slot) = slots.get(i) { .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None });
if !valid_input { // Goes through each slot and marks some amount from each slot as claimed
unsatisfied_requirements.push((input, *amount)); for slot in input_slots {
} else { // Checks that the item in the slot can be used for the input
for taken in 0..*amount { if let Some(item) = inv
if let Some(item) = inv.take(*slot, ability_map, msm) { .get(*slot)
recipe_inputs.push(item); .filter(|item| item.matches_recipe_input(input))
} else { {
unsatisfied_requirements.push((input, *amount - taken)); // Gets the number of items claimed from the slot, or sets to 0 if slot has
break; // 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;
} }
} else { }
unsatisfied_requirements.push((input, *amount)); // 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));
} }
}); });
// If there are no unsatisfied requirements, create the items produced by the // If there are no unsatisfied requirements, create the items produced by the
// recipe in the necessary quantity, else insert the ingredients back into the // recipe in the necessary quantity and remove the items that the recipe
// inventory // consumes
if unsatisfied_requirements.is_empty() { 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 (item_def, quantity) = &self.output; let (item_def, quantity) = &self.output;
let crafted_item = Item::new_from_item_def(Arc::clone(item_def), &[], ability_map, msm); let crafted_item = Item::new_from_item_def(Arc::clone(item_def), &[], ability_map, msm);
let mut crafted_items = Vec::with_capacity(*quantity as usize); let mut crafted_items = Vec::with_capacity(*quantity as usize);
@ -82,10 +97,6 @@ impl Recipe {
} }
Ok(crafted_items) Ok(crafted_items)
} else { } else {
for item in recipe_inputs {
inv.push(item)
.expect("Item was in inventory before craft attempt");
}
Err(unsatisfied_requirements) Err(unsatisfied_requirements)
} }
} }
@ -104,26 +115,28 @@ impl Recipe {
pub fn inventory_contains_ingredients<'a>( pub fn inventory_contains_ingredients<'a>(
&self, &self,
inv: &'a Inventory, inv: &'a Inventory,
) -> Result<Vec<InvSlotId>, Vec<(&RecipeInput, u32)>> { ) -> 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(); let mut slot_claims = HashMap::<InvSlotId, u32>::new();
// Important to be a vec and to remain separate from slot_claims as it must // Important to be a vec and to remain separate from slot_claims as it must
// remain ordered, unlike the hashmap // remain ordered, unlike the hashmap
let mut slots = Vec::<InvSlotId>::new(); 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(); let mut missing = Vec::<(&RecipeInput, u32)>::new();
for (input, mut needed) in self.inputs() { for (i, (input, mut needed)) in self.inputs().enumerate() {
let mut contains_any = false; 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() { for (inv_slot_id, slot) in inv.slots_with_id() {
if let Some(item) = slot if let Some(item) = slot
.as_ref() .as_ref()
.filter(|item| item.matches_recipe_input(&*input)) .filter(|item| item.matches_recipe_input(&*input))
{ {
let claim = slot_claims.entry(inv_slot_id).or_insert(0); let claim = slot_claims.entry(inv_slot_id).or_insert(0);
slots.push(inv_slot_id); slots.push((i as u32, inv_slot_id));
// FIXME: Fishy, looks like it can underflow before min which can trigger an let can_claim = (item.amount().saturating_sub(*claim)).min(needed);
// overflow check.
let can_claim = (item.amount() - *claim).min(needed);
*claim += can_claim; *claim += can_claim;
needed -= can_claim; needed -= can_claim;
contains_any = true; contains_any = true;
@ -154,16 +167,24 @@ pub fn try_salvage(
msm: &MaterialStatManifest, msm: &MaterialStatManifest,
) -> Result<Vec<Item>, SalvageError> { ) -> Result<Vec<Item>, SalvageError> {
if inv.get(slot).map_or(false, |item| item.is_salvageable()) { if inv.get(slot).map_or(false, |item| item.is_salvageable()) {
let salvage_item = inv let salvage_item = inv.get(slot).expect("Expected item to exist in inventory");
.take(slot, ability_map, msm) let salvage_output: Vec<_> = salvage_item
.expect("Expected item to exist in inventory"); .salvage_output()
match salvage_item.try_salvage() { .map(|asset| Item::new_from_asset_expect(asset))
Ok(items) => Ok(items), .collect();
Err(item) => { if salvage_output.is_empty() {
inv.push(item) // If no output items, assume salvaging was a failure
.expect("Item taken from inventory just before"); // TODO: If we ever change salvaging to have a percent chance, remove the check
Err(SalvageError::NotSalvageable) // of outputs being empty (requires assets to exist for rock and wood materials
}, // so that salvaging doesn't silently fail)
Err(SalvageError::NotSalvageable)
} else {
// Remove item that is being salvaged
let _ = inv
.take(slot, ability_map, msm)
.expect("Expected item to exist in inventory");
// Return the salvaging output
Ok(salvage_output)
} }
} else { } else {
Err(SalvageError::NotSalvageable) Err(SalvageError::NotSalvageable)