Skip to main content

steel_core/entity/
monster.rs

1//! Shared hostile-mob markers, behavior, and spawn rules.
2
3use steel_math::fast_floor;
4use steel_protocol::packets::game::SoundSource;
5use steel_registry::dimension_type::MonsterSpawnLightLevel;
6use steel_registry::entity_type::EntityTypeRef;
7use steel_registry::sound_event::SoundEventRef;
8use steel_registry::sound_events;
9use steel_registry::vanilla_game_rules::MOB_DROPS;
10use steel_utils::BlockPos;
11use steel_utils::random::Random;
12use steel_utils::types::Difficulty;
13
14use crate::chunk::light::{LightLayer, MAX_LIGHT_LEVEL};
15use crate::entity::damage::DamageSource;
16use crate::entity::mob::LIGHT_MAGIC_VALUE_BRIGHTNESS_GATE;
17use crate::entity::{EntitySpawnReason, Mob, PathfinderMob, SharedEntity};
18use crate::physics::MoveResult;
19use crate::player::Player;
20use crate::world::{LevelReader, World};
21
22pub const DEFAULT_XP_REWARD: i32 = 5;
23
24const THUNDERING_SKY_DARKENING: u8 = 10;
25const BRIGHT_LIGHT_NO_ACTION_TIME_INCREMENT: i32 = 2;
26const SKY_LIGHT_SPAWN_ROLL_BOUND: i32 = 32;
27
28/// Marks hostile entities, including those outside the `Monster` hierarchy.
29/// Shared rules use this marker to reject leashing.
30pub trait Enemy {}
31
32impl<T: Monster + ?Sized> Enemy for T {}
33
34/// Shared hostile-mob behavior. Concrete mobs delegate to `_monster` hooks
35/// to avoid name collisions with base-trait methods.
36pub trait Monster: PathfinderMob + Enemy {
37    fn default_xp_reward_monster(&self) -> i32 {
38        DEFAULT_XP_REWARD
39    }
40
41    fn sound_source_monster(&self) -> SoundSource {
42        SoundSource::Hostile
43    }
44
45    fn hurt_sound_monster(&self, _source: &DamageSource) -> Option<SoundEventRef> {
46        Some(&sound_events::ENTITY_HOSTILE_HURT)
47    }
48
49    fn death_sound_monster(&self) -> Option<SoundEventRef> {
50        Some(&sound_events::ENTITY_HOSTILE_DEATH)
51    }
52
53    fn fall_sounds_monster(&self) -> (SoundEventRef, SoundEventRef) {
54        (
55            &sound_events::ENTITY_HOSTILE_SMALL_FALL,
56            &sound_events::ENTITY_HOSTILE_BIG_FALL,
57        )
58    }
59
60    /// Returns the hostile water-entry splash sound.
61    fn swim_splash_sound_monster(&self) -> SoundEventRef {
62        &sound_events::ENTITY_HOSTILE_SPLASH
63    }
64
65    /// Returns the hostile swim sound.
66    fn swim_sound_monster(&self) -> SoundEventRef {
67        &sound_events::ENTITY_HOSTILE_SWIM
68    }
69
70    /// Adds two idle ticks in bright light to hasten despawning.
71    fn update_no_action_time(&self) {
72        let Some(world) = self.level() else {
73            return;
74        };
75        let eye_pos = BlockPos::new(
76            fast_floor(self.position().x),
77            fast_floor(self.get_eye_y()),
78            fast_floor(self.position().z),
79        );
80        if world.light_level_dependent_magic_value(eye_pos) > LIGHT_MAGIC_VALUE_BRIGHTNESS_GATE {
81            self.set_no_action_time(self.no_action_time() + BRIGHT_LIGHT_NO_ACTION_TIME_INCREMENT);
82        }
83    }
84
85    fn monster_ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
86        self.update_no_action_time();
87        self.mob_ai_step(entity)
88    }
89
90    /// Favors darker positions for pathfinding.
91    fn get_walk_target_value_monster(&self, pos: BlockPos) -> f32 {
92        let Some(world) = self.level() else {
93            return 0.0;
94        };
95        -world.pathfinding_cost_from_light_levels(pos)
96    }
97
98    fn is_preventing_player_rest(&self, _level: &World, _player: &Player) -> bool {
99        true
100    }
101
102    fn should_drop_experience_monster(&self) -> bool {
103        true
104    }
105
106    /// Allows loot when `mobDrops` is enabled, including for babies.
107    fn should_drop_loot_monster(&self, world: &World) -> bool {
108        world.get_game_rule(&MOB_DROPS)
109    }
110
111    /// Checks dimension light limits using the caller's seeded RNG.
112    fn is_dark_enough_to_spawn(level: &World, pos: BlockPos, random: &mut impl Random) -> bool
113    where
114        Self: Sized,
115    {
116        if i32::from(level.light_value_at(LightLayer::Sky, pos))
117            > random.next_i32_bounded(SKY_LIGHT_SPAWN_ROLL_BOUND)
118        {
119            return false;
120        }
121
122        let dimension_type = &level.dimension_type;
123        let block_light_limit = dimension_type.monster_spawn_block_light_limit;
124        if block_light_limit < i32::from(MAX_LIGHT_LEVEL)
125            && i32::from(level.light_value_at(LightLayer::Block, pos)) > block_light_limit
126        {
127            return false;
128        }
129
130        let sky_darkening = if level.is_thundering() {
131            THUNDERING_SKY_DARKENING
132        } else {
133            level.sky_darkening()
134        };
135        let brightness = level.max_local_raw_brightness(pos, sky_darkening);
136        i32::from(brightness)
137            <= sample_monster_spawn_light_test(&dimension_type.monster_spawn_light_level, random)
138    }
139
140    /// Checks difficulty, light, and the spawn surface.
141    fn check_monster_spawn_rules(
142        entity_type: EntityTypeRef,
143        level: &World,
144        spawn_reason: EntitySpawnReason,
145        pos: BlockPos,
146        random: &mut impl Random,
147    ) -> bool
148    where
149        Self: Sized,
150    {
151        is_spawn_allowed_on_difficulty(entity_type, level)
152            && (spawn_reason.ignores_light_requirements()
153                || Self::is_dark_enough_to_spawn(level, pos, random))
154            && <Self as Mob>::check_mob_spawn_rules(entity_type, level, spawn_reason, pos)
155    }
156
157    /// Checks difficulty and the spawn surface without a light requirement.
158    fn check_any_light_monster_spawn_rules(
159        entity_type: EntityTypeRef,
160        level: &World,
161        spawn_reason: EntitySpawnReason,
162        pos: BlockPos,
163    ) -> bool
164    where
165        Self: Sized,
166    {
167        is_spawn_allowed_on_difficulty(entity_type, level)
168            && <Self as Mob>::check_mob_spawn_rules(entity_type, level, spawn_reason, pos)
169    }
170
171    /// Adds sky visibility to monster spawn rules, except for spawners.
172    fn check_surface_monsters_spawn_rules(
173        entity_type: EntityTypeRef,
174        level: &World,
175        spawn_reason: EntitySpawnReason,
176        pos: BlockPos,
177        random: &mut impl Random,
178    ) -> bool
179    where
180        Self: Sized,
181    {
182        Self::check_monster_spawn_rules(entity_type, level, spawn_reason, pos, random)
183            && (spawn_reason.is_spawner() || level.can_see_sky(pos))
184    }
185}
186
187/// Applies vanilla's `EntityType.canSpawn` difficulty gate here until Steel
188/// has a creation/spawner gate that rejects hostile types on Peaceful.
189fn is_spawn_allowed_on_difficulty(entity_type: EntityTypeRef, level: &World) -> bool {
190    entity_type.allowed_in_peaceful || level.difficulty() != Difficulty::Peaceful
191}
192
193/// Samples the dimension's spawn-light provider. Vanilla uses uniform
194/// providers; unknown distributions currently return their minimum.
195fn sample_monster_spawn_light_test(
196    level: &MonsterSpawnLightLevel,
197    random: &mut impl Random,
198) -> i32 {
199    match level {
200        MonsterSpawnLightLevel::Simple(level) => *level,
201        MonsterSpawnLightLevel::Complex {
202            distribution_type,
203            min_inclusive,
204            max_inclusive,
205        } => {
206            if *distribution_type == "minecraft:uniform" {
207                random.next_i32_between(*min_inclusive, *max_inclusive)
208            } else {
209                *min_inclusive
210            }
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests;