steel_core/entity/
potion_contents.rs1use steel_registry::MobEffectInstance as RegistryMobEffectInstance;
6use steel_registry::data_components::PotionContents;
7
8use crate::behavior::MOB_EFFECT_BEHAVIORS;
9use crate::entity::{Entity, LivingEntity, MobEffectInstance as RuntimeMobEffectInstance};
10use crate::world::World;
11
12pub(crate) fn apply_potion_contents(
14 contents: &PotionContents,
15 world: &World,
16 user: &dyn LivingEntity,
17 duration_scale: f32,
18) {
19 let damage_source_entity = user.as_player().map(Entity::id);
22 for effect in contents.all_effects() {
23 let behavior = MOB_EFFECT_BEHAVIORS.get_behavior(effect.effect());
24 if let Some(instantaneous) = behavior.as_instantaneous() {
25 instantaneous.apply_instantaneous(
30 world,
31 user,
32 effect.amplifier(),
33 damage_source_entity,
34 damage_source_entity,
35 1.0,
36 );
37 continue;
38 }
39
40 let scaled_duration = scale_effect_duration(effect.duration(), duration_scale);
41 user.add_mob_effect(to_runtime_instance(&effect, scaled_duration));
42 }
43}
44
45fn scale_effect_duration(duration: i32, scale: f32) -> i32 {
49 if duration == -1 || duration == 0 {
50 return duration;
51 }
52 ((duration as f32 * scale).floor() as i32).max(1)
53}
54
55pub(crate) const fn to_runtime_instance(
58 effect: &RegistryMobEffectInstance,
59 duration: i32,
60) -> RuntimeMobEffectInstance {
61 RuntimeMobEffectInstance::with_duration(effect.effect(), duration, effect.amplifier())
62 .with_ambient(effect.ambient())
63 .with_visible(effect.show_particles())
64 .with_show_icon(effect.show_icon())
65}
66
67#[cfg(test)]
68mod tests {
69 use steel_registry::data_components::PotionContents;
70 use steel_registry::{
71 MobEffectInstance as RegistryMobEffectInstance, init_vanilla_registry, vanilla_mob_effects,
72 };
73 use steel_utils::ChunkPos;
74
75 use super::{apply_potion_contents, scale_effect_duration};
76 use crate::entity::LivingEntity;
77 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
78
79 #[test]
82 fn scale_effect_duration_leaves_infinite_and_zero_durations_untouched() {
83 assert_eq!(scale_effect_duration(-1, 0.5), -1);
84 assert_eq!(scale_effect_duration(0, 0.5), 0);
85 assert_eq!(scale_effect_duration(-1, 100.0), -1);
87 assert_eq!(scale_effect_duration(0, 0.0), 0);
88 }
89
90 #[test]
94 fn scale_effect_duration_floors_and_clamps_finite_durations() {
95 assert_eq!(scale_effect_duration(100, 0.5), 50);
96 assert_eq!(scale_effect_duration(9, 0.34), 3);
98 assert_eq!(scale_effect_duration(1, 0.1), 1);
100 assert_eq!(scale_effect_duration(100, 1.0), 100);
101 }
102
103 #[test]
108 fn instant_health_amplifier_at_shift_width_does_not_panic_and_wraps_like_vanilla() {
109 init_vanilla_registry();
110 let world = fresh_test_world("instant_health_high_amplifier");
111 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
112 let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
113 player.set_health(1.0);
114
115 let contents = PotionContents::new(
116 None,
117 None,
118 vec![RegistryMobEffectInstance::simple(
119 vanilla_mob_effects::INSTANT_HEALTH,
120 1,
121 32,
122 )],
123 None,
124 );
125
126 apply_potion_contents(&contents, &world, player.as_ref(), 1.0);
127
128 assert_eq!(player.get_health(), 5.0);
130 }
131}