steel_core/command/builtins/
enchant.rs1use 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}