mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10001 from Ragbie/fix/enderman-teleport-danger
Endermen teleport only where vanilla lets them
This commit is contained in:
5 files changed
+148
-39
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user