//! Support for enchantments and items with attribute modifiers. use std::collections::HashMap; use azalea_core::{data_registry::ResolvableDataRegistry, registry_holder::value::AttributeEffect}; use azalea_entity::{Attributes, inventory::Inventory}; use azalea_inventory::{ ItemStack, components::{self, AttributeModifier, EquipmentSlot}, }; use azalea_registry::identifier::Identifier; use bevy_ecs::{ component::Component, entity::Entity, event::EntityEvent, observer::On, query::With, system::{Commands, Query}, }; use tracing::{debug, error, warn}; use crate::local_player::WorldHolder; /// A component that contains the equipment slots that we had last tick. /// /// This is used by [`collect_equipment_changes`] for applying enchantments. #[derive(Component, Debug, Default)] pub struct LastEquipmentItems { pub map: HashMap, } pub fn collect_equipment_changes( mut commands: Commands, mut query: Query<(Entity, &Inventory, Option<&LastEquipmentItems>), With>, ) { for (entity, inventory, last_equipment_items) in &mut query { let last_equipment_items = if let Some(e) = last_equipment_items { e } else { commands .entity(entity) .insert(LastEquipmentItems::default()); continue; }; let mut changes = HashMap::new(); for equipment_slot in EquipmentSlot::values() { let current_item = inventory .get_equipment(equipment_slot) .unwrap_or(&ItemStack::Empty); let last_item = last_equipment_items .map .get(&equipment_slot) .unwrap_or(&ItemStack::Empty); if current_item == last_item { // item hasn't changed, nothing to do continue; } changes.insert( equipment_slot, EquipmentChange { old: last_item.clone(), new: current_item.clone(), }, ); } if changes.is_empty() { continue; } commands.trigger(EquipmentChangesEvent { entity, map: changes, }); } } #[derive(Debug, EntityEvent)] pub struct EquipmentChangesEvent { pub entity: Entity, pub map: HashMap, } #[derive(Debug)] pub struct EquipmentChange { pub old: ItemStack, pub new: ItemStack, } pub fn handle_equipment_changes( equipment_changes: On, mut query: Query<(&WorldHolder, &mut LastEquipmentItems, &mut Attributes)>, ) { let Ok((world_holder, mut last_equipment_items, mut attributes)) = query.get_mut(equipment_changes.entity) else { error!( "got EquipmentChangesEvent with unknown entity {}", equipment_changes.entity ); return; }; if !equipment_changes.map.is_empty() { debug!("equipment changes: {:?}", equipment_changes.map); } for (&slot, change) in &equipment_changes.map { if change.old.is_present() { // stopLocationBasedEffects for (attribute, modifier) in collect_attribute_modifiers_from_item(slot, &change.old, world_holder) { if let Some(attribute) = attributes.get_mut(attribute) { attribute.remove(&modifier.id); } } last_equipment_items.map.remove(&slot); } if change.new.is_present() { // see ItemStack.forEachModifier in vanilla for (attribute, modifier) in collect_attribute_modifiers_from_item(slot, &change.new, world_holder) { if let Some(attribute) = attributes.get_mut(attribute) { attribute.remove(&modifier.id); attribute.insert(modifier); } } // runLocationChangedEffects last_equipment_items.map.insert(slot, change.new.clone()); } } } fn collect_attribute_modifiers_from_item( slot: EquipmentSlot, item: &ItemStack, world_holder: &WorldHolder, ) -> Vec<(azalea_registry::builtin::Attribute, AttributeModifier)> { let mut modifiers = Vec::new(); // handle the attribute_modifiers component first let attribute_modifiers = item .get_component::() .unwrap_or_default(); for modifier in &attribute_modifiers.modifiers { modifiers.push((modifier.kind, modifier.modifier.clone())); } // now handle enchants let enchants = item .get_component::() .unwrap_or_default(); if !enchants.levels.is_empty() { let registry_holder = &world_holder.shared.read().registries; for (enchant, &level) in &enchants.levels { let Some((_enchant_id, enchant_definition)) = enchant.resolve(registry_holder) else { warn!( "Got equipment with an enchantment that wasn't in the registry, so it couldn't be resolved to an ID" ); continue; }; let effects = enchant_definition.get::(); for effect in effects.unwrap_or_default() { // TODO: check if the effect definition allows the slot let modifier = AttributeModifier { id: Identifier::new(format!("{}/{slot}", effect.id)), amount: effect.amount.calculate(level) as f64, operation: effect.operation, }; modifiers.push((effect.attribute, modifier)); } } } modifiers }