Skip to main content

steel_core/entity/
potion_contents.rs

1//! Vanilla `PotionContents` behavior extension: the methods that need
2//! `LivingEntity`/`World`, which can't live alongside the data in
3//! `steel_registry::data_components::PotionContents`.
4
5use steel_registry::MobEffectInstance as RegistryMobEffectInstance;
6use steel_registry::data_components::PotionContents;
7
8use crate::behavior::MOB_EFFECT_BEHAVIORS;
9use crate::entity::{LivingEntityRef, MobEffectInstance as RuntimeMobEffectInstance};
10use crate::world::World;
11
12pub(crate) fn apply_potion_contents(
13    contents: &PotionContents,
14    world: &World,
15    user: LivingEntityRef<'_>,
16    duration_scale: f32,
17) {
18    // Vanilla passes the drinker itself as both `source` and `owner` when it
19    // is a player (`null` otherwise), attributing instantaneous damage to it.
20    let damage_source_entity = user.living().as_player().map(|_| user.entity());
21    for effect in contents.all_effects() {
22        let behavior = MOB_EFFECT_BEHAVIORS.get_behavior(effect.effect());
23        if let Some(instantaneous) = behavior.as_instantaneous() {
24            // Vanilla always passes `scale = 1.0` from this call site; only a
25            // splash/lingering potion passes a distance-based falloff scale and
26            // a `source` distinct from `owner`.
27            instantaneous.apply_instantaneous(
28                world,
29                user.living(),
30                effect.amplifier(),
31                damage_source_entity,
32                damage_source_entity,
33                1.0,
34            );
35            continue;
36        }
37
38        let scaled_effect = effect.with_scaled_duration(duration_scale);
39        user.living().add_mob_effect(to_runtime_instance(
40            &scaled_effect,
41            scaled_effect.duration(),
42        ));
43    }
44}
45
46/// Builds the runtime active-effect state for one registry mob-effect
47/// instance, ready to hand to `LivingEntity::add_mob_effect`.
48pub(crate) const fn to_runtime_instance(
49    effect: &RegistryMobEffectInstance,
50    duration: i32,
51) -> RuntimeMobEffectInstance {
52    RuntimeMobEffectInstance::with_duration(effect.effect(), duration, effect.amplifier())
53        .with_ambient(effect.ambient())
54        .with_visible(effect.show_particles())
55        .with_show_icon(effect.show_icon())
56}
57
58/// Reproduces MC-276746: splash potions use `show_particles` for `show_icon`
59pub(crate) const fn to_runtime_instance_icon_from_visibility(
60    effect: &RegistryMobEffectInstance,
61    duration: i32,
62) -> RuntimeMobEffectInstance {
63    to_runtime_instance(effect, duration).with_show_icon(effect.show_particles())
64}
65
66#[cfg(test)]
67mod tests {
68    use std::sync::Arc;
69
70    use steel_registry::data_components::PotionContents;
71    use steel_registry::{
72        MobEffectInstance as RegistryMobEffectInstance, init_vanilla_registry, vanilla_mob_effects,
73    };
74    use steel_utils::ChunkPos;
75
76    use super::{
77        apply_potion_contents, to_runtime_instance, to_runtime_instance_icon_from_visibility,
78    };
79    use crate::behavior::init_behaviors;
80    use crate::entity::{LivingEntity, LivingEntityRef, SharedEntity};
81    use crate::player::Player;
82    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
83
84    #[test]
85    fn splash_rebuild_takes_its_icon_flag_from_visibility() {
86        init_vanilla_registry();
87        let effect = RegistryMobEffectInstance::new(
88            vanilla_mob_effects::LUCK,
89            100,
90            0,
91            false,
92            true,
93            false,
94            None,
95        );
96
97        let drunk = to_runtime_instance(&effect, 100);
98        assert!(drunk.is_visible());
99        assert!(
100            !drunk.show_icon(),
101            "drinking preserves the source icon flag"
102        );
103
104        let splashed = to_runtime_instance_icon_from_visibility(&effect, 100);
105        assert!(splashed.is_visible());
106        assert!(
107            splashed.show_icon(),
108            "the splash rebuild must take show_icon from visible"
109        );
110
111        // Everything else stays identical between the two paths.
112        assert_eq!(splashed.effect(), drunk.effect());
113        assert_eq!(splashed.duration(), drunk.duration());
114        assert_eq!(splashed.amplifier(), drunk.amplifier());
115        assert_eq!(splashed.is_ambient(), drunk.is_ambient());
116    }
117
118    /// Vanilla's `int` shift is masked to the low 5 bits (Java `<<` never
119    /// throws), so an Instant Health/Instant Damage amplifier of 32 or more
120    /// must not panic and must reproduce that masked value rather than the
121    /// naive (and here overflowing) shift amount.
122    #[test]
123    fn instant_health_amplifier_at_shift_width_does_not_panic_and_wraps_like_vanilla() {
124        init_vanilla_registry();
125        init_behaviors();
126        let world_fixture = fresh_test_world("instant_health_high_amplifier");
127        let world = &world_fixture.world;
128        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
129        let player = TestPlayerBuilder::new(Arc::clone(world), "Test", 1).build();
130        let player_entity: SharedEntity = Arc::<Player>::clone(&player);
131        player.set_health(1.0);
132
133        let contents = PotionContents::new(
134            None,
135            None,
136            vec![RegistryMobEffectInstance::simple(
137                vanilla_mob_effects::INSTANT_HEALTH,
138                1,
139                32,
140            )],
141            None,
142        );
143
144        apply_potion_contents(
145            &contents,
146            world,
147            LivingEntityRef::new(&player_entity).expect("player is living"),
148            1.0,
149        );
150
151        // 4 << 32 wraps to 4 << (32 % 32) == 4 << 0 == 4, matching Java.
152        assert_eq!(player.get_health(), 5.0);
153    }
154}