Merge pull request #9233 from glitchsnake/fix/8703-breeze-invulnerable-to-breeze

🤖 Agent: matches Breeze.isInvulnerableTo (source.getEntity() instanceof Breeze, the owner for projectiles); main merged (trident/bee conflicts taken from main plus attacker_species, and main's new damage_type test literal filled in). The older wind-charge special case in game/wind.rs now duplicates breeze::accepts_damage: follow-up. verify green at this head.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-04 17:59:16 +01:00
commit 6ddd14e727
20 files changed
+243 -6

No files matched your search

@@ -236,6 +236,7 @@ pub fn hurt_mobs_with_kind(mobs: &mut [Mob], position: Vec3, amount: f32, kind:
kind,
attacker: None,
from: Some([position.x, position.y, position.z]),
attacker_species: None,
};
for mob in mobs
.iter_mut()
@@ -210,10 +210,15 @@ impl Game<'_> {
Target::Mob(i) => Some(Who::Mob(i)),
_ => None,
});
let attacker_species = match attacker {
Some(Who::Mob(i)) => self.mobs.iter().find(|m| m.id == i).map(|m| m.species()),
_ => None,
};
let damage = Damage {
kind: Self::damage_kind(&ty),
attacker,
from,
attacker_species,
};
// DamageSource.getEntity is the causing player, even when
// another client issued the command or supplied the direct entity.
+14 -1
View File
@@ -1723,7 +1723,12 @@ impl Game<'_> {
}
}
/// Melee or ranged damage to another mob; whether the hit landed.
fn mob_hits_mob(&mut self, target: u32, amount: f32, source: Damage) -> bool {
fn mob_hits_mob(&mut self, target: u32, amount: f32, mut source: Damage) -> bool {
if source.attacker_species.is_none()
&& let Some(Who::Mob(id)) = source.attacker
{
source.attacker_species = self.mobs.iter().find(|m| m.id == id).map(|m| m.species());
}
let mut killed = false;
let mut hurt = false;
let mut infested = false;
@@ -2402,6 +2407,7 @@ impl Game<'_> {
kind: DamageKind::SonicBoom,
attacker: Some(Who::Mob(attacker)),
from: Some(from),
attacker_species: Some(Species::Warden),
};
let hurt = self.mob_hits_mob(id, 10.0, source);
let resistance = self
@@ -4733,10 +4739,15 @@ impl Game<'_> {
}
_ => DamageKind::Thrown,
};
let attacker_species = match p.owner {
Some(Who::Mob(id)) => self.mobs.iter().find(|m| m.id == id).map(|m| m.species()),
_ => None,
};
let source = Damage {
kind: source_kind,
attacker: p.owner,
from: Some([p.previous.x, p.previous.y, p.previous.z]),
attacker_species,
};
let aim = self.attacker_aim(source.attacker);
match (&p.kind, hit) {
@@ -7956,6 +7967,7 @@ mod damage_type_tests {
kind: DamageKind::Named(named),
attacker,
from: None,
attacker_species: None,
}),
"example:fan_fire"
);
@@ -7970,6 +7982,7 @@ mod damage_type_tests {
kind: DamageKind::Explosion,
attacker,
from: Some([0.0; 3]),
attacker_species: None,
};
let creeper = blast(Some(Who::Mob(7)));
assert_eq!(source_damage_type(creeper), "minecraft:player_explosion");
+2
View File
@@ -462,6 +462,7 @@ fn is_fire(kind: DamageKind) -> bool {
kind,
attacker: None,
from: None,
attacker_species: None,
};
source.is_fire()
}
@@ -1366,6 +1367,7 @@ impl Game<'_> {
kind,
attacker: by_player.then_some(Who::Player),
from: None,
attacker_species: None,
});
// `damageSources().explosion(crystal, player)`: the crystal is the direct entity.
if kind == DamageKind::Explosion {
@@ -666,6 +666,7 @@ impl Game<'_> {
.filter(|&o| o != m.id)
.map(horizon_client::mobs::Who::Mob),
from: Some(center),
attacker_species: cause,
};
// `EXPLOSION_KNOCKBACK_RESISTANCE`, not `KNOCKBACK_RESISTANCE`:
// its base is 0 for every mob type, so iron golems and
+1
View File
@@ -167,6 +167,7 @@ impl Game<'_> {
kind: horizon_client::mobs::DamageKind::Magic,
attacker: owner.map(horizon_client::mobs::Who::Mob),
from: Some(f.pos),
attacker_species: None,
},
);
}
@@ -547,6 +547,7 @@ impl Game<'_> {
kind: DamageKind::Generic,
attacker: None,
from: Some(b.at),
attacker_species: None,
};
let mut struck = Vec::new();
let mut events = Vec::new();
@@ -10993,6 +10993,7 @@ mod tests {
kind,
attacker,
from: None,
attacker_species: None,
});
(s.damage_type, s.causing, s.direct)
};
@@ -775,6 +775,7 @@ impl Game<'_> {
kind: DamageKind::Arrow,
attacker: Some(Who::Player),
from,
attacker_species: None,
},
aim,
[t.velocity.x, t.velocity.z],
+1
View File
@@ -156,6 +156,7 @@ impl Game<'_> {
kind: DamageKind::Thrown,
attacker: p.owner,
from: Some([p.previous.x, p.previous.y, p.previous.z]),
attacker_species: owner_breeze.then_some(Species::Breeze),
};
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {
// Breeze.isInvulnerableTo: its own kind's charges.
+4 -3
View File
@@ -192,14 +192,15 @@ impl Game<'_> {
self.hit_special(id, amount, by_player, kind, Some(&p.kind));
}
Some(Who::Mob(id)) => {
let owner_species = owner
.and_then(|o| self.mobs.iter().find(|m| m.id == o))
.map(|m| m.species());
let source = Damage {
kind,
attacker: live_owner,
from: skull.from,
attacker_species: owner_species,
};
let owner_species = owner
.and_then(|o| self.mobs.iter().find(|m| m.id == o))
.map(|m| m.species());
let killed = match self.mobs.iter_mut().find(|m| m.id == id) {
// `WitherBoss.hurtServer`: nothing from a wither friend.
Some(m)
+1
View File
@@ -1222,6 +1222,7 @@ mod tests {
kind: DamageKind::PlayerAttack,
attacker: None,
from: Some([3.0, 0.0, 0.0]),
attacker_species: None,
};
// Canceled: no damage, cooldown, sound or knockback.
let mut kept = mob(9001);
+16
View File
@@ -755,6 +755,7 @@ pub struct Damage {
pub kind: DamageKind,
pub attacker: Option<Who>,
pub from: Option<[f64; 3]>,
pub attacker_species: Option<Species>,
}
impl Damage {
pub fn fall() -> Self {
@@ -762,6 +763,7 @@ impl Damage {
kind: DamageKind::Fall,
attacker: None,
from: None,
attacker_species: None,
}
}
pub fn of(kind: DamageKind) -> Self {
@@ -769,6 +771,7 @@ impl Damage {
kind,
attacker: None,
from: None,
attacker_species: None,
}
}
pub fn player(from: [f64; 3]) -> Self {
@@ -776,6 +779,7 @@ impl Damage {
kind: DamageKind::PlayerAttack,
attacker: Some(Who::Player),
from: Some(from),
attacker_species: None,
}
}
/// `DamageSources.mace`: a player's smash attack.
@@ -784,6 +788,7 @@ impl Damage {
kind: DamageKind::MaceSmash,
attacker: Some(Who::Player),
from: Some(from),
attacker_species: None,
}
}
/// `DamageSources.spear`: a player's stab with a spear.
@@ -792,6 +797,7 @@ impl Damage {
kind: DamageKind::Spear,
attacker: Some(Who::Player),
from: Some(from),
attacker_species: None,
}
}
/// `DamageSources.indirectMagic(potion, owner)`: `attacker` is the owner
@@ -802,6 +808,7 @@ impl Damage {
kind: DamageKind::IndirectMagic,
attacker,
from: Some(from),
attacker_species: None,
}
}
/// `DamageSources.fireworks(rocket, owner)`: `attacker` is the owner
@@ -812,6 +819,7 @@ impl Damage {
kind: DamageKind::Fireworks,
attacker,
from: Some(from),
attacker_species: None,
}
}
pub fn mob(id: u32, from: [f64; 3]) -> Self {
@@ -819,8 +827,13 @@ impl Damage {
kind: DamageKind::MobAttack,
attacker: Some(Who::Mob(id)),
from: Some(from),
attacker_species: None,
}
}
pub fn with_species(mut self, species: Species) -> Self {
self.attacker_species = Some(species);
self
}
/// DamageTypeTags.PANIC_CAUSES
pub fn causes_panic(&self) -> bool {
if let DamageKind::Named(named) = self.kind {
@@ -4732,6 +4745,7 @@ mod fire_resistance_damage_tests {
kind: DamageKind::Fireball,
attacker: Some(Who::Player),
from: None,
attacker_species: None,
};
assert!(mob.damage(1.0, source));
assert_eq!(mob.health, 10.0);
@@ -4817,6 +4831,7 @@ mod magic_absorb_tests {
kind: DamageKind::IndirectMagic,
attacker: Some(Who::Mob(m.id)),
from: None,
attacker_species: Some(Species::Witch),
};
m.damage(20.0, own);
assert_eq!(m.health, 10.0);
@@ -4885,6 +4900,7 @@ mod armor_hurt_tests {
kind: DamageKind::MobAttack,
attacker: None,
from: None,
attacker_species: None,
};
let mut m = horse();
m.extra.body = Some("minecraft:diamond_horse_armor".into());
+2 -1
View File
@@ -2436,6 +2436,7 @@ pub fn walk_target_value(m: &Mob, w: &MobWorld, p: [i32; 3]) -> f32 {
/// Species damage filters (LivingEntity.hurtServer overrides).
pub fn accepts_damage(m: &mut Mob, source: &Damage, _amount: f32) -> bool {
match m.species() {
Species::Breeze => super::breeze::accepts_damage(m, source),
// Allay.hurtServer: its liked player cannot hurt it.
Species::Allay if m.extra.tame && source.attacker == Some(Who::Player) => false,
Species::Creaking => super::creaking::accepts_damage(m, source),
@@ -2526,7 +2527,7 @@ pub fn do_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
}
let mut amount = super::gear::attack_damage(m, cx.w.items);
let from = [m.position.x, m.position.y, m.position.z];
let source = Damage::mob(m.id, from);
let source = Damage::mob(m.id, from).with_species(m.species());
let first_event = cx.events.len();
// `CaveSpider`, `Husk` and `WitherSkeleton.doHurtTarget` all test
// `target instanceof LivingEntity`, so the effect reaches a mob the hit
+1
View File
@@ -349,6 +349,7 @@ pub fn sting(m: &mut Mob, who: Who, cx: &mut Cx) {
kind: DamageKind::Sting,
attacker: Some(Who::Mob(m.id)),
from: Some(from),
attacker_species: Some(Species::Bee),
};
match who {
Who::Player => {
+186 -1
View File
@@ -2,7 +2,7 @@
//! arithmetic, the sounds of `Breeze.tick` and `BreezeModel`'s layers. The
//! brain (`BreezeAi` and its behaviours) is in `monster_brain`.
use super::{
Mob, MobEvent, MobWorld, Species,
Damage, Mob, MobEvent, MobWorld, Species, Who,
anim::{Poses, State},
keyframes::Animation,
};
@@ -29,6 +29,10 @@ pub fn register(m: &mut Mob) {
m.set_malus(super::path::PathType::OnTopOfTrapdoor, -1.0);
m.set_malus(super::path::PathType::Fire, -1.0);
}
/// `Breeze.isInvulnerableTo`: another breeze (or itself).
pub fn accepts_damage(m: &Mob, source: &Damage) -> bool {
!(source.attacker == Some(Who::Mob(m.id)) || source.attacker_species == Some(Species::Breeze))
}
pub fn pose(m: &Mob) -> u8 {
m.extra.variant3
}
@@ -479,3 +483,184 @@ mod ground_particle_tests {
let _ = ran;
}
}
#[cfg(test)]
mod invulnerability_tests {
use super::*;
use crate::mobs::{DamageKind, sensor_scan_tests::kind};
fn test_breeze(id: u32) -> Mob {
let mut kind = kind();
let k = std::sync::Arc::make_mut(&mut kind);
k.id = "minecraft:breeze".into();
k.species = Species::Breeze;
Mob::new(id, kind, Vec3::ZERO, 0.0)
}
/// Scenario 1: Default/legacy behavior preserved:
/// Breeze takes normal damage from player, generic damage, fall damage.
#[test]
fn breeze_takes_normal_damage_from_player_fall_and_generic() {
let mut b = test_breeze(1);
let initial_health = b.health;
assert!(initial_health > 0.0);
// Player attack
let player_hit = Damage::player([0.0, 0.0, 0.0]);
assert!(
b.damage(2.0, player_hit),
"player attack should hurt breeze"
);
assert_eq!(b.health, initial_health - 2.0);
// Fall damage
b.invulnerable = 0;
let health_before_fall = b.health;
assert!(
b.damage(3.0, Damage::fall()),
"fall damage should hurt breeze"
);
assert_eq!(b.health, health_before_fall - 3.0);
// Generic damage
b.invulnerable = 0;
let health_before_generic = b.health;
assert!(
b.damage(1.0, Damage::of(DamageKind::Generic)),
"generic damage should hurt breeze"
);
assert_eq!(b.health, health_before_generic - 1.0);
}
/// Scenario 2: Blocking/rejection of disallowed actions:
/// Breeze rejects damage when attacker is another Breeze (`source.attacker_species == Some(Species::Breeze)`),
/// returning `false` from `m.damage(...)` and taking no damage.
#[test]
fn breeze_rejects_damage_from_another_breeze() {
let mut b = test_breeze(1);
let initial_health = b.health;
// Direct mob attack from another breeze
let other_breeze_mob = Damage::mob(2, [0.0, 0.0, 0.0]).with_species(Species::Breeze);
assert!(
!b.damage(5.0, other_breeze_mob),
"breeze should reject damage from another breeze"
);
assert_eq!(b.health, initial_health, "health must not change");
// Thrown wind charge or projectile from another breeze
let other_breeze_proj = Damage {
kind: DamageKind::Thrown,
attacker: Some(Who::Mob(2)),
from: Some([1.0, 0.0, 0.0]),
attacker_species: Some(Species::Breeze),
};
assert!(
!b.damage(3.0, other_breeze_proj),
"breeze should reject projectile damage from another breeze"
);
assert_eq!(b.health, initial_health, "health must remain intact");
// Directly verify breeze::accepts_damage
assert!(!accepts_damage(&b, &other_breeze_mob));
assert!(!accepts_damage(&b, &other_breeze_proj));
}
/// Scenario 3: Successful execution when requirements are met:
/// Breeze accepts damage from other mob species (e.g. Zombie, Skeleton) when `source.attacker_species` is Zombie/Skeleton.
#[test]
fn breeze_accepts_damage_from_other_mob_species() {
let mut b = test_breeze(1);
let initial_health = b.health;
// Attack from Zombie
let zombie_hit = Damage::mob(5, [0.0, 0.0, 0.0]).with_species(Species::Zombie);
assert!(
accepts_damage(&b, &zombie_hit),
"breeze accepts damage from zombie"
);
assert!(
b.damage(4.0, zombie_hit),
"breeze must take damage from zombie"
);
assert_eq!(b.health, initial_health - 4.0);
// Attack from Skeleton
b.invulnerable = 0;
let skeleton_hit = Damage::mob(6, [0.0, 0.0, 0.0]).with_species(Species::Skeleton);
assert!(
accepts_damage(&b, &skeleton_hit),
"breeze accepts damage from skeleton"
);
assert!(
b.damage(2.0, skeleton_hit),
"breeze must take damage from skeleton"
);
assert_eq!(b.health, initial_health - 6.0);
}
/// Scenario 4: Special / edge cases:
/// Breeze rejects self-inflicted damage (`source.attacker == Some(Who::Mob(m.id))`),
/// and verify bypasses_invulnerability behavior if applicable.
#[test]
fn breeze_rejects_self_damage_and_respects_invulnerability_flags() {
let mut b = test_breeze(1);
let initial_health = b.health;
// Self-inflicted damage with no species tag
let self_hit_no_species = Damage::mob(b.id, [0.0, 0.0, 0.0]);
assert!(
!accepts_damage(&b, &self_hit_no_species),
"breeze rejects self-hit even without species"
);
assert!(
!b.damage(5.0, self_hit_no_species),
"breeze takes no damage from itself"
);
assert_eq!(b.health, initial_health);
// Self-inflicted damage with species tag
let self_hit_with_species =
Damage::mob(b.id, [0.0, 0.0, 0.0]).with_species(Species::Breeze);
assert!(
!accepts_damage(&b, &self_hit_with_species),
"breeze rejects self-hit with species"
);
assert!(
!b.damage(5.0, self_hit_with_species),
"breeze takes no damage from itself"
);
assert_eq!(b.health, initial_health);
// Invulnerable flag on Mob:
b.invulnerable_flag = true;
// Normal damage does not bypass invulnerability flag
let normal_player = Damage::player([0.0, 0.0, 0.0]);
assert!(
!b.damage(5.0, normal_player),
"invulnerable_flag blocks normal player damage"
);
assert_eq!(b.health, initial_health);
// GenericKill bypasses invulnerability flag
let kill_damage = Damage::of(DamageKind::GenericKill);
assert!(kill_damage.kind.bypasses_invulnerability());
assert!(
b.damage(1.0, kill_damage),
"GenericKill bypasses invulnerability flag"
);
assert_eq!(b.health, initial_health - 1.0);
// But another Breeze with GenericKill is still rejected by Breeze.isInvulnerableTo
b.invulnerable = 0;
let breeze_kill = Damage::of(DamageKind::GenericKill).with_species(Species::Breeze);
assert!(
!accepts_damage(&b, &breeze_kill),
"Breeze.isInvulnerableTo rejects breeze attacker even for GenericKill"
);
assert!(
!b.damage(1.0, breeze_kill),
"breeze takes no damage from breeze GenericKill"
);
}
}
+1
View File
@@ -61,6 +61,7 @@ mod tests {
kind: DamageKind::MobAttack,
attacker,
from: None,
attacker_species: None,
},
damage: 2.0,
in_water: false,
@@ -25,6 +25,7 @@ pub fn contact(m: &mut Mob, f: &Frame, teams_allow: bool) {
kind: DamageKind::MobAttack,
attacker: Some(Who::Mob(f.dragon_id)),
from: Some([f.origin.x, f.origin.y, f.origin.z]),
attacker_species: None,
};
for wing in &f.wings {
if wing.intersects(bounds) {
@@ -140,6 +140,7 @@ impl Goal for GuardianAttack {
kind: DamageKind::Magic,
attacker: Some(Who::Mob(m.id)),
from: Some(from),
attacker_species: Some(m.species()),
},
}),
}
@@ -313,6 +314,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
kind: DamageKind::Thorns,
attacker: Some(Who::Mob(m.id)),
from: Some(from),
attacker_species: Some(m.species()),
},
}),
None => {}
@@ -242,6 +242,7 @@ fn stab_attack(
kind: DamageKind::Spear,
attacker: Some(Who::Mob(m.id)),
from: Some(from),
attacker_species: Some(m.species()),
};
let mut affected = deals_knockback;
if deals_damage {