mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10075 from Ragbie/fix/nautilus-sight-armadillo-panic
A nautilus only goes for a pufferfish it sees, and frost panics armadillos
This commit is contained in:
5 files changed
+210
-33
No files matched your search
@@ -2030,7 +2030,9 @@ fn species_ai_step(
|
||||
super::cube::ai_step(m, w, events);
|
||||
super::sulfur_cube::ai_step(m, w, events);
|
||||
}
|
||||
Species::Nautilus | Species::ZombieNautilus => super::nautilus::ai_step(m, w, events),
|
||||
Species::Nautilus | Species::ZombieNautilus => {
|
||||
super::nautilus::ai_step(m, w, level_random, events)
|
||||
}
|
||||
Species::Chicken => farm::chicken_ai_step(m, w, events),
|
||||
Species::Parrot => super::parrot::calculate_flapping(m),
|
||||
Species::Armadillo => super::armadillo::tick(m),
|
||||
|
||||
@@ -272,6 +272,8 @@ pub fn environmental(kind: DamageKind) -> bool {
|
||||
| DamageKind::Lava
|
||||
| DamageKind::LightningBolt
|
||||
| DamageKind::HotFloor
|
||||
| DamageKind::Freeze
|
||||
| DamageKind::SulfurCubeHot
|
||||
)
|
||||
}
|
||||
/// The hurt sound: a ball's is muffled, and it has no ambient sound
|
||||
@@ -806,3 +808,44 @@ mod tests {
|
||||
assert!(!environmental(DamageKind::Campfire));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod panic_environmental_tests {
|
||||
use super::*;
|
||||
|
||||
/// `#panic_environmental_causes` from the vanilla data: every type in it
|
||||
/// panics the armadillo (`Armadillo.actuallyHurt` rolls out).
|
||||
#[test]
|
||||
fn the_environmental_causes_are_vanillas_tag() {
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION)
|
||||
.join("data/minecraft/tags/damage_type/panic_environmental_causes.json");
|
||||
let Ok(text) = std::fs::read_to_string(path) else {
|
||||
return;
|
||||
};
|
||||
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
let mut ids: Vec<&str> = json["values"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap())
|
||||
.collect();
|
||||
ids.sort_unstable();
|
||||
let kinds = [
|
||||
("minecraft:cactus", DamageKind::Cactus),
|
||||
("minecraft:freeze", DamageKind::Freeze),
|
||||
("minecraft:hot_floor", DamageKind::HotFloor),
|
||||
("minecraft:in_fire", DamageKind::InFire),
|
||||
("minecraft:lava", DamageKind::Lava),
|
||||
("minecraft:lightning_bolt", DamageKind::LightningBolt),
|
||||
("minecraft:on_fire", DamageKind::OnFire),
|
||||
("minecraft:sulfur_cube_hot", DamageKind::SulfurCubeHot),
|
||||
];
|
||||
assert_eq!(ids, kinds.map(|(id, _)| id).to_vec());
|
||||
for (id, kind) in kinds {
|
||||
assert!(environmental(kind), "{id}");
|
||||
}
|
||||
assert!(!environmental(DamageKind::Drown));
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,12 @@ fn check_restriction(m: &mut Mob) {
|
||||
/// `AbstractNautilus.tick` and the brains' memories: the rider's breath, the
|
||||
/// dash clock, bubbles, and choosing whom to charge at
|
||||
/// (`NautilusAi.findNearestValidAttackTarget`).
|
||||
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
pub fn ai_step(
|
||||
m: &mut Mob,
|
||||
w: &MobWorld,
|
||||
level_random: &mut horizon_core::random::LegacyRandom,
|
||||
events: &mut Vec<MobEvent>,
|
||||
) {
|
||||
// customServerAiStep: the home is kept before the rest of the step.
|
||||
check_restriction(m);
|
||||
if m.extra.scratch[1] != 0 {
|
||||
@@ -321,7 +326,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.extra.anger -= 1;
|
||||
}
|
||||
if m.ai.target.is_none() && m.alive() && !m.no_ai {
|
||||
m.ai.target = find_target(m, w);
|
||||
m.ai.target = find_target(m, w, level_random);
|
||||
}
|
||||
}
|
||||
/// `spawnBubbles`: more as it swims faster, from behind its mouth.
|
||||
@@ -375,7 +380,11 @@ pub fn aggravated(m: &Mob) -> bool {
|
||||
}
|
||||
/// `NautilusAi.findNearestValidAttackTarget`: whoever it is angry at, else
|
||||
/// now and then (a coin flip every 2400 to 3600 ticks) the nearest pufferfish.
|
||||
fn find_target(m: &mut Mob, w: &MobWorld) -> Option<Who> {
|
||||
fn find_target(
|
||||
m: &mut Mob,
|
||||
w: &MobWorld,
|
||||
level_random: &mut horizon_core::random::LegacyRandom,
|
||||
) -> Option<Who> {
|
||||
if m.extra.in_love > 0 || !m.fluids.in_water || m.is_baby() || m.extra.tame {
|
||||
return None;
|
||||
}
|
||||
@@ -391,15 +400,26 @@ fn find_target(m: &mut Mob, w: &MobWorld) -> Option<Who> {
|
||||
if m.extra.counter > 0 {
|
||||
return None;
|
||||
}
|
||||
m.extra.counter = 2400 + m.random.next_int_bound(1201).unwrap_or(0);
|
||||
if m.random.next_float() < 0.5 {
|
||||
// The cooldown and the coin come from the level's random (`level.getRandom()`).
|
||||
m.extra.counter = 2400 + level_random.next_int_bound(1201).unwrap_or(0);
|
||||
if level_random.next_float() < 0.5 {
|
||||
return None;
|
||||
}
|
||||
w.nearby(m.position, 16.0, |s| {
|
||||
s.species == Species::Pufferfish && in_water_at(w, s.position)
|
||||
})
|
||||
.next()
|
||||
.map(|s| Who::Mob(s.id))
|
||||
// NEAREST_VISIBLE_LIVING_ENTITIES.findClosest(isHostileTarget): the
|
||||
// nearest #nautilus_hostiles in water that the nautilus can see
|
||||
// (`Sensor.isEntityTargetable`: line of sight).
|
||||
let mut near: Vec<(f64, u32)> = w
|
||||
.nearby(m.position, 16.0, |s| {
|
||||
s.species == Species::Pufferfish && in_water_at(w, s.position)
|
||||
})
|
||||
.map(|s| (s.position.distance_squared(m.position), s.id))
|
||||
// TargetingConditions.range(16): feet to feet.
|
||||
.filter(|&(d, _)| d <= 16.0 * 16.0)
|
||||
.collect();
|
||||
near.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
near.into_iter()
|
||||
.map(|(_, id)| Who::Mob(id))
|
||||
.find(|&who| m.sees(w, who))
|
||||
}
|
||||
/// `hurtServer`: `NautilusAi.setAngerTarget` for 400 ticks.
|
||||
pub fn after_hurt(m: &mut Mob, source: &super::Damage) {
|
||||
@@ -790,3 +810,83 @@ mod tests {
|
||||
assert_eq!(m.rider.expect("the rider stays on").jump, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hostile_target_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A pool from x -8 to 8, with a stone wall at x = 3 when `wall`.
|
||||
fn pool(
|
||||
wall: bool,
|
||||
) -> impl FnOnce(&mut crate::world::World, &horizon_world::registry::BlockRegistry) {
|
||||
move |world, registry| {
|
||||
let water = registry.default_state("minecraft:water").unwrap();
|
||||
let stone = registry.default_state("minecraft:stone").unwrap();
|
||||
for cx in -1..2 {
|
||||
for cz in -1..1 {
|
||||
world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(crate::chunk::Chunk::empty(cx, cz)));
|
||||
}
|
||||
}
|
||||
for x in -8..=18 {
|
||||
for y in 60..=66 {
|
||||
for z in -4..=4 {
|
||||
let block = if wall && x == 3 { stone } else { water };
|
||||
world.set(x, y, z, block).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `NautilusAi.findNearestValidAttackTarget`: past the cooldown and the
|
||||
/// level random's 1 in 2, the target is the closest #nautilus_hostiles in
|
||||
/// water among NEAREST_VISIBLE_LIVING_ENTITIES, so not a pufferfish behind
|
||||
/// stone. Each seed is a fresh level random.
|
||||
fn targets(wall: bool, fish_at: Vec3) -> Option<Vec<Option<Who>>> {
|
||||
let mut out = None;
|
||||
let mut fish = None;
|
||||
crate::mobs::world::with_built_test_world(
|
||||
&[],
|
||||
|_, _| {},
|
||||
|_, types| {
|
||||
let kind = Arc::new(types.types["minecraft:pufferfish"].clone());
|
||||
fish = Some(Mob::new(2, kind, fish_at, 0.0).seen(None));
|
||||
},
|
||||
);
|
||||
let fish = fish?;
|
||||
crate::mobs::world::with_built_test_world(&[fish], pool(wall), |w, types| {
|
||||
let kind = Arc::new(types.types["minecraft:nautilus"].clone());
|
||||
let picks = (0..16)
|
||||
.map(|seed| {
|
||||
let mut m = Mob::new(1, kind.clone(), Vec3::new(0.5, 62.0, 0.5), 270.0);
|
||||
m.fluids.in_water = true;
|
||||
let mut level = horizon_core::random::LegacyRandom::new(seed);
|
||||
find_target(&mut m, w, &mut level)
|
||||
})
|
||||
.collect();
|
||||
out = Some(picks);
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nautilus_only_targets_a_pufferfish_it_sees() {
|
||||
let near = Vec3::new(6.5, 62.0, 0.5);
|
||||
// 16 out on x and 3 on z: inside the sensor's box, past range(16).
|
||||
let far = Vec3::new(16.5, 62.0, 3.5);
|
||||
let (Some(open), Some(walled), Some(beyond)) = (
|
||||
targets(false, near),
|
||||
targets(true, near),
|
||||
targets(false, far),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert!(open.contains(&Some(Who::Mob(2))), "{open:?}");
|
||||
assert!(open.iter().all(|t| t.is_none() || *t == Some(Who::Mob(2))));
|
||||
assert!(walled.iter().all(Option::is_none), "{walled:?}");
|
||||
assert!(beyond.iter().all(Option::is_none), "{beyond:?}");
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
//! (`MAIN_GENE_ID`, `HIDDEN_GENE_ID`); the rest is [`State`], whose `flags`,
|
||||
//! `unhappy`, `sneeze` and `eat` are the synched data a server sends.
|
||||
use super::{
|
||||
DamageKind, Mob, MobEvent, MobWorld, Species, Who,
|
||||
Mob, MobEvent, MobWorld, Species, Who,
|
||||
body_anim::Panda as Pose,
|
||||
finalize::{self, Attr},
|
||||
goals::{
|
||||
@@ -708,19 +708,11 @@ impl Goal for PandaPanic {
|
||||
self.0.stop(m, cx)
|
||||
}
|
||||
}
|
||||
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES`, or burning.
|
||||
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES` (`armadillo::environmental`), or burning.
|
||||
fn environmental(m: &Mob) -> bool {
|
||||
m.fire_ticks > 0 && !m.kind.flags.fire_immune
|
||||
|| m.last_damage.is_some_and(|(d, t)| {
|
||||
matches!(
|
||||
d.kind,
|
||||
DamageKind::OnFire
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Cactus
|
||||
| DamageKind::LightningBolt
|
||||
| DamageKind::HotFloor
|
||||
) && i64::from(m.age) - t <= 40
|
||||
super::armadillo::environmental(d.kind) && i64::from(m.age) - t <= 40
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1095,3 +1087,27 @@ pub fn register(m: &mut Mob) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod freeze_panic_tests {
|
||||
use super::*;
|
||||
|
||||
/// `#panic_environmental_causes` holds freeze and sulfur_cube_hot too.
|
||||
#[test]
|
||||
fn freezing_and_a_hot_sulfur_cube_are_environmental() {
|
||||
let kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
|
||||
for cause in [
|
||||
super::super::DamageKind::Freeze,
|
||||
super::super::DamageKind::SulfurCubeHot,
|
||||
] {
|
||||
let mut m = Mob::new(
|
||||
1,
|
||||
std::sync::Arc::new(kind.clone()),
|
||||
horizon_core::geometry::Vec3::ZERO,
|
||||
0.0,
|
||||
);
|
||||
m.last_damage = Some((super::super::Damage::of(cause), i64::from(m.age)));
|
||||
assert!(environmental(&m), "{cause:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
|
||||
use super::{
|
||||
DamageKind, Mob, MobWorld, Species, Who,
|
||||
Mob, MobWorld, Species, Who,
|
||||
goals::{
|
||||
Cx, Float, FollowParent, Goal, HurtByTarget, LookAtPlayer, MOVE, MeleeAttack,
|
||||
NearestTarget, Panic, RandomLookAround, Stroll, goal_clone,
|
||||
@@ -88,20 +88,12 @@ impl Goal for PolarBearPanic {
|
||||
self.0.stop(m, cx)
|
||||
}
|
||||
}
|
||||
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES` (fire, lava, cactus), or
|
||||
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES` (`armadillo::environmental`), or
|
||||
/// burning.
|
||||
fn environmental_panic(m: &Mob) -> bool {
|
||||
m.fire_ticks > 0 && !m.kind.flags.fire_immune
|
||||
|| m.last_damage.is_some_and(|(d, t)| {
|
||||
matches!(
|
||||
d.kind,
|
||||
DamageKind::OnFire
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Cactus
|
||||
| DamageKind::LightningBolt
|
||||
| DamageKind::HotFloor
|
||||
) && i64::from(m.age) - t <= 40
|
||||
super::armadillo::environmental(d.kind) && i64::from(m.age) - t <= 40
|
||||
})
|
||||
}
|
||||
|
||||
@@ -276,3 +268,27 @@ mod tests {
|
||||
assert_eq!(m.dims().1, f64::from(base_h * (1.0 + 1.0 / STAND_TICKS)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod freeze_panic_tests {
|
||||
use super::*;
|
||||
|
||||
/// `#panic_environmental_causes` holds freeze and sulfur_cube_hot too.
|
||||
#[test]
|
||||
fn freezing_and_a_hot_sulfur_cube_are_environmental() {
|
||||
let kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
|
||||
for cause in [
|
||||
super::super::DamageKind::Freeze,
|
||||
super::super::DamageKind::SulfurCubeHot,
|
||||
] {
|
||||
let mut m = Mob::new(
|
||||
1,
|
||||
std::sync::Arc::new(kind.clone()),
|
||||
horizon_core::geometry::Vec3::ZERO,
|
||||
0.0,
|
||||
);
|
||||
m.last_damage = Some((super::super::Damage::of(cause), i64::from(m.age)));
|
||||
assert!(environmental_panic(&m), "{cause:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user