mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10034 from Ragbie/fix/mob-spear-reach
A mob's melee reach follows the spear it holds
This commit is contained in:
4 files changed
+138
-11
No files matched your search
@@ -220,6 +220,7 @@ fn goal_name(m: &Mob, name: &str) -> String {
|
||||
"RandomSwimming" => "RandomSwimmingGoal",
|
||||
"LookAtPlayer" => "LookAtPlayerGoal",
|
||||
"RandomLookAround" => "RandomLookAroundGoal",
|
||||
"SpearUse" => "SpearUseGoal",
|
||||
"HurtByTarget" => "HurtByTargetGoal",
|
||||
"NearestTarget" => "NearestAttackableTargetGoal",
|
||||
"Panic" => "PanicGoal",
|
||||
|
||||
@@ -246,12 +246,8 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
// Player.touch: entities within the player's box grown by (1, 0.5, 1).
|
||||
&& m.bounds().intersects(p.bounds.inflate(Vec3::new(1.0, 0.5, 1.0)))
|
||||
{
|
||||
let reach = 2.04f32.sqrt() as f64 - 0.6;
|
||||
if m.bounds()
|
||||
.inflate(Vec3::new(reach, 0.0, reach))
|
||||
.intersects(p.bounds)
|
||||
&& m.sees(w, Who::Player)
|
||||
{
|
||||
// isWithinMeleeAttackRange, with what the cube holds.
|
||||
if m.melee_reaches(p.bounds) && m.sees(w, Who::Player) {
|
||||
events.push(MobEvent::CubeHurtPlayer {
|
||||
cube: m.id,
|
||||
amount: attack_damage(m),
|
||||
@@ -274,7 +270,8 @@ fn attack_damage(m: &Mob) -> f32 {
|
||||
/// `AbstractCubeMob.push(entity)`: an iron golem whose `pushEntities` takes
|
||||
/// this cube (its box overlaps the cube's, `EntitySelector.pushableBy`) gets
|
||||
/// `dealDamage`: alive, `doTeamsAllowDamage`, `isWithinMeleeAttackRange`
|
||||
/// (always true for overlapping boxes) and `hasLineOfSight`, then a
|
||||
/// (true for overlapping boxes unless a held spear's minimum range refuses
|
||||
/// them) and `hasLineOfSight`, then a
|
||||
/// `mobAttack` hit for `getAttackDamage`.
|
||||
fn hurt_touching_golems(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
// canDealDamage also needs Mob.isEffectiveAi: a NoAI cube never hurts.
|
||||
@@ -292,6 +289,7 @@ fn hurt_touching_golems(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
&& o.species == Species::IronGolem
|
||||
&& o.alive
|
||||
&& o.bounds.intersects(bounds)
|
||||
&& m.melee_reaches(o.bounds)
|
||||
&& crate::scoreboard::pushable_by(theirs, own, pushable)
|
||||
&& crate::scoreboard::teams_allow_damage(own, theirs)
|
||||
})
|
||||
|
||||
@@ -655,11 +655,63 @@ impl Mob {
|
||||
self.ai.seen_cache.push((who, v));
|
||||
v
|
||||
}
|
||||
/// Mob.isWithinMeleeAttackRange: the box inflated by sqrt(2.04) - 0.6.
|
||||
/// Mob.isWithinMeleeAttackRange: the box inflated by sqrt(2.04) - 0.6, or
|
||||
/// for an active item with `attack_range` (the spears) by its
|
||||
/// `effectiveMaxRange` for a mob (`max_reach * mob_factor`), and not
|
||||
/// within its `effectiveMinRange` (`min_reach * mob_factor`).
|
||||
pub fn within_melee_range(&self, t: &TargetInfo) -> bool {
|
||||
let reach = 2.04f32.sqrt() as f64 - 0.6;
|
||||
let b = self.bounds().inflate(Vec3::new(reach, 0.0, reach));
|
||||
b.intersects(t.bounds)
|
||||
self.melee_reaches(t.bounds)
|
||||
}
|
||||
/// [`Self::within_melee_range`] for a target's box (`getHitbox`).
|
||||
pub fn melee_reaches(&self, target: Aabb) -> bool {
|
||||
let (max, min) = match self.active_attack_range() {
|
||||
Some((max, min)) => (f64::from(max), f64::from(min)),
|
||||
None => (2.04f32.sqrt() as f64 - 0.6, 0.0),
|
||||
};
|
||||
let reaches = |range: f64| {
|
||||
self.bounds()
|
||||
.inflate(Vec3::new(range, 0.0, range))
|
||||
.intersects(target)
|
||||
};
|
||||
reaches(max) && (min <= 0.0 || !reaches(min))
|
||||
}
|
||||
/// The active item's `attack_range` for a mob: `effectiveMaxRange` and
|
||||
/// `effectiveMinRange` (`max_reach` and `min_reach` times `mob_factor`).
|
||||
/// A patch on the held stack wins (`!minecraft:attack_range` removes it;
|
||||
/// the codec's defaults are 0, 3 and a factor of 1); else the spears'.
|
||||
fn active_attack_range(&self) -> Option<(f32, f32)> {
|
||||
let item = self.active_item()?;
|
||||
let patch = (self.extra.main_hand.as_deref() == Some(item))
|
||||
.then(|| self.extra.main_hand_stack().and_then(|s| s.data()))
|
||||
.flatten();
|
||||
if patch.is_some_and(|d| d.other("!minecraft:attack_range").is_some()) {
|
||||
return None;
|
||||
}
|
||||
if let Some(range) = patch.and_then(|d| d.other("minecraft:attack_range")) {
|
||||
let float = |key: &str, default: f32| match range.get(key) {
|
||||
Some(horizon_world::nbt::Tag::Float(bits)) => f32::from_bits(*bits),
|
||||
Some(horizon_world::nbt::Tag::Double(bits)) => f64::from_bits(*bits) as f32,
|
||||
Some(horizon_world::nbt::Tag::Int(v)) => *v as f32,
|
||||
_ => default,
|
||||
};
|
||||
let factor = float("mob_factor", 1.0);
|
||||
return Some((
|
||||
float("max_reach", 3.0) * factor,
|
||||
float("min_reach", 0.0) * factor,
|
||||
));
|
||||
}
|
||||
crate::spear::Kinetic::of(item).map(|_| {
|
||||
let r = &crate::spear::REACH;
|
||||
(r.max * r.mob_factor, r.min * r.mob_factor)
|
||||
})
|
||||
}
|
||||
/// `LivingEntity.getActiveItem`: the item being used, else the main hand's.
|
||||
pub fn active_item(&self) -> Option<&str> {
|
||||
if super::spear_use::is_using_item(self) && self.extra.using_off_hand {
|
||||
self.extra.off_hand.as_deref()
|
||||
} else {
|
||||
self.extra.main_hand.as_deref()
|
||||
}
|
||||
}
|
||||
/// Whether the species is a `TamableAnimal` (not the horses and llamas,
|
||||
/// which are tamed another way).
|
||||
@@ -3319,3 +3371,63 @@ mod breed_xp_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod melee_range_tests {
|
||||
use super::*;
|
||||
|
||||
fn mob_holding(item: &str, components: Option<&str>) -> Mob {
|
||||
let kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
|
||||
let mut m = Mob::new(1, Arc::new(kind), Vec3::new(0.5, 64.0, 0.5), 0.0);
|
||||
let mut stack = crate::items::ItemStack::new(item, 1);
|
||||
if let Some(text) = components {
|
||||
let tag = horizon_world::nbt::parse_snbt(text).unwrap().0;
|
||||
(stack.damage, stack.extra) = crate::item_data::decode_components(Some(&tag));
|
||||
}
|
||||
m.extra.set_main_hand_stack(Some(stack));
|
||||
m
|
||||
}
|
||||
/// A one-block target whose near face is `gap` blocks east of the mob's box.
|
||||
fn target(m: &Mob, gap: f64) -> Aabb {
|
||||
let x = m.bounds().max_position().x + gap;
|
||||
Aabb::new(Vec3::new(x, 64.0, 0.0), Vec3::new(x + 1.0, 65.0, 1.0))
|
||||
}
|
||||
|
||||
/// `Mob.isWithinMeleeAttackRange` with `getActiveItem().get(ATTACK_RANGE)`:
|
||||
/// no component reaches sqrt(2.04) - 0.6 (about 0.83); a spear 4.5 and 2.0
|
||||
/// times its mob factor 0.5; a patched component with the codec's
|
||||
/// defaults (`mob_factor` 1, `min_reach` 0); a removed one the default.
|
||||
#[test]
|
||||
fn the_active_items_attack_range_sets_the_reach() {
|
||||
let sword = mob_holding("minecraft:iron_sword", None);
|
||||
assert!(sword.melee_reaches(target(&sword, 0.8)));
|
||||
assert!(!sword.melee_reaches(target(&sword, 0.9)));
|
||||
|
||||
let spear = mob_holding("minecraft:iron_spear", None);
|
||||
assert!(spear.melee_reaches(target(&spear, 2.2)));
|
||||
assert!(!spear.melee_reaches(target(&spear, 2.3)));
|
||||
assert!(
|
||||
!spear.melee_reaches(target(&spear, 0.5)),
|
||||
"inside the minimum range"
|
||||
);
|
||||
assert!(spear.melee_reaches(target(&spear, 1.1)));
|
||||
|
||||
let long = mob_holding(
|
||||
"minecraft:iron_sword",
|
||||
Some(r#"{"minecraft:attack_range":{max_reach:6.0f}}"#),
|
||||
);
|
||||
assert!(long.melee_reaches(target(&long, 5.9)));
|
||||
assert!(
|
||||
long.melee_reaches(target(&long, 0.0)),
|
||||
"no minimum by default"
|
||||
);
|
||||
assert!(!long.melee_reaches(target(&long, 6.1)));
|
||||
|
||||
let blunt = mob_holding(
|
||||
"minecraft:iron_spear",
|
||||
Some(r#"{"!minecraft:attack_range":{}}"#),
|
||||
);
|
||||
assert!(blunt.melee_reaches(target(&blunt, 0.5)));
|
||||
assert!(!blunt.melee_reaches(target(&blunt, 2.0)));
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,22 @@ def scenarios():
|
||||
add(f'{species}_alone', 200, [
|
||||
f'mob minecraft:{species} 0.5 -60 0.5 90 {seed * 7 + 1} persistent',
|
||||
])
|
||||
# A zombie holding a spear (Mob.isWithinMeleeAttackRange): the spear's attack_range times its
|
||||
# mob_factor, 0.5 to 2.25, instead of the default reach; a player too close is out of range.
|
||||
add('zombie_spear_player', 200, ['player 6.5 -60 0.5',
|
||||
'mob minecraft:zombie 0.5 -60 0.5 0 361 persistent hand=iron_spear*1'],
|
||||
'time=18000')
|
||||
add('zombie_spear_close', 120, ['player 1.2 -60 0.5',
|
||||
'mob minecraft:zombie 0.5 -60 0.5 270 362 persistent hand=wooden_spear*1'],
|
||||
'time=18000')
|
||||
# A vindicator has no SpearUseGoal, so its MeleeAttackGoal hits with the spear's range: it must keep
|
||||
# half a block off the player and reaches 2.25 out.
|
||||
add('vindicator_spear_player', 200, ['player 6.5 -60 0.5',
|
||||
'mob minecraft:vindicator 0.5 -60 0.5 0 363 persistent hand=iron_spear*1'],
|
||||
'time=18000')
|
||||
add('vindicator_spear_close', 120, ['player 1.0 -60 0.5',
|
||||
'mob minecraft:vindicator 0.5 -60 0.5 270 364 persistent hand=stone_spear*1'],
|
||||
'time=18000')
|
||||
# Drowned in a pool with the player on the shore and in the water.
|
||||
add('drowned_pool', 200, ['fill -6 -64 -6 6 -61 6 minecraft:water',
|
||||
'player 9.5 -60 0.5',
|
||||
|
||||
Reference in new issue
Block a user