Merge pull request #10001 from Ragbie/fix/enderman-teleport-danger

Endermen teleport only where vanilla lets them
This commit is contained in:
Kacper authored and GitHub committed 2026-11-06 09:11:31 +01:00
commit 8504efabb8
5 files changed
+148 -39

No files matched your search

+2 -1
View File
@@ -2164,7 +2164,7 @@ impl Game<'_> {
// A mob's own `gameEvent(..)` calls: Armadillo.rollUp/rollOut's
// ENTITY_ACTION, Creeper.tick's PRIME_FUSE, Mob.ate's EAT, the
// shulker's lid (Shulker.setRawPeekAmount), an evoker's vexes
// (ENTITY_PLACE).
// (ENTITY_PLACE), Enderman.teleport's TELEPORT where it was.
MobEvent::GameEvent {
event, at, source, ..
} if matches!(
@@ -2175,6 +2175,7 @@ impl Game<'_> {
| "minecraft:container_open"
| "minecraft:container_close"
| "minecraft:entity_place"
| "minecraft:teleport"
) =>
{
let ctx = self.who_context(Some(Who::Mob(source)), None);
+28
View File
@@ -3326,6 +3326,25 @@ impl Mob {
source: Damage,
armor_effectiveness: Option<&mut dyn FnMut(f32) -> f32>,
player_uuid: Option<[i32; 4]>,
) -> bool {
// Enderman.hurtServer: past `isInvulnerableTo`, the teleport draw
// follows `super.hurtServer` whatever it answered (a hurt cooldown, a
// dead enderman), its hurt sound's pitch included.
let enderman = self.species() == Species::Enderman
&& !self.removed
&& !(self.invulnerable_flag && !source.kind.bypasses_invulnerability());
let hurt = self.hurt_server(amount, source, armor_effectiveness, player_uuid);
if enderman {
enderman::after_hurt(self, &source);
}
hurt
}
fn hurt_server(
&mut self,
amount: f32,
source: Damage,
armor_effectiveness: Option<&mut dyn FnMut(f32) -> f32>,
player_uuid: Option<[i32; 4]>,
) -> bool {
axolotl::before_damage(self, amount, &source);
raider::note_hurt(self);
@@ -3599,7 +3618,16 @@ impl Mob {
/// `damage` by a source whose direct entity is `direct` (an entity type id), which `source`
/// cannot carry: `DamageSources.indirectMagic(potion or cloud, owner)`.
pub fn damage_by(&mut self, amount: f32, source: Damage, direct: &'static str) -> bool {
// Enderman.hurtServer: only a thrown potion skips the teleport draw;
// a lingering cloud's `indirect_magic` goes through it.
let cloud = self.species() == Species::Enderman
&& direct == "minecraft:area_effect_cloud"
&& !self.removed
&& !(self.invulnerable_flag && !source.kind.bypasses_invulnerability());
let hurt = self.damage(amount, source);
if cloud {
enderman::teleport_chance(self, &source);
}
if hurt {
self.last_direct = Some(direct);
}
+27 -5
View File
@@ -711,6 +711,10 @@ pub fn tick(
if horse {
super::equine::ai_step(m, w, events);
}
// LivingEntity.aiStep ends in the water's hurt with or without AI;
// a NoAI enderman's hurts still teleport it (Enderman.hurtServer).
hurt_by_water(m, w, events);
enderman_hurt_teleports(m, w, events);
// Ravager.aiStep runs outside serverAiStep: NoAI still advances
// attack/stun/roar, including the stun-to-roar transition.
if m.species() == Species::Ravager {
@@ -1241,10 +1245,6 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
}
}
}
if m.kind.flags.water_sensitive && (m.fluids.in_water || in_rain(m, w)) {
m.damage(1.0, Damage::of(DamageKind::Drown));
}
freeze_tick(m, w);
}
if m.species() == Species::Illusioner {
@@ -1266,6 +1266,8 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
// lava ignites to the full 160 / 300 ticks and its hit lands after the
// invulnerable time of that tick has already dropped.
block_contact_damage(m, w, fire);
// Enderman.hurtServer teleports inside the hurt (fire, lava, a block).
enderman_hurt_teleports(m, w, events);
if !m.alive() {
m.death_time += 1;
}
@@ -1630,10 +1632,13 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
} else if !(m.species() == Species::Mannequin && m.extra.immovable) {
// Mannequin.isEffectiveAi: an immovable one never travels.
m.travel(w, input, events);
// A fall's hurt (checkFallDamage) teleports at once.
enderman_hurt_teleports(m, w, events);
}
// LivingEntity.aiStep: applyEffectsFromBlocks follows travel.
m.apply_effects_from_blocks(w, events);
push_entities(m, w, events);
hurt_by_water(m, w, events);
// Spider.tick: climbing while pressed against a wall.
if m.kind.flags.climber {
m.climbing = m.horizontal_collision;
@@ -1643,6 +1648,24 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
burn_undead(m, w, events);
species_ai_step(m, w, level_random, events);
}
/// The end of `LivingEntity.aiStep`: a mob that water hurts (an enderman, a
/// blaze, a snow golem) takes 1 drowning damage in water or rain
/// (`isSensitiveToWater`, `isInWaterOrRain`). An enderman's teleport is part of
/// that hurt (`Enderman.hurtServer`), so it happens now, not next tick.
fn hurt_by_water(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if m.kind.flags.water_sensitive && (m.fluids.in_water || in_rain(m, w)) {
m.damage(1.0, Damage::of(DamageKind::Drown));
enderman_hurt_teleports(m, w, events);
}
}
/// The teleport `Enderman.hurtServer` makes inside a hurt, after the hurt's
/// sound; with or without AI.
fn enderman_hurt_teleports(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if m.species() == Species::Enderman && m.extra.counter2 > 0 {
events.append(&mut m.outbox);
super::enderman::run_pending_teleports(m, w, events);
}
}
impl Mob {
/// LivingEntity.isPushable (and the push it takes from `Entity.push(Entity)`):
/// bats and shulkers are never moved by others, nor is a mob on a climbable
@@ -2562,7 +2585,6 @@ pub fn accepts_damage(m: &mut Mob, source: &Damage, _amount: f32) -> bool {
pub fn after_hurt(m: &mut Mob, source: &Damage) {
m.ai.no_action_time = 0;
match m.species() {
Species::Enderman => super::enderman::after_hurt(m, source),
Species::Armadillo => super::armadillo::after_hurt(m, source),
Species::Shulker => super::shulker::after_hurt(m),
Species::Warden => super::warden::hurt(m, source),
+71 -33
View File
@@ -71,8 +71,12 @@ pub fn stared_at(m: &Mob, w: &MobWorld) -> bool {
Vec3::new(m.position.x, m.eye_y(), m.position.z),
)
}
/// Enderman.teleport(x, y, z): never while riding something; drop to standing
/// ground, require free space without liquid.
/// `Enderman.teleport(x, y, z)`: never while riding something, then
/// `LivingEntity.randomTeleport(x, y, z, true, #enderman_does_not_teleport_to)`:
/// the first `#entities_can_teleport_to` block below the spot is the ground,
/// and the enderman lands at the spot's own height less the blocks it went
/// down (`checkPositionAndTeleport`), unless the ground or a block in its new
/// box is one it does not teleport to, the box collides or holds a liquid.
pub fn teleport_to(
m: &mut Mob,
w: &MobWorld,
@@ -84,8 +88,8 @@ pub fn teleport_to(
if m.extra.passenger {
return false;
}
// LivingEntity.randomTeleport: WorldBorder.clampVec3ToBound keeps the
// target inside the border (Mth.clamp, 1.0E-5F inside the far edges).
// WorldBorder.clampVec3ToBound keeps the target inside the border
// (Mth.clamp, 1.0E-5F inside the far edges).
let (x, z) = match w.border {
Some(border) => {
let [min_x, min_z, max_x, max_z] = border.bounds(0.0);
@@ -101,40 +105,71 @@ pub fn teleport_to(
if !w.world.loaded(bx, bz) {
return false;
}
let refused = |name: &str| w.has_tag("minecraft:enderman_does_not_teleport_to", name);
let mut by = y.floor() as i32;
while by > w.world.bottom_y() && !w.facts.get(w.world.block(bx, by, bz)).full_collision {
let mut ny = y;
let ground = loop {
if by <= w.world.bottom_y() {
return false;
}
by -= 1;
}
let ground = w.facts.get(w.world.block(bx, by, bz));
if !ground.full_collision || ground.fluid == Fluid::Water {
let name = w.block_name(bx, by, bz);
if w.has_tag("minecraft:entities_can_teleport_to", name) {
break name;
}
ny -= 1.0;
};
if refused(ground) {
return false;
}
let ny = f64::from(by + 1);
let half = f64::from(m.width()) / 2.0;
let b = Aabb::new(
Vec3::new(x - half, ny, z - half),
Vec3::new(x + half, ny + f64::from(m.height()), z + half),
);
if !w.no_collision(b) || w.fluids(b, ny + m.eye_height()).in_water || w.fluids(b, ny).in_lava {
if !w.no_collision(b) {
return false;
}
let old = m.center();
// LevelReader.containsAnyLiquid and findBlocksIn: the same closed range of
// cells, from BlockPos.containing(min) to BlockPos.containing(max).
let (lo, hi) = (b.min_position(), b.max_position());
for cx in lo.x.floor() as i32..=hi.x.floor() as i32 {
for cy in lo.y.floor() as i32..=hi.y.floor() as i32 {
for cz in lo.z.floor() as i32..=hi.z.floor() as i32 {
if w.fluid(cx, cy, cz) != Fluid::None || refused(w.block_name(cx, cy, cz)) {
return false;
}
}
}
}
// Enderman.canRandomlyTeleportTo: not onto ground that holds water
// (a waterlogged slab, leaves or roots).
if w.fluid(bx, by, bz) == Fluid::Water {
return false;
}
let old = m.position;
let old_block = [
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
];
// Entity.teleportTo -> snapTo: the old position moves too, the motion stays.
m.position = Vec3::new(x, ny, z);
m.previous = m.position;
m.velocity = Vec3::ZERO;
m.ai.nav.stop();
// `LivingEntity.randomTeleport(..., true)` broadcasts entity event 46
// before the trail and the teleport sounds.
// `randomTeleport(..., true)` broadcasts entity event 46.
events.push(MobEvent::EntityEvent {
mob: m.id,
event: 46,
at: [m.position.x, m.position.y, m.position.z],
});
// Enderman.teleport: GameEvent.TELEPORT where it was, silent or not.
events.push(MobEvent::GameEvent {
event: "minecraft:teleport",
at: [old.x, old.y, old.z],
source: m.id,
state: horizon_world::registry::BlockStateId(0),
});
if !m.silent {
// Level event 2018: the portal trail, from the old block to the new
// one, packed as `BlockUtil.clampedPackDifferenceInPosition` does.
@@ -145,14 +180,12 @@ pub fn teleport_to(
pos: old_block,
data: packed(0) << 16 | packed(1) << 8 | packed(2),
});
// Both sounds are at the new feet: `xo` was moved by snapTo, and
// Entity.playSound plays at the position.
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::Sound("entity.enderman.teleport", at, 1.0, 1.0));
events.push(MobEvent::Sound("entity.enderman.teleport", at, 1.0, 1.0));
}
events.push(MobEvent::Sound("entity.enderman.teleport", old, 1.0, 1.0));
events.push(MobEvent::Sound(
"entity.enderman.teleport",
m.center(),
1.0,
1.0,
));
true
}
pub fn teleport_random(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) -> bool {
@@ -226,20 +259,29 @@ pub fn on_damage(m: &mut Mob, source: &Damage) -> bool {
true
}
pub fn after_hurt(m: &mut Mob, source: &Damage) {
// A thrown potion's hit (`indirect_magic`, which only a splash potion
// deals to a mob) leaves `Enderman.hurtServer` before the random chance:
// A thrown potion's hit (`indirect_magic` with the potion as the direct
// entity, `Mob::damage_by`) and a projectile it dodges leave
// `Enderman.hurtServer` before the random chance:
// `repeatedlyTryToTeleport` replaces it.
if source.kind != super::DamageKind::IndirectMagic
&& source.attacker.is_none()
&& m.random.next_int_bound(10).unwrap_or(0) != 0
{
if !dodges(m, source) && source.kind != super::DamageKind::IndirectMagic {
teleport_chance(m, source);
}
}
/// `Enderman.hurtServer`'s `!(source.getEntity() instanceof LivingEntity) &&
/// random.nextInt(10) != 0`: a teleport after the hurt.
pub fn teleport_chance(m: &mut Mob, source: &Damage) {
if source.attacker.is_none() && m.random.next_int_bound(10).unwrap_or(0) != 0 {
m.extra.counter2 = 1;
}
}
/// Pending teleports (repeatedlyTryToTeleport: up to 64 tries).
/// Enderman.aiStep: never jumps; the teleports a hurt left run.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
// Enderman.aiStep: never jumps.
m.ai.jumping = false;
run_pending_teleports(m, w, events);
}
/// The teleports `hurtServer` asked for (`teleport()`, or
/// `repeatedlyTryToTeleport`: up to 64 tries).
pub fn run_pending_teleports(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if m.extra.counter2 > 0 {
let tries = m.extra.counter2;
m.extra.counter2 = 0;
@@ -256,10 +298,6 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
}
}
}
if (m.fluids.in_water || super::ai::in_rain(m, w)) && m.alive() {
// Hurt by water: teleport away.
m.extra.counter2 = m.extra.counter2.max(1);
}
}
/// EndermanModel.setupAnim: half-speed clamped limbs, held-block arms, creepy head.
/// Humanoid riding pose runs first (`isPassenger`); only xRot is halved/clamped after.
+20
View File
@@ -113,6 +113,26 @@ def scenarios():
for i, (x, seed) in enumerate([(0.5, 240), (6.5, 241)]):
add(f'iron_golem_village_{i}', 400, VILLAGE + [f'mob minecraft:iron_golem {x} -60 3.5 0 {seed} persistent'], 'difficulty=0')
add('iron_golem_outside_village', 400, VILLAGE + ['mob minecraft:iron_golem 30.5 -60 3.5 0 242 persistent'], 'difficulty=0')
# An enderman in the rain teleports away again and again (LivingEntity.randomTeleport): it lands
# on the first #entities_can_teleport_to block below the spot, at the spot's own height less the
# blocks it went down, never into a liquid, and not onto or into #enderman_does_not_teleport_to.
# The open field stops before a teleport lands past the probe's loaded 9x9 chunks.
add('enderman_rain_teleports', 46, ['mob minecraft:enderman 0.5 -60 0.5 0 351 persistent'], 'rain')
add('enderman_rain_dangers', 300, [
'fill -32 -61 -32 -1 -61 32 minecraft:magma_block',
'fill 1 -60 -32 32 -60 -1 minecraft:sweet_berry_bush[age=3]',
'fill 1 -60 1 32 -60 32 minecraft:wither_rose',
'fill -8 -61 -8 -2 -61 8 minecraft:bedrock',
'mob minecraft:enderman 0.5 -60 0.5 0 352 persistent',
], 'rain')
# Not onto ground that holds water (Enderman.canRandomlyTeleportTo): a floor of waterlogged slabs.
add('enderman_rain_waterlogged', 190, [
'fill -32 -61 -32 32 -61 32 minecraft:oak_slab[type=top,waterlogged=true]',
'fill -3 -61 -3 3 -61 3 minecraft:stone',
'mob minecraft:enderman 0.5 -60 0.5 0 353 persistent',
], 'rain')
# A NoAI enderman still teleports out of the rain's hurt (Enderman.hurtServer).
add('enderman_rain_noai', 46, ['mob minecraft:enderman 0.5 -60 0.5 0 354 persistent noai'], 'rain')
# RandomStandGoal (rearing, with the ambient sound) of every equine, left alone for 800 ticks.
for i, species in enumerate(['horse', 'donkey', 'mule', 'zombie_horse', 'skeleton_horse', 'llama']):
# The zombie horse at night: by day it burns, and a burning horse's hurts are another matter.