Merge pull request #9952 from Ragbie/fix/iron-golem-toss

Iron golems toss by knockback resistance and keep their anger like vanilla
This commit is contained in:
rdbt authored and GitHub committed 2026-11-06 21:26:25 +00:00
commit ef78519011
10 files changed
+263 -90

No files matched your search

@@ -607,7 +607,12 @@ fn player_damage(e: &MobEvent) -> f32 {
| MobEvent::CubeHurtPlayer { amount, .. }
| MobEvent::StingPlayer { amount, .. }
| MobEvent::AttackPlayerWithEffect { amount, .. }
| MobEvent::DamagePlayer { amount, .. } => *amount,
| MobEvent::DamagePlayer { amount, .. }
| MobEvent::IronGolemHit {
target: Who::Player,
amount,
..
} => *amount,
MobEvent::SonicBoom {
target: Who::Player,
..
@@ -626,7 +631,12 @@ fn player_hurt(player: Option<&PlayerView>, e: &MobEvent) -> bool {
| MobEvent::CubeHurtPlayer { amount, .. }
| MobEvent::StingPlayer { amount, .. }
| MobEvent::AttackPlayerWithEffect { amount, .. }
| MobEvent::DamagePlayer { amount, .. } => *amount > 0.0,
| MobEvent::DamagePlayer { amount, .. }
| MobEvent::IronGolemHit {
target: Who::Player,
amount,
..
} => *amount > 0.0,
MobEvent::SonicBoom {
target: Who::Player,
..
@@ -1391,14 +1401,28 @@ pub fn main() -> horizon_client::Result<()> {
// (LivingEntity.hurtServer), with the sounds they raise.
let mut k = from;
while k < events.len() {
if let MobEvent::AttackMob {
target,
amount,
source,
} = events[k]
let (hit, toss) = match events[k] {
MobEvent::AttackMob {
target,
amount,
source,
} => (Some((target, amount, source)), false),
// IronGolem.doHurtTarget: thrown up only if the hit landed.
MobEvent::IronGolemHit {
target: Who::Mob(target),
amount,
source,
} => (Some((target, amount, source)), true),
_ => (None, false),
};
if let Some((target, amount, source)) = hit
&& let Some(t) = mobs.iter_mut().find(|m| m.id == target)
&& t.damage(amount, source)
{
if toss {
t.velocity.y +=
f64::from(0.4f32) * (1.0 - t.knockback_resistance()).max(0.0);
}
let raised = std::mem::take(&mut t.outbox);
let n = raised.len();
events.splice(k + 1..k + 1, raised);
+42 -8
View File
@@ -492,8 +492,19 @@ pub fn mob_to_nbt(
-1
}),
);
if let Some(uuid) = m.extra.angry_at.and_then(|who| who_uuid(who, &mut uuid_of)) {
put(&mut tag, "angry_at", Tag::IntArray(uuid.to_vec()));
// `angry_at` only while there is an anger target: the UUID of the one
// the level has (`uuid_of`). A mob target read from the save that is
// not in the world (`Who::Mob(0)`) keeps the loaded UUID.
match m.extra.angry_at {
Some(crate::mobs::Who::Mob(0)) => {}
target => match target.and_then(|who| who_uuid(who, &mut uuid_of)) {
Some(uuid) => put(&mut tag, "angry_at", Tag::IntArray(uuid.to_vec())),
None => {
if let Tag::Compound(map) = &mut tag {
map.remove(&"angry_at".into());
}
}
},
}
}
// `Mannequin.addAdditionalSaveData`: the profile whose skin it wears, whether it
@@ -1750,12 +1761,13 @@ pub fn mob_from_nbt(
// target come back with the mob.
if persistent_anger(m.species()) {
m.extra.anger_end = number(tag.get("anger_end_time")).unwrap_or(-1);
if tag
.get("angry_at")
.is_some_and(|t| uuid_from_tag(Some(t)) == Some(player_uuid()))
{
m.extra.angry_at = Some(crate::mobs::Who::Player);
}
// Anger at an entity other than the player is anger at a mob that may
// not be here (`EntityReference`): it still is not `isAngryAtAllPlayers`.
m.extra.angry_at = match uuid_from_tag(tag.get("angry_at")) {
Some(uuid) if uuid == player_uuid() => Some(crate::mobs::Who::Player),
Some(_) => Some(crate::mobs::Who::Mob(0)),
None => None,
};
}
// `Mannequin.readAdditionalSaveData`: the profile comes back with the skin, then
// validated pose, `immovable`, `hide_description` and `description` (`updateDescription`).
@@ -7335,6 +7347,28 @@ mod neutral_anger_tests {
let back = mob_from_nbt(&tag, &types, 2, None).unwrap();
assert_eq!(back.extra.anger_end, 3000);
assert_eq!(back.extra.angry_at, Some(crate::mobs::Who::Player));
// An iron golem angry at a monster: the target's UUID comes back as
// anger at a mob (not at every player under universal_anger), and is
// written back as it was read.
let mut golem = Mob::new(4, types["minecraft:iron_golem"].clone(), Vec3::ZERO, 0.0);
golem.extra.anger = 400;
golem.extra.anger_end = 3000;
let mut raw = mob_to_nbt(&golem, [1, 2, 3, 6], None, None, |_| None);
if let Tag::Compound(map) = &mut raw {
map.insert("angry_at".into(), Tag::IntArray(vec![9, 8, 7, 6]));
}
let back = mob_from_nbt(&raw, &types, 5, None).unwrap();
assert_eq!(back.extra.angry_at, Some(crate::mobs::Who::Mob(0)));
let again = mob_to_nbt(&back, [1, 2, 3, 6], Some(&raw), None, |_| None);
assert_eq!(
crate::save::uuid_from_tag(again.get("angry_at")),
Some([9, 8, 7, 6])
);
// Once the anger is over (`stopBeingAngry`), the loaded target goes.
let mut calm = back.clone();
calm.extra.angry_at = None;
let again = mob_to_nbt(&calm, [1, 2, 3, 6], Some(&raw), None, |_| None);
assert!(again.get("angry_at").is_none());
// A mob that is no NeutralMob writes no anger.
let pig = Mob::new(3, types["minecraft:pig"].clone(), Vec3::ZERO, 0.0);
let tag = mob_to_nbt(&pig, [1, 2, 3, 5], None, None, |_| None);
+38 -10
View File
@@ -2552,18 +2552,46 @@ impl Game<'_> {
MobEvent::Tamed { .. } | MobEvent::Buck { .. } | MobEvent::StabDismount { .. } => {
self.mount_event(&e)
}
MobEvent::Launch { target, up } => match target {
Who::Player => {
if let Some(p) = self.player.as_mut() {
p.velocity.y += up;
MobEvent::IronGolemHit {
target,
amount,
source,
} => {
// IronGolem.doHurtTarget: the toss only follows a hit that
// landed, scaled by the target's KNOCKBACK_RESISTANCE.
let hurt = match target {
Who::Player => {
let from = source.from.unwrap_or_default();
self.mob_hits_player(amount, from, "minecraft:mob_attack")
}
Who::Mob(id) => self.mob_hits_mob(id, amount, source),
};
if hurt {
let resistance = match target {
Who::Player => self.player.as_mut().map_or(0.0, |p| {
p.attribute("minecraft:knockback_resistance").unwrap_or(0.0)
}),
Who::Mob(id) => self
.mobs
.iter()
.find(|m| m.id == id)
.map_or(0.0, |m| self.mob_knockback_resistance(m)),
};
let up = f64::from(0.4f32) * (1.0 - resistance).max(0.0);
match target {
Who::Player => {
if let Some(p) = self.player.as_mut() {
p.velocity.y += up;
}
}
Who::Mob(id) => {
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {
m.velocity.y += up;
}
}
}
}
Who::Mob(id) => {
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {
m.velocity.y += up;
}
}
},
}
MobEvent::Bonemeal { pos } => {
let at = BlockPos::new(pos[0], pos[1], pos[2]);
if self.with_level(|b, level, ticks| b.grow_crop(level, ticks, at)) {
+5 -3
View File
@@ -43,7 +43,7 @@ pub(super) fn player_for(
// An iron golem's anger is for the player who hit it (`NeutralMob` keeps
// the angry-at UUID), and outlasts the 100 ticks of `getLastHurtByMob`:
// while it lasts, the golem keeps that player, not whoever is nearest.
let angry = m.species() == Species::IronGolem && m.extra.anger > 0;
let angry = m.species() == Species::IronGolem && m.extra.angry_at == Some(Who::Player);
let targeting = m.ai.target == Some(Who::Player)
|| angry
|| m.ai.last_hurt_by == Some(Who::Player) && m.age as i32 - m.ai.last_hurt_by_time <= 100;
@@ -135,7 +135,7 @@ pub(super) fn acts_on_player(e: &MobEvent) -> bool {
target: Who::Player,
..
}
| MobEvent::Launch {
| MobEvent::IronGolemHit {
target: Who::Player,
..
}
@@ -570,11 +570,13 @@ mod tests {
golem.ai.last_hurt_by_time = 100;
golem.ai.target = None;
golem.extra.anger = 300;
golem.extra.angry_at = Some(Who::Player);
// The attacker (conn 7) ran off; a bystander (conn 8) is nearer.
let guests = vec![(7, view(30.0, false)), (8, view(3.0, false))];
assert_eq!(player_for(&golem, None, &guests, Some(7)), Some(7));
// Calm again: the nearest player is seen.
// Calm again (`stopBeingAngry`): the nearest player is seen.
golem.extra.anger = 0;
golem.extra.angry_at = None;
assert_eq!(player_for(&golem, None, &guests, Some(7)), Some(8));
}
}
+75 -3
View File
@@ -1168,9 +1168,12 @@ impl Game<'_> {
-1
};
self.mobs[i].extra.angry_at = if anger > 0 {
// A wolf keeps its anger's target itself, also once the target
// is gone (`tame::update_persistent_anger`).
if self.mobs[i].species() == horizon_client::mobs::Species::Wolf {
// A wolf and an iron golem keep their anger's target themselves,
// also once the target is gone (`tame::update_persistent_anger`).
if matches!(
self.mobs[i].species(),
horizon_client::mobs::Species::Wolf | horizon_client::mobs::Species::IronGolem
) {
self.mobs[i].extra.angry_at
} else {
self.mobs[i].ai.target
@@ -1178,6 +1181,14 @@ impl Game<'_> {
} else {
None
};
// A mob gets its UUID when it is first saved (`uuid_for`): the anger
// target still in the world gets it now, so `uuid_of` can name it.
if let Some(horizon_client::mobs::Who::Mob(target)) = self.mobs[i].extra.angry_at
&& target != 0
&& self.mobs.iter().any(|m| m.id == target)
{
self.uuid_for(target);
}
let mut tag = entity_nbt::mob_to_nbt(
&self.mobs[i],
uuid,
@@ -2315,6 +2326,67 @@ mod tests {
assert!(!take_fresh(&mut fresh));
assert_eq!(fresh, Tag::Compound(Default::default()));
}
/// `NeutralMob.addPersistentAngerSaveData` names the mob the anger is at,
/// also one never saved yet (a mob gets its UUID when it is first saved).
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn anger_at_a_mob_not_saved_yet_names_it() {
use crate::game::{Audio, Options, Progress, Settings, load_resources};
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
let settings = Settings::default();
let loaded = load_resources(
root.clone(),
None,
&root,
&settings,
std::time::Instant::now(),
false,
true,
&Progress::new(),
&mut |_, _| unreachable!(),
)
.unwrap();
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
options.resources = root.clone();
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
let mut game = Game::new(
&loaded.resources,
loaded.pack,
loaded.gui.unwrap(),
options,
settings,
Audio::silent(&root),
None,
)
.unwrap();
let types = game.res.mob_types();
let golem = horizon_client::mobs::Mob::new(
1,
types.get("minecraft:iron_golem").unwrap().clone(),
Vec3::ZERO,
0.0,
);
let zombie = horizon_client::mobs::Mob::new(
2,
types.get("minecraft:zombie").unwrap().clone(),
Vec3::new(3.0, 0.0, 0.0),
0.0,
);
game.mobs.push(golem);
game.mobs.push(zombie);
game.mobs[0].extra.anger = 400;
game.mobs[0].extra.angry_at = Some(horizon_client::mobs::Who::Mob(2));
assert_eq!(game.persist.uuid_of(2), None);
let tag = game.mob_tree_nbt(0);
let Some(Tag::IntArray(named)) = tag.get("angry_at") else {
panic!("no angry_at in {tag:?}");
};
assert_eq!(
Some(named.as_slice()),
game.persist.uuid_of(2).as_ref().map(|u| &u[..])
);
}
}
#[cfg(test)]
mod interaction_saved_defaults_tests {
+6 -3
View File
@@ -2179,10 +2179,13 @@ pub enum MobEvent {
FinishAdmiring {
mob: u32,
},
/// Throw an entity upward (iron golem swings).
Launch {
/// `IronGolem.doHurtTarget`: the hit, then, only when `hurtServer` let it
/// land, the target thrown up by `0.4f` scaled by
/// `max(0, 1 - KNOCKBACK_RESISTANCE)`.
IronGolemHit {
target: Who,
up: f64,
amount: f32,
source: Damage,
},
/// Damage the mob itself (snow golems melting).
HurtSelf {
+36 -30
View File
@@ -702,6 +702,12 @@ pub fn tick(
if horse {
super::equine::tail_tick(m);
}
// LivingEntity.aiStep: without AI there is no serverAiStep and no
// travel, but the motion below 0.003 is still dropped and
// pushEntities still runs, so a NoAI mob pushes what it overlaps
// (and is pushed back by its own push).
drop_tiny_motion(m);
push_entities(m, w, events);
ageable_step(m, w, level_random, events);
animal_step(m, events);
if m.species() == Species::Chicken {
@@ -1596,12 +1602,7 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
if m.ai.no_jump_delay > 0 {
m.ai.no_jump_delay -= 1;
}
let v = m.velocity;
m.velocity = Vec3::new(
if v.x.abs() < 0.003 { 0.0 } else { v.x },
if v.y.abs() < 0.003 { 0.0 } else { v.y },
if v.z.abs() < 0.003 { 0.0 } else { v.z },
);
drop_tiny_motion(m);
m.ai.xxa *= 0.98;
m.ai.zza *= 0.98;
// LivingEntity.isImmobile: no goals or input (a biting, stunned or
@@ -1696,6 +1697,16 @@ fn root_vehicle(w: &MobWorld, id: u32, vehicle: Option<u32>) -> u32 {
}
root
}
/// `LivingEntity.aiStep`, with or without AI: a mob's motion below 0.003 on an
/// axis stops.
fn drop_tiny_motion(m: &mut Mob) {
let v = m.velocity;
m.velocity = Vec3::new(
if v.x.abs() < 0.003 { 0.0 } else { v.x },
if v.y.abs() < 0.003 { 0.0 } else { v.y },
if v.z.abs() < 0.003 { 0.0 } else { v.z },
);
}
/// LivingEntity.pushEntities: every pushable entity overlapping the box is
/// pushed apart from this one (`doPush(e)` = `e.push(this)`); pushes of other
/// mobs are `Push` events their tick picks up.
@@ -2006,7 +2017,7 @@ fn species_ai_step(
Species::Wolf => super::tame::ai_step(m, w, events),
Species::Cat => super::tame::cat_ai_step(m, events),
Species::Blaze => super::nether::blaze_ai_step(m, events),
Species::IronGolem => super::golem::iron_ai_step(m),
Species::IronGolem => super::golem::iron_ai_step(m, w),
Species::Piglin | Species::PiglinBrute | Species::Hoglin => {
super::piglin::ai_step(m, w, level_random, events)
}
@@ -2606,7 +2617,6 @@ pub fn after_hurt(m: &mut Mob, source: &Damage) {
| Species::Drowned
| Species::ZombieVillager
| Species::ZombifiedPiglin => super::undead::after_hurt(m, source),
// NeutralMob: an iron golem hit by the player stays angry a while.
// PiglinAi.wasHurtBy: drop what is being admired, no admiring for
// a while after a player's hit, and anger.
Species::Piglin if source.attacker == Some(Who::Player) => {
@@ -2614,9 +2624,6 @@ pub fn after_hurt(m: &mut Mob, source: &Damage) {
m.extra.egg_time = 400;
m.extra.anger = 600;
}
Species::IronGolem if source.attacker == Some(Who::Player) => {
m.extra.anger = 400 + m.random.next_int_bound(380).unwrap_or(0);
}
_ => {}
}
}
@@ -2826,16 +2833,9 @@ pub fn forgive_dead_player(m: &mut Mob) {
m.ai.target = None;
}
}
// The iron golem keeps a timer and its target (the hurt-by goal
// after a player's hit).
Species::IronGolem => {
if m.extra.anger > 0 && m.ai.target == Some(Who::Player) {
m.ai.last_hurt_by = None;
m.ai.last_hurt_by_time = m.age as i32;
m.extra.anger = 0;
m.ai.target = None;
}
}
// The iron golem keeps its anger's target as a wolf does
// (`tame::update_persistent_anger`): `playerDied` ends the anger at
// that player whether or not it is the golem's target now.
_ => super::tame::forgive_dead_player(m),
}
}
@@ -4209,20 +4209,26 @@ mod forgiveness_tests {
}
/// The iron golem's anger is a timer and its target: the dying player
/// they were hurt by ends both, and anger at another mob stays.
/// it is angry at ends both (`NeutralMob.playerDied`), even once the golem
/// has lost sight of them, and anger at another mob stays.
#[test]
fn an_iron_golems_timer_and_target_end_together() {
let mut golem = mob(Species::IronGolem);
golem.extra.anger = 300;
golem.ai.target = Some(Who::Player);
golem.ai.last_hurt_by = Some(Who::Player);
forgive_dead_player(&mut golem);
assert_eq!(golem.extra.anger, 0);
assert_eq!(golem.ai.target, None);
assert_eq!(golem.ai.last_hurt_by, None);
for target in [Some(Who::Player), None] {
let mut golem = mob(Species::IronGolem);
golem.extra.anger = 300;
golem.extra.angry_at = Some(Who::Player);
golem.ai.target = target;
golem.ai.last_hurt_by = Some(Who::Player);
forgive_dead_player(&mut golem);
assert_eq!(golem.extra.anger, 0, "{target:?}");
assert_eq!(golem.extra.angry_at, None, "{target:?}");
assert_eq!(golem.ai.target, None, "{target:?}");
assert_eq!(golem.ai.last_hurt_by, None, "{target:?}");
}
let mut golem = mob(Species::IronGolem);
golem.extra.anger = 300;
golem.extra.angry_at = Some(Who::Mob(7));
golem.ai.target = Some(Who::Mob(7));
forgive_dead_player(&mut golem);
assert_eq!(golem.extra.anger, 300, "anger at another mob stays");
+14 -21
View File
@@ -96,7 +96,7 @@ pub fn register_iron(m: &mut Mob) {
3,
NearestTarget::player(true)
.interval(10)
.when(|m, _| m.extra.anger > 0),
.when(super::tame::angry_at_player),
);
m.ai.targets.add(
3,
@@ -399,17 +399,11 @@ pub fn iron_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
attack
};
let from = [m.position.x, m.position.y, m.position.z];
match who {
Who::Player => cx.events.push(MobEvent::AttackPlayer(amount, from)),
Who::Mob(id) => cx.events.push(MobEvent::AttackMob {
target: id,
amount,
source: super::Damage::mob(m.id, from),
}),
}
cx.events.push(MobEvent::Launch {
// The hit, then the toss only if it landed (game/creatures.rs).
cx.events.push(MobEvent::IronGolemHit {
target: who,
up: 0.4,
amount,
source: super::Damage::mob(m.id, from),
});
cx.events.push(MobEvent::Sound(
"entity.iron_golem.attack",
@@ -445,20 +439,17 @@ pub fn client_tick(m: &mut Mob) {
m.extra.counter -= 1;
}
}
/// `IronGolem.aiStep` timers and anger.
pub fn iron_ai_step(m: &mut Mob) {
/// `IronGolem.aiStep` timers and anger: `updatePersistentAnger(level, true)`,
/// as a wolf's, so any target (a monster too) restarts the anger timer with a
/// draw of the golem's random every tick it is held.
pub fn iron_ai_step(m: &mut Mob, w: &MobWorld) {
if m.extra.anim_ticks > 0 {
m.extra.anim_ticks -= 1;
}
if m.extra.counter > 0 {
m.extra.counter -= 1;
}
if m.extra.anger > 0 {
m.extra.anger -= 1;
if m.extra.anger == 0 && m.ai.target == Some(Who::Player) {
m.ai.target = None;
}
}
super::tame::update_persistent_anger(m, w);
}
/// `Crackiness.GOLEM` texture for the health left.
pub fn crackiness(m: &Mob) -> Option<&'static str> {
@@ -767,6 +758,7 @@ mod tests {
w.player = Some(test_player(Vec3::new(3.5, 64.0, 0.5)));
let mut golem = mob(1, Species::IronGolem, Vec3::new(0.5, 64.0, 0.5));
golem.extra.anger = 100;
golem.extra.angry_at = Some(Who::Player);
let mut events = Vec::new();
let mut lr = horizon_core::random::LegacyRandom::new(0);
let mut cx = Cx {
@@ -776,7 +768,7 @@ mod tests {
};
let mut goal = NearestTarget::player(true)
.interval(0)
.when(|m, _| m.extra.anger > 0);
.when(super::super::tame::angry_at_player);
assert!(goal.can_use(&mut golem, &mut cx));
goal.start(&mut golem, &mut cx);
assert_eq!(golem.ai.target, Some(Who::Player));
@@ -818,8 +810,9 @@ mod created_golem_tests {
let mut m = Mob::new(id, kind, Vec3::new(0.5, 64.0, 0.5), 0.0);
// `CarvedPumpkinBlock.trySpawnGolem`: setPlayerCreated(true).
m.extra.player_created = created;
// The anger a player's hit gives it (`after_hurt`).
// The anger at the player who hit it (`updatePersistentAnger`).
m.extra.anger = 400;
m.extra.angry_at = Some(Who::Player);
m
};
let zombie = Mob::new(
+6 -5
View File
@@ -606,13 +606,14 @@ pub fn forgive_dead_player(m: &mut Mob) {
stop_being_angry(m);
}
}
/// `NeutralMob.updatePersistentAnger(level, true)` from `Wolf.aiStep`: any
/// target (the attacker, a pack-mate's attacker the `HurtByTargetGoal` alert
/// gave it, prey, the owner's enemy) is the anger's target, and the 20 to 39
/// seconds of `PERSISTENT_ANGER_TIME` start over every tick it is held.
/// `NeutralMob.updatePersistentAnger(level, true)` from `Wolf.aiStep` and
/// `IronGolem.aiStep`: any target (the attacker, a pack-mate's attacker the
/// `HurtByTargetGoal` alert gave it, prey, the owner's enemy, a golem's
/// monster) is the anger's target, and the 20 to 39 seconds of
/// `PERSISTENT_ANGER_TIME` (the same for both) start over every tick it is held.
/// Without a target the anger runs out (`extra.anger` counts down to the end
/// time), and anger at a mob ends when that mob dies.
fn update_persistent_anger(m: &mut Mob, w: &MobWorld) {
pub(super) fn update_persistent_anger(m: &mut Mob, w: &MobWorld) {
let angry_at = m.extra.angry_at;
if let Some(target @ Who::Mob(id)) = m.ai.target
&& w.seen(id).is_none_or(|s| !s.alive)
+10
View File
@@ -113,6 +113,16 @@ 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')
# IronGolem.doHurtTarget throws what it hits up by 0.4 scaled by max(0, 1 - knockback resistance):
# all of it for a zombie, a quarter for a ravager (0.75), none for a warden (1). The zombie's
# scenario stops before the golem kills it (the probe has no target killed within the
# attacker's tick). A golem holding a target restarts its anger timer every tick
# (NeutralMob.updatePersistentAnger), and the NoAI targets still push (LivingEntity.aiStep).
for i, (species, ticks) in enumerate([('zombie', 48), ('ravager', 120), ('warden', 120)]):
add(f'iron_golem_tosses_{species}', ticks, [
f'mob minecraft:iron_golem 0.5 -60 0.5 270 {330 + i} persistent',
f'mob minecraft:{species} 3.5 -60 0.5 90 {340 + i} persistent noai',
], 'difficulty=2 time=18000')
# 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.