Wolves: any target makes a wolf angry, so an alerted pack looks angry

Wolf.aiStep ends with NeutralMob.updatePersistentAnger(level, true): a
wolf with a target is angry at it, and the 20-39 s timer
(PERSISTENT_ANGER_TIME) starts over every tick it holds one. Horizon only
set the anger in hurtServer when the player hit the wolf, so the pack
mates HurtByTargetGoal alerted attacked without the angry texture, and the
random draws of an angry wolf came in the wrong order. isAngryAt(player)
now reads the anger's target, and stopBeingAngry ends it when the time
runs out, the target mob dies, or the player is in creative, spectator
or peaceful. Saving a wolf keeps that anger target instead of replacing
it with the current target.

The mob_ai harness prints a wolf's anger and gets a wolf_hurt scenario.

Fixes #3185

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
d3grandoandClaude Opus 5.5 committed 2026-10-29 15:58:42 +07:00
1 parent 11494e17f9
commit e086b2cfdd
5 files changed
+89 -15

No files matched your search

@@ -725,6 +725,15 @@ fn print(out: &mut String, t: i32, i: usize, m: &Mob, player_down: bool, now: i6
.map_or("-".into(), |p| format!("{},{},{}", p[0], p[1], p[2]));
let _ = write!(out, "\tcg {} {held} {opened}", m.extra.variant3);
}
if m.species() == Species::Wolf {
// NeutralMob anger left (the angry texture), -1 when calm.
let anger = if m.extra.anger > 0 {
i64::from(m.extra.anger)
} else {
-1
};
let _ = write!(out, "\twolf {anger}");
}
if let Some(b) = horizon_client::mobs::bee::get(m) {
let anger = if b.anger_end > 0 {
b.anger_end - now
+7 -1
View File
@@ -547,7 +547,13 @@ impl Game<'_> {
-1
};
self.mobs[i].extra.angry_at = if anger > 0 {
self.mobs[i].ai.target
// 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 {
self.mobs[i].extra.angry_at
} else {
self.mobs[i].ai.target
}
} else {
None
};
+64 -14
View File
@@ -149,8 +149,9 @@ fn wolf_prey(s: &super::Seen) -> bool {
fn baby_turtle_on_land(s: &super::Seen) -> bool {
s.species == Species::Turtle && s.baby
}
/// `NeutralMob.isAngryAt(player)`: the player is the anger's target.
fn angry_at_player(m: &Mob, _: &MobWorld) -> bool {
m.extra.anger > 0
m.extra.angry_at == Some(Who::Player)
}
/// `Wolf.isAngry`.
pub fn angry(m: &Mob) -> bool {
@@ -574,14 +575,66 @@ impl Goal for Beg {
self.ticks -= 1;
}
}
/// `Wolf.tick` / `aiStep`: anger countdown, begging tilt and shaking off water.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if m.extra.anger > 0 {
m.extra.anger -= 1;
if m.extra.anger == 0 && m.ai.target == Some(Who::Player) {
m.ai.target = None;
}
/// `NeutralMob.isValidPlayerTarget`: a player in survival or adventure, off
/// peaceful.
fn valid_player_target(w: &MobWorld) -> bool {
w.player
.as_ref()
.is_some_and(|p| !p.creative && !p.spectator)
&& w.difficulty.id != 0
}
/// `NeutralMob.stopBeingAngry`.
fn stop_being_angry(m: &mut Mob) {
// setLastHurtByMob(null) also stamps the time.
m.ai.last_hurt_by = None;
m.ai.last_hurt_by_time = m.age as i32;
m.ai.target = None;
m.extra.angry_at = None;
m.extra.anger = 0;
}
/// `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.
/// 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) {
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)
&& angry_at == Some(target)
{
stop_being_angry(m);
return;
}
// Mob.getTarget: a player in creative or spectator mode is no target.
let target = m.ai.target.filter(|t| {
*t != Who::Player
|| w.player
.as_ref()
.is_some_and(|p| !p.creative && !p.spectator)
});
if let Some(target) = target {
m.extra.angry_at = Some(target);
// Wolf.startPersistentAngerTimer: TimeUtil.rangeOfSeconds(20, 39).
m.extra.anger = 400 + m.random.next_int_bound(381).unwrap_or(0);
} else if m.extra.anger > 0 {
m.extra.anger -= 1;
}
if angry_at.is_some()
&& m.extra.anger == 0
&& !(target == Some(Who::Player) && valid_player_target(w))
{
stop_being_angry(m);
}
// The anger's target as it was when the tick began.
if angry_at == Some(Who::Player) && !valid_player_target(w) {
stop_being_angry(m);
}
}
/// `Wolf.tick` / `aiStep`: anger, begging tilt and shaking off water.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
update_persistent_anger(m, w);
if !m.alive() {
return;
}
@@ -628,13 +681,10 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
m.anim.vo[SHAKE] = 0.0;
}
}
/// `Wolf.hurtServer` and `NeutralMob` anger: stop sitting; untamed wolves
/// hit by the player stay angry for 20 to 39 seconds.
pub fn after_hurt(m: &mut Mob, source: &Damage) {
/// `Wolf.hurtServer`: stop sitting. The anger comes with the target
/// (`HurtByTargetGoal`, then `updatePersistentAnger`).
pub fn after_hurt(m: &mut Mob, _source: &Damage) {
m.extra.sitting = false;
if source.attacker == Some(Who::Player) && !m.extra.tame {
m.extra.anger = 400 + m.random.next_int_bound(380).unwrap_or(0);
}
}
/// `Wolf.getTailAngle`.
fn tail_angle(m: &Mob) -> f32 {
@@ -1072,6 +1072,11 @@ public final class VanillaMobAiOracle {
sb.append("\tcg ").append(golem.getState().ordinal()).append(' ').append(stack(golem.getMainHandItem())).append(' ')
.append(opened == null ? "-" : pos((BlockPos) opened));
}
if (m instanceof net.minecraft.world.entity.animal.wolf.Wolf wolf) {
// NeutralMob anger left (the angry texture), -1 when calm.
long end = wolf.getPersistentAngerEndTime();
sb.append(" wolf ").append(end > 0 ? end - wolf.level().getGameTime() : -1);
}
if (m instanceof net.minecraft.world.entity.animal.bee.Bee bee) {
// Flags (roll 2, stung 4, nectar 8), anger left, ticks without nectar, flower cooldown, flower.
Object acc = field(net.minecraft.world.entity.animal.bee.Bee.class, "DATA_FLAGS_ID").get(null);
+4
View File
@@ -272,6 +272,10 @@ def scenarios():
add('bee_hive_rain', 300, [HIVE, 'mob minecraft:bee 0.5 -58 0.5 0 162 persistent'], 'rain')
add('bee_hive_morning', 1000, [HIVE, 'mob minecraft:bee 0.5 -58 0.5 0 163 persistent'], 'time=22900 daycycle')
add('bee_hive_honey', 2600, [HIVE, 'mob minecraft:bee 0.5 -58 0.5 0 164 persistent nectar'])
# A wolf the player hits (#3185, #2820): any wolf with a target is angry (the angry texture):
# NeutralMob.updatePersistentAnger in Wolf.aiStep restarts the 20-39 s timer, one random draw,
# every tick it has one, not once when hurt.
add('wolf_hurt', 200, ['player 5.5 -60 0.5', 'mob minecraft:wolf 0.5 -60 0.5 0 181 persistent', 'hurt 20 0'])
# Mobs dropped into deep water (#542): FloatGoal users float at the surface (their navigation
# can float, so Mob.jumpInLiquid adds 0.04 a tick), zombies sink.
for species, seed in [('pig', 171), ('cow', 172), ('wolf', 173), ('cat', 174), ('horse', 175),