Skip to main content

steel_core/command/builtins/
enchant.rs

1//! Vanilla main-hand enchantment command.
2
3use std::borrow::Cow;
4
5use steel_registry::{
6    REGISTRY, TaggedRegistryExt as _,
7    enchantment::{Enchantment, EnchantmentRef},
8    equipment::EquipmentSlot,
9    vanilla_enchantment_tags::EnchantmentTag,
10};
11use steel_utils::{Identifier, translations};
12use text_components::{Modifier, TextComponent, format::Color, translation::TranslatedMessage};
13
14use super::super::{
15    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
16    execution::{
17        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
18        literal,
19    },
20    registration::CommandRegistration,
21};
22use crate::entity::{LivingEntity, SharedEntity};
23
24pub(super) fn registration() -> CommandRegistration<CommandSource> {
25    CommandRegistration::new(Identifier::vanilla_static("enchant"), |_| command())
26}
27
28fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
29    literal("enchant").then(
30        argument("targets", SteelArgumentType::entities()).then(
31            argument("enchantment", SteelArgumentType::enchantment())
32                .executes(enchant_default_level)
33                .then(
34                    argument("level", ArgumentType::integer(0, i32::MAX))
35                        .executes(enchant_with_level),
36                ),
37        ),
38    )
39}
40
41fn enchant_default_level(
42    context: &SteelCommandContext<CommandSource>,
43) -> Result<i32, CommandSyntaxError> {
44    enchant(context, 1)
45}
46
47fn enchant_with_level(
48    context: &SteelCommandContext<CommandSource>,
49) -> Result<i32, CommandSyntaxError> {
50    let Some(level) = context.integer("level") else {
51        return Err(missing_argument("level"));
52    };
53    enchant(context, level)
54}
55
56fn enchant(
57    context: &SteelCommandContext<CommandSource>,
58    level: i32,
59) -> Result<i32, CommandSyntaxError> {
60    let targets = context.entities("targets")?;
61    let Some(enchantment) = context.enchantment("enchantment") else {
62        return Err(missing_argument("enchantment"));
63    };
64    let level = u32::try_from(level)
65        .map_err(|_| CommandSyntaxError::dynamic("Enchantment level cannot be negative"))?;
66    if level > enchantment.max_level {
67        let message = translations::COMMANDS_ENCHANT_FAILED_LEVEL
68            .message([level.to_string(), enchantment.max_level.to_string()])
69            .component();
70        return Err(CommandSyntaxError::dynamic(message));
71    }
72
73    let mut success = 0usize;
74    for target in &targets {
75        let Some(living) = target.as_living_entity() else {
76            if targets.len() == 1 {
77                return Err(not_living_error(target));
78            }
79            continue;
80        };
81
82        match enchant_main_hand(living, enchantment, level) {
83            EnchantTargetResult::Enchanted => success += 1,
84            EnchantTargetResult::Itemless if targets.len() == 1 => {
85                return Err(itemless_error(target));
86            }
87            EnchantTargetResult::Incompatible(item_name) if targets.len() == 1 => {
88                return Err(incompatible_error(item_name));
89            }
90            EnchantTargetResult::Itemless | EnchantTargetResult::Incompatible(_) => {}
91        }
92    }
93
94    if success == 0 {
95        return Err(CommandSyntaxError::dynamic(TextComponent::from(
96            &translations::COMMANDS_ENCHANT_FAILED,
97        )));
98    }
99
100    let enchantment_name = enchantment_display_name(enchantment, level);
101    let message = if let [target] = targets.as_slice() {
102        translations::COMMANDS_ENCHANT_SUCCESS_SINGLE
103            .message([
104                enchantment_name,
105                TextComponent::plain(target.plain_text_name()),
106            ])
107            .component()
108    } else {
109        translations::COMMANDS_ENCHANT_SUCCESS_MULTIPLE
110            .message([
111                enchantment_name,
112                TextComponent::from(targets.len().to_string()),
113            ])
114            .component()
115    };
116    context.source().send_success(&message, true);
117
118    i32::try_from(success).map_err(|_| {
119        CommandSyntaxError::dynamic("Enchanted entity count exceeds the command result range")
120    })
121}
122
123#[derive(Debug, PartialEq, Eq)]
124enum EnchantTargetResult {
125    Enchanted,
126    Itemless,
127    Incompatible(Box<str>),
128}
129
130fn enchant_main_hand(
131    target: &dyn LivingEntity,
132    enchantment: EnchantmentRef,
133    level: u32,
134) -> EnchantTargetResult {
135    let mut result = EnchantTargetResult::Itemless;
136    target.with_equipment_slot(EquipmentSlot::MainHand, &mut |item| {
137        if item.is_empty() {
138            return;
139        }
140        if !enchantment.can_enchant(item.item())
141            || !Enchantment::is_compatible_with_existing(enchantment, item)
142        {
143            result =
144                EnchantTargetResult::Incompatible(item.item().key.to_string().into_boxed_str());
145            return;
146        }
147        result = EnchantTargetResult::Enchanted;
148    });
149
150    if result == EnchantTargetResult::Enchanted {
151        target.with_equipment_slot_mut(EquipmentSlot::MainHand, &mut |item| {
152            item.upgrade_enchantment(enchantment.key.clone(), level);
153        });
154    }
155    result
156}
157
158fn not_living_error(target: &SharedEntity) -> CommandSyntaxError {
159    let message = translations::COMMANDS_ENCHANT_FAILED_ENTITY
160        .message([TextComponent::plain(target.plain_text_name())])
161        .component();
162    CommandSyntaxError::dynamic(message)
163}
164
165fn itemless_error(target: &SharedEntity) -> CommandSyntaxError {
166    let message = translations::COMMANDS_ENCHANT_FAILED_ITEMLESS
167        .message([TextComponent::plain(target.plain_text_name())])
168        .component();
169    CommandSyntaxError::dynamic(message)
170}
171
172fn incompatible_error(item_name: Box<str>) -> CommandSyntaxError {
173    let message = translations::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE
174        .message([TextComponent::plain(String::from(item_name))])
175        .component();
176    CommandSyntaxError::dynamic(message)
177}
178
179fn enchantment_display_name(enchantment: EnchantmentRef, level: u32) -> TextComponent {
180    let color = if REGISTRY
181        .enchantments
182        .is_in_tag(enchantment, &EnchantmentTag::CURSE)
183    {
184        Color::Red
185    } else {
186        Color::Gray
187    };
188    let mut component = TextComponent::translated(TranslatedMessage {
189        key: Cow::Owned(format!(
190            "enchantment.{}.{}",
191            enchantment.key.namespace, enchantment.key.path
192        )),
193        args: None,
194        fallback: None,
195    })
196    .color(color);
197
198    if level != 1 || enchantment.max_level != 1 {
199        component =
200            component
201                .add_child(TextComponent::plain(" "))
202                .add_child(TextComponent::translated(TranslatedMessage {
203                    key: Cow::Owned(format!("enchantment.level.{level}")),
204                    args: None,
205                    fallback: None,
206                }));
207    }
208    component
209}
210
211fn missing_argument(name: &str) -> CommandSyntaxError {
212    CommandSyntaxError::dynamic(format!(
213        "Parsed value for {name} is missing from the command context"
214    ))
215}
216
217#[cfg(test)]
218mod tests {
219    use std::sync::Weak;
220
221    use glam::DVec3;
222    use steel_registry::{
223        entity_type::EntityTypeRef, equipment::EquipmentSlot, init_vanilla_registry,
224        item_stack::ItemStack, vanilla_enchantments, vanilla_entities, vanilla_items,
225    };
226    use steel_utils::locks::SyncMutex;
227
228    use super::super::create_dispatcher;
229    use super::{EnchantTargetResult, enchant_main_hand};
230    use crate::{
231        command::{
232            brigadier::{ArgumentType, CommandDispatcher, NodeId},
233            execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
234        },
235        entity::{Entity, EntityBase, LivingEntity, LivingEntityBase},
236    };
237
238    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
239
240    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
241        let Some(children) = dispatcher.children(parent) else {
242            panic!("parent node should exist");
243        };
244        let Some(child) = children.iter().copied().find(|child| {
245            dispatcher
246                .node(*child)
247                .is_some_and(|node| node.name() == name)
248        }) else {
249            panic!("child {name} should exist");
250        };
251        child
252    }
253
254    #[test]
255    fn enchant_graph_uses_all_entities_and_an_enchantment_resource() {
256        init_vanilla_registry();
257        let Ok(dispatcher) = create_dispatcher() else {
258            panic!("built-in commands should register");
259        };
260        let enchant = child(&dispatcher, dispatcher.root(), "enchant");
261        let targets = child(&dispatcher, enchant, "targets");
262        assert_eq!(
263            dispatcher
264                .node(targets)
265                .and_then(|node| node.argument_type()),
266            Some(&SteelArgumentType::entities())
267        );
268
269        let enchantment = child(&dispatcher, targets, "enchantment");
270        assert_eq!(
271            dispatcher
272                .node(enchantment)
273                .and_then(|node| node.argument_type()),
274            Some(&SteelArgumentType::enchantment())
275        );
276        assert!(matches!(
277            dispatcher.node(enchantment),
278            Some(node) if node.is_executable()
279        ));
280
281        let level = child(&dispatcher, enchantment, "level");
282        assert_eq!(
283            dispatcher.node(level).and_then(|node| node.argument_type()),
284            Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
285        );
286    }
287
288    #[test]
289    fn enchant_main_hand_applies_once_and_then_rejects_the_same_enchantment() {
290        init_vanilla_registry();
291        let target = TestLivingEntity::new(&vanilla_entities::ZOMBIE);
292        target.equip(ItemStack::new(&vanilla_items::DIAMOND_SWORD));
293
294        assert_eq!(
295            enchant_main_hand(&target, &vanilla_enchantments::SHARPNESS, 1),
296            EnchantTargetResult::Enchanted
297        );
298        assert_eq!(target.main_hand_enchantment_level(), 1);
299        assert!(matches!(
300            enchant_main_hand(&target, &vanilla_enchantments::SHARPNESS, 2),
301            EnchantTargetResult::Incompatible(_)
302        ));
303        assert_eq!(target.main_hand_enchantment_level(), 1);
304    }
305
306    struct TestLivingEntity {
307        base: EntityBase,
308        living_base: LivingEntityBase,
309        health: SyncMutex<f32>,
310        entity_type: EntityTypeRef,
311    }
312
313    impl TestLivingEntity {
314        fn new(entity_type: EntityTypeRef) -> Self {
315            Self {
316                base: EntityBase::new(1, DVec3::ZERO, entity_type.dimensions, Weak::new()),
317                living_base: LivingEntityBase::new(entity_type),
318                health: SyncMutex::new(20.0),
319                entity_type,
320            }
321        }
322
323        fn equip(&self, stack: ItemStack) {
324            self.living_base
325                .equipment()
326                .lock()
327                .set(EquipmentSlot::MainHand, stack);
328        }
329
330        fn main_hand_enchantment_level(&self) -> i32 {
331            let mut level = 0;
332            self.with_equipment_slot(EquipmentSlot::MainHand, &mut |item| {
333                level = item.get_enchantment_level(&vanilla_enchantments::SHARPNESS.key);
334            });
335            level
336        }
337    }
338
339    crate::entity::impl_test_downcast_type!(TestLivingEntity);
340
341    impl Entity for TestLivingEntity {
342        fn base(&self) -> &EntityBase {
343            &self.base
344        }
345
346        fn entity_type(&self) -> EntityTypeRef {
347            self.entity_type
348        }
349    }
350
351    impl LivingEntity for TestLivingEntity {
352        fn living_base(&self) -> &LivingEntityBase {
353            &self.living_base
354        }
355
356        fn get_health(&self) -> f32 {
357            *self.health.lock()
358        }
359
360        fn set_health(&self, health: f32) {
361            *self.health.lock() = health;
362        }
363
364        fn get_absorption_amount(&self) -> f32 {
365            0.0
366        }
367
368        fn set_absorption_amount(&self, _amount: f32) {}
369    }
370}