Added repair recipes

This commit is contained in:
Sam 2022-07-17 15:33:14 -04:00
parent c3f5bc13f1
commit 0966753699
9 changed files with 383 additions and 62 deletions

View File

@ -0,0 +1,79 @@
(
recipes: {
// Weapons
ItemDefId("common.items.weapons.sword.starter"): ( inputs: [], ),
ItemDefId("common.items.weapons.axe.starter_axe"): ( inputs: [], ),
ItemDefId("common.items.weapons.hammer.starter_hammer"): ( inputs: [], ),
ItemDefId("common.items.weapons.bow.starter"): ( inputs: [], ),
ItemDefId("common.items.weapons.staff.starter_staff"): ( inputs: [], ),
ItemDefId("common.items.weapons.sceptre.starter_sceptre"): ( inputs: [], ),
ItemDefId("common.items.weapons.sword_1h.starter"): ( inputs: [], ),
ModularWeapon(material: "common.items.mineral.ingot.bronze"): (
inputs: [
(Item("common.items.mineral.ingot.bronze"), 1),
],
),
ModularWeapon(material: "common.items.mineral.ingot.iron"): (
inputs: [
(Item("common.items.mineral.ingot.iron"), 1),
],
),
ModularWeapon(material: "common.items.mineral.ingot.steel"): (
inputs: [
(Item("common.items.mineral.ingot.steel"), 1),
],
),
ModularWeapon(material: "common.items.mineral.ingot.cobalt"): (
inputs: [
(Item("common.items.mineral.ingot.cobalt"), 1),
],
),
ModularWeapon(material: "common.items.mineral.ingot.bloodsteel"): (
inputs: [
(Item("common.items.mineral.ingot.bloodsteel"), 1),
],
),
ModularWeapon(material: "common.items.mineral.ingot.orichalcum"): (
inputs: [
(Item("common.items.mineral.ingot.orichalcum"), 1),
],
),
ModularWeapon(material: "common.items.log.wood"): (
inputs: [
(Item("common.items.log.wood"), 1),
],
),
ModularWeapon(material: "common.items.log.bamboo"): (
inputs: [
(Item("common.items.log.bamboo"), 1),
],
),
ModularWeapon(material: "common.items.log.hardwood"): (
inputs: [
(Item("common.items.log.hardwood"), 1),
],
),
ModularWeapon(material: "common.items.log.ironwood"): (
inputs: [
(Item("common.items.log.ironwood"), 1),
],
),
ModularWeapon(material: "common.items.log.frostwood"): (
inputs: [
(Item("common.items.log.frostwood"), 1),
],
),
ModularWeapon(material: "common.items.log.eldwood"): (
inputs: [
(Item("common.items.log.eldwood"), 1),
],
),
// Armor
},
fallback: (
inputs: [
(Item("common.items.utility.coins"), 1000),
],
),
)

View File

@ -38,7 +38,7 @@ use common::{
lod,
mounting::Rider,
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook},
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
shared_server_config::ServerConstants,
spiral::Spiral2d,
@ -222,6 +222,7 @@ pub struct Client {
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>,
@ -362,6 +363,7 @@ impl Client {
material_stats,
ability_map,
server_constants,
repair_recipe_book,
} = loop {
tokio::select! {
// Spawn in a blocking thread (leaving the network thread free). This is mostly
@ -655,6 +657,7 @@ impl Client {
world_map.pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
))
@ -670,6 +673,7 @@ impl Client {
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
) = loop {
@ -708,6 +712,7 @@ impl Client {
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
available_recipes: HashMap::default(),
chat_mode: ChatMode::default(),
@ -1146,6 +1151,8 @@ impl Client {
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
pub fn repair_recipe_book(&self) -> &RepairRecipeBook { &self.repair_recipe_book }
pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
&self.available_recipes
}
@ -1291,12 +1298,17 @@ impl Client {
/// Repairs the item in the given inventory slot. `sprite_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn repair_item(&mut self, slot: Slot, sprite_pos: Vec3<i32>) -> bool {
pub fn repair_item(
&mut self,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Vec3<i32>,
) -> bool {
let is_repairable = {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
inventory.map_or(false, |inv| {
if let Some(item) = match slot {
if let Some(item) = match item {
Slot::Equip(equip_slot) => inv.equipped(equip_slot),
Slot::Inventory(invslot) => inv.get(invslot),
} {
@ -1309,7 +1321,7 @@ impl Client {
if is_repairable {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::Repair(slot),
craft_event: CraftEvent::Repair { item, slots },
craft_sprite: Some(sprite_pos),
},
)));

View File

@ -10,7 +10,7 @@ use common::{
event::UpdateCharacterMetadata,
lod,
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook},
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{Time, TimeOfDay},
shared_server_config::ServerConstants,
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
@ -65,6 +65,7 @@ pub enum ServerInit {
world_map: crate::msg::world_msg::WorldMapMsg,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap,
server_constants: ServerConstants,

View File

@ -105,7 +105,10 @@ pub enum CraftEvent {
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
},
Repair(Slot),
Repair {
item: Slot,
slots: Vec<(u32, InvSlotId)>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,11 +1,12 @@
use crate::{
assets::{self, AssetExt, AssetHandle},
comp::{
inventory::slot::InvSlotId,
inventory::slot::{InvSlotId, Slot},
item::{
modular,
tool::{AbilityMap, ToolKind},
ItemBase, ItemDef, ItemDefinitionIdOwned, ItemKind, ItemTag, MaterialStatManifest,
ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
MaterialStatManifest,
},
Inventory, Item,
},
@ -39,6 +40,45 @@ pub enum RecipeInput {
ListSameItem(Vec<Arc<ItemDef>>),
}
impl RecipeInput {
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
&'a self,
amount: u32,
slot_claims: &mut HashMap<InvSlotId, u32>,
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
inv: &Inventory,
input_slots: I,
) {
let mut required = amount;
// contains_any check used for recipes that have an input that is not consumed,
// e.g. craftsman hammer
// Goes through each slot and marks some amount from each slot as claimed
let contains_any = input_slots.into_iter().all(|slot| {
// 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(self, 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;
true
} else {
false
}
});
// 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((self, required));
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Recipe {
pub output: (Arc<ItemDef>, u32),
@ -594,65 +634,26 @@ impl ComponentRecipe {
let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new();
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
slot_claims: &mut HashMap<InvSlotId, u32>,
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
inv: &Inventory,
input: &'a RecipeInput,
amount: u32,
input_slots: I,
) {
let mut required = amount;
// contains_any check used for recipes that have an input that is not consumed,
// e.g. craftsman hammer
// Goes through each slot and marks some amount from each slot as claimed
let contains_any = input_slots.into_iter().all(|slot| {
// 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;
true
} else {
false
}
});
// 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(
self.material.0.handle_requirement(
self.material.1,
&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(
modifier_input.handle_requirement(
*modifier_amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
modifier_input,
*modifier_amount,
core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
);
}
@ -666,12 +667,11 @@ impl ComponentRecipe {
.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(
input.handle_requirement(
*amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
input,
*amount,
input_slots,
);
});
@ -900,6 +900,199 @@ impl assets::Compound for ComponentRecipeBook {
}
}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
enum RepairKey {
ItemDefId(String),
ModularWeapon { material: String },
}
impl RepairKey {
fn from_item(item: &Item) -> Option<Self> {
match item.item_definition_id() {
ItemDefinitionId::Simple(item_id) => Some(Self::ItemDefId(String::from(item_id))),
ItemDefinitionId::Compound { .. } => None,
ItemDefinitionId::Modular { pseudo_base, .. } => match pseudo_base {
"veloren.core.pseudo_items.modular.tool" => {
if let Some(ItemDefinitionId::Simple(material)) = item
.components()
.iter()
.find(|comp| {
matches!(
&*comp.kind(),
ItemKind::ModularComponent(
modular::ModularComponent::ToolPrimaryComponent { .. }
)
)
})
.and_then(|comp| {
comp.components()
.iter()
.next()
.map(|comp| comp.item_definition_id())
})
{
let material = String::from(material);
Some(Self::ModularWeapon { material })
} else {
None
}
},
_ => None,
},
}
}
}
#[derive(Serialize, Deserialize, Clone)]
struct RawRepairRecipe {
inputs: Vec<(RawRecipeInput, u32)>,
}
#[derive(Serialize, Deserialize, Clone)]
struct RawRepairRecipeBook {
recipes: HashMap<RepairKey, RawRepairRecipe>,
fallback: RawRepairRecipe,
}
impl assets::Asset for RawRepairRecipeBook {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RepairRecipe {
inputs: Vec<(RecipeInput, u32)>,
}
impl RepairRecipe {
/// Determine whether the inventory contains the ingredients for a repair.
/// If it does, return a vec of inventory slots that contain the
/// ingredients needed, whose positions correspond to particular repair
/// 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)>> {
inventory_contains_ingredients(self.inputs(), inv, 1)
}
pub fn inputs(&self) -> impl ExactSizeIterator<Item = (&RecipeInput, u32)> {
self.inputs.iter().map(|(input, amount)| (input, *amount))
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RepairRecipeBook {
recipes: HashMap<RepairKey, RepairRecipe>,
fallback: RepairRecipe,
}
impl RepairRecipeBook {
pub fn repair_recipe(&self, item: &Item) -> Option<&RepairRecipe> {
RepairKey::from_item(item)
.as_ref()
.and_then(|key| self.recipes.get(key))
.or_else(|| item.has_durability().then_some(&self.fallback))
}
pub fn repair_item(
&self,
inv: &mut Inventory,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Result<(), Vec<(&RecipeInput, u32)>> {
let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new();
if let Some(item) = match item {
Slot::Equip(slot) => inv.equipped(slot),
Slot::Inventory(slot) => inv.get(slot),
} {
if let Some(repair_recipe) = self.repair_recipe(item) {
repair_recipe
.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
input.handle_requirement(
*amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
input_slots,
);
})
}
}
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");
}
}
inv.repair_item_at_slot(item);
Ok(())
} else {
Err(unsatisfied_requirements)
}
}
}
impl assets::Compound for RepairRecipeBook {
fn load(
cache: assets::AnyCache,
specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
#[inline]
fn load_recipe_input(
(input, amount): &(RawRecipeInput, u32),
) -> Result<(RecipeInput, u32), assets::Error> {
let input = input.load_recipe_input()?;
Ok((input, *amount))
}
let raw = cache.load::<RawRepairRecipeBook>(specifier)?.cloned();
let recipes = raw
.recipes
.iter()
.map(|(key, RawRepairRecipe { inputs })| {
let inputs = inputs
.iter()
.map(load_recipe_input)
.collect::<Result<Vec<_>, _>>()?;
Ok((key.clone(), RepairRecipe { inputs }))
})
.collect::<Result<_, assets::Error>>()?;
let fallback = RepairRecipe {
inputs: raw
.fallback
.inputs
.iter()
.map(load_recipe_input)
.collect::<Result<Vec<_>, _>>()?,
};
Ok(RepairRecipeBook { recipes, fallback })
}
}
pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
RecipeBook::load_expect("common.recipe_book")
}
@ -908,6 +1101,10 @@ pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
ComponentRecipeBook::load_expect("common.component_recipe_book")
}
pub fn default_repair_recipe_book() -> AssetHandle<RepairRecipeBook> {
RepairRecipeBook::load_expect("common.repair_recipe_book")
}
impl assets::Compound for ReverseComponentRecipeBook {
fn load(
cache: assets::AnyCache,

View File

@ -12,7 +12,9 @@ use common::{
slot::{self, Slot},
},
consts::MAX_PICKUP_RANGE,
recipe::{self, default_component_recipe_book, default_recipe_book},
recipe::{
self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book,
},
terrain::{Block, SpriteKind},
trade::Trades,
uid::Uid,
@ -856,10 +858,17 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
None
}
},
CraftEvent::Repair(slot) => {
CraftEvent::Repair { item, slots } => {
let repair_recipes = default_repair_recipe_book().read();
let sprite = get_craft_sprite(state, craft_sprite);
if matches!(sprite, Some(SpriteKind::RepairBench)) {
inventory.repair_item_at_slot(slot);
let _ = repair_recipes.repair_item(
&mut inventory,
item,
slots,
&state.ecs().read_resource::<AbilityMap>(),
&state.ecs().read_resource::<item::MaterialStatManifest>(),
);
}
None
},

View File

@ -8,7 +8,7 @@ use crate::{
use common::{
comp::{self, Admin, Player, Stats},
event::{EventBus, ServerEvent},
recipe::{default_component_recipe_book, default_recipe_book},
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
resources::TimeOfDay,
shared_server_config::ServerConstants,
uid::{Uid, UidAllocator},
@ -348,6 +348,7 @@ impl<'a> System<'a> for Sys {
world_map: (*read_data.map).clone(),
recipe_book: default_recipe_book().cloned(),
component_recipe_book: default_component_recipe_book().cloned(),
repair_recipe_book: default_repair_recipe_book().cloned(),
material_stats: (*read_data.material_stats).clone(),
ability_map: (*read_data.ability_map).clone(),
server_constants: ServerConstants {

View File

@ -726,7 +726,7 @@ pub enum Event {
craft_sprite: Option<Vec3<i32>>,
},
RepairItem {
slot: Slot,
item: Slot,
sprite_pos: Vec3<i32>,
},
InviteMember(Uid),
@ -3944,7 +3944,7 @@ impl Hud {
self.show.crafting_fields.craft_sprite
{
events.push(Event::RepairItem {
slot: from,
item: from,
sprite_pos,
})
}

View File

@ -1777,8 +1777,27 @@ impl PlayState for SessionState {
HudEvent::SalvageItem { slot, salvage_pos } => {
self.client.borrow_mut().salvage_item(slot, salvage_pos);
},
HudEvent::RepairItem { slot, sprite_pos } => {
self.client.borrow_mut().repair_item(slot, sprite_pos);
HudEvent::RepairItem { item, sprite_pos } => {
let slots = {
let client = self.client.borrow();
let slots = (|| {
if let Some(inventory) = client.inventories().get(client.entity()) {
let item = match item {
Slot::Equip(slot) => inventory.equipped(slot),
Slot::Inventory(slot) => inventory.get(slot),
}?;
let repair_recipe =
client.repair_recipe_book().repair_recipe(item)?;
repair_recipe.inventory_contains_ingredients(inventory).ok()
} else {
None
}
})();
slots.unwrap_or_default()
};
self.client
.borrow_mut()
.repair_item(item, slots, sprite_pos);
},
HudEvent::InviteMember(uid) => {
self.client.borrow_mut().send_invite(uid, InviteKind::Group);